* 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:
@@ -5,7 +5,7 @@ os: linux
|
||||
dist: focal
|
||||
|
||||
node_js:
|
||||
- 12
|
||||
- lts/*
|
||||
|
||||
install:
|
||||
- yarn
|
||||
|
||||
3
.vscode/rrweb-monorepo.code-workspace
vendored
3
.vscode/rrweb-monorepo.code-workspace
vendored
@@ -24,8 +24,7 @@
|
||||
"settings": {
|
||||
"jest.disabledWorkspaceFolders": [
|
||||
" rrweb monorepo",
|
||||
"rrweb-player (package)",
|
||||
"rrdom (package)"
|
||||
"rrweb-player (package)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
2
guide.md
2
guide.md
@@ -299,10 +299,12 @@ The replayer accepts options as its constructor's second parameter, and it has t
|
||||
| insertStyleRules | [] | accepts multiple CSS rule string, which will be injected into the replay iframe |
|
||||
| triggerFocus | true | whether to trigger focus during replay |
|
||||
| UNSAFE_replayCanvas | false | whether to replay the canvas element. **Enable this will remove the sandbox, which is unsafe.** |
|
||||
| pauseAnimation | true | whether to pause CSS animation when the replayer is paused |
|
||||
| mouseTail | true | whether to show mouse tail during replay. Set to false to disable mouse tail. A complete config can be found in this [type](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L407) |
|
||||
| unpackFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
|
||||
| logConfig | - | configuration of console output playback, refer to the [console recipe](./docs/recipes/console.md) |
|
||||
| plugins | [] | load plugins to provide extended replay functions. [What is plugins?](./docs/recipes/plugin.md) |
|
||||
| useVirtualDom | true | whether to use Virtual Dom optimization in the process of skipping to a new point of time |
|
||||
|
||||
#### Use rrweb-player
|
||||
|
||||
|
||||
@@ -295,10 +295,11 @@ replayer.pause(5000);
|
||||
| insertStyleRules | [] | 可以传入多个 CSS rule string,用于自定义回放时 iframe 内的样式 |
|
||||
| triggerFocus | true | 回放时是否回放 focus 交互 |
|
||||
| UNSAFE_replayCanvas | false | 回放时是否回放 canvas 内容,**开启后将会关闭沙盒策略,导致一定风险** |
|
||||
| pauseAnimation | true | 当播放器停止播放时,是否将 CSS 动画也停止播放 |
|
||||
| mouseTail | true | 是否在回放时增加鼠标轨迹。传入 false 可关闭,传入对象可以定制轨迹持续时间、样式等,配置详见[类型](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L407) |
|
||||
| unpackFn | - | 数据解压缩函数,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
|
||||
| logConfig | - | console logger 数据播放设置,详见[console 录制和播放](./docs/recipes/console.zh_CN.md) |
|
||||
| plugins | [] | 加载插件以获得额外的回放功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) |
|
||||
| useVirtualDom | true | 在播放器跳转到一个新的时间点的过程中,是否使用 Virtual Dom 优化 |
|
||||
|
||||
#### 使用 rrweb-player
|
||||
|
||||
|
||||
@@ -30,5 +30,8 @@
|
||||
"test:watch": "yarn lerna run test:watch --parallel",
|
||||
"dev": "yarn lerna run dev --parallel",
|
||||
"repl": "cd packages/rrweb && npm run repl"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/jsdom/cssom": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"dev": "rollup -c -w",
|
||||
"bundle": "rollup --config",
|
||||
"bundle:es-only": "cross-env ES_ONLY=true rollup --config",
|
||||
"check-types": "tsc -noEmit",
|
||||
"test": "jest",
|
||||
"prepublish": "npm run bundle"
|
||||
},
|
||||
@@ -27,18 +28,24 @@
|
||||
"@rollup/plugin-commonjs": "^20.0.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.4",
|
||||
"@types/cssom": "^0.4.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/cssstyle": "^2.2.1",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/nwsapi": "^2.2.2",
|
||||
"jest": "^27.1.1",
|
||||
"@types/puppeteer": "^5.4.4",
|
||||
"compare-versions": "^4.1.3",
|
||||
"jest": "^27.5.1",
|
||||
"puppeteer": "^9.1.1",
|
||||
"rollup": "^2.56.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.30.0",
|
||||
"rollup-plugin-typescript2": "^0.31.2",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"rrweb-snapshot": "^1.1.14",
|
||||
"ts-jest": "^27.0.5",
|
||||
"typescript": "^3.9.5"
|
||||
"ts-jest": "^27.1.3",
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cssom": "^0.5.0",
|
||||
"cssstyle": "^2.3.0",
|
||||
"nwsapi": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
|
||||
import pkg from './package.json';
|
||||
|
||||
function toMinPath(path) {
|
||||
@@ -11,6 +12,10 @@ function toMinPath(path) {
|
||||
const basePlugins = [
|
||||
resolve({ browser: true }),
|
||||
commonjs(),
|
||||
|
||||
// supports bundling `web-worker:..filename` from rrweb
|
||||
webWorkerLoader(),
|
||||
|
||||
typescript({
|
||||
tsconfigOverride: { compilerOptions: { module: 'ESNext' } },
|
||||
}),
|
||||
@@ -27,6 +32,11 @@ const baseConfigs = [
|
||||
name: 'RRDocument',
|
||||
path: 'document-nodejs',
|
||||
},
|
||||
{
|
||||
input: './src/virtual-dom.ts',
|
||||
name: 'RRDocument',
|
||||
path: 'virtual-dom',
|
||||
},
|
||||
];
|
||||
|
||||
let configs = [];
|
||||
|
||||
513
packages/rrdom/src/diff.ts
Normal file
513
packages/rrdom/src/diff.ts
Normal file
@@ -0,0 +1,513 @@
|
||||
import { NodeType as RRNodeType, Mirror as NodeMirror } from 'rrweb-snapshot';
|
||||
import type {
|
||||
canvasMutationData,
|
||||
canvasEventWithTime,
|
||||
inputData,
|
||||
scrollData,
|
||||
} from 'rrweb/src/types';
|
||||
import type {
|
||||
IRRCDATASection,
|
||||
IRRComment,
|
||||
IRRDocument,
|
||||
IRRDocumentType,
|
||||
IRRElement,
|
||||
IRRNode,
|
||||
IRRText,
|
||||
} from './document';
|
||||
import type {
|
||||
RRCanvasElement,
|
||||
RRElement,
|
||||
RRIFrameElement,
|
||||
RRMediaElement,
|
||||
RRStyleElement,
|
||||
RRDocument,
|
||||
Mirror,
|
||||
} from './virtual-dom';
|
||||
|
||||
const NAMESPACES: Record<string, string> = {
|
||||
svg: 'http://www.w3.org/2000/svg',
|
||||
'xlink:href': 'http://www.w3.org/1999/xlink',
|
||||
xmlns: 'http://www.w3.org/2000/xmlns/',
|
||||
};
|
||||
|
||||
// camel case svg element tag names
|
||||
const SVGTagMap: Record<string, string> = {
|
||||
altglyph: 'altGlyph',
|
||||
altglyphdef: 'altGlyphDef',
|
||||
altglyphitem: 'altGlyphItem',
|
||||
animatecolor: 'animateColor',
|
||||
animatemotion: 'animateMotion',
|
||||
animatetransform: 'animateTransform',
|
||||
clippath: 'clipPath',
|
||||
feblend: 'feBlend',
|
||||
fecolormatrix: 'feColorMatrix',
|
||||
fecomponenttransfer: 'feComponentTransfer',
|
||||
fecomposite: 'feComposite',
|
||||
feconvolvematrix: 'feConvolveMatrix',
|
||||
fediffuselighting: 'feDiffuseLighting',
|
||||
fedisplacementmap: 'feDisplacementMap',
|
||||
fedistantlight: 'feDistantLight',
|
||||
fedropshadow: 'feDropShadow',
|
||||
feflood: 'feFlood',
|
||||
fefunca: 'feFuncA',
|
||||
fefuncb: 'feFuncB',
|
||||
fefuncg: 'feFuncG',
|
||||
fefuncr: 'feFuncR',
|
||||
fegaussianblur: 'feGaussianBlur',
|
||||
feimage: 'feImage',
|
||||
femerge: 'feMerge',
|
||||
femergenode: 'feMergeNode',
|
||||
femorphology: 'feMorphology',
|
||||
feoffset: 'feOffset',
|
||||
fepointlight: 'fePointLight',
|
||||
fespecularlighting: 'feSpecularLighting',
|
||||
fespotlight: 'feSpotLight',
|
||||
fetile: 'feTile',
|
||||
feturbulence: 'feTurbulence',
|
||||
foreignobject: 'foreignObject',
|
||||
glyphref: 'glyphRef',
|
||||
lineargradient: 'linearGradient',
|
||||
radialgradient: 'radialGradient',
|
||||
};
|
||||
|
||||
export type ReplayerHandler = {
|
||||
mirror: NodeMirror;
|
||||
applyCanvas: (
|
||||
canvasEvent: canvasEventWithTime,
|
||||
canvasMutationData: canvasMutationData,
|
||||
target: HTMLCanvasElement,
|
||||
) => void;
|
||||
applyInput: (data: inputData) => void;
|
||||
applyScroll: (data: scrollData, isSync: boolean) => void;
|
||||
};
|
||||
|
||||
export function diff(
|
||||
oldTree: Node,
|
||||
newTree: IRRNode,
|
||||
replayer: ReplayerHandler,
|
||||
rrnodeMirror?: Mirror,
|
||||
) {
|
||||
const oldChildren = oldTree.childNodes;
|
||||
const newChildren = newTree.childNodes;
|
||||
rrnodeMirror =
|
||||
rrnodeMirror ||
|
||||
(newTree as RRDocument).mirror ||
|
||||
(newTree.ownerDocument as RRDocument).mirror;
|
||||
|
||||
if (oldChildren.length > 0 || newChildren.length > 0) {
|
||||
diffChildren(
|
||||
Array.from(oldChildren),
|
||||
newChildren,
|
||||
oldTree,
|
||||
replayer,
|
||||
rrnodeMirror,
|
||||
);
|
||||
}
|
||||
|
||||
let inputDataToApply = null,
|
||||
scrollDataToApply = null;
|
||||
switch (newTree.RRNodeType) {
|
||||
case RRNodeType.Document:
|
||||
const newRRDocument = newTree as IRRDocument;
|
||||
scrollDataToApply = (newRRDocument as RRDocument).scrollData;
|
||||
break;
|
||||
case RRNodeType.Element:
|
||||
const oldElement = (oldTree as Node) as HTMLElement;
|
||||
const newRRElement = newTree as IRRElement;
|
||||
diffProps(oldElement, newRRElement, rrnodeMirror);
|
||||
scrollDataToApply = (newRRElement as RRElement).scrollData;
|
||||
inputDataToApply = (newRRElement as RRElement).inputData;
|
||||
switch (newRRElement.tagName) {
|
||||
case 'AUDIO':
|
||||
case 'VIDEO':
|
||||
const oldMediaElement = (oldTree as Node) as HTMLMediaElement;
|
||||
const newMediaRRElement = newRRElement as RRMediaElement;
|
||||
if (newMediaRRElement.paused !== undefined)
|
||||
newMediaRRElement.paused
|
||||
? oldMediaElement.pause()
|
||||
: oldMediaElement.play();
|
||||
if (newMediaRRElement.muted !== undefined)
|
||||
oldMediaElement.muted = newMediaRRElement.muted;
|
||||
if (newMediaRRElement.volume !== undefined)
|
||||
oldMediaElement.volume = newMediaRRElement.volume;
|
||||
if (newMediaRRElement.currentTime !== undefined)
|
||||
oldMediaElement.currentTime = newMediaRRElement.currentTime;
|
||||
break;
|
||||
case 'CANVAS':
|
||||
(newTree as RRCanvasElement).canvasMutations.forEach(
|
||||
(canvasMutation) =>
|
||||
replayer.applyCanvas(
|
||||
canvasMutation.event,
|
||||
canvasMutation.mutation,
|
||||
(oldTree as Node) as HTMLCanvasElement,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'STYLE':
|
||||
applyVirtualStyleRulesToNode(
|
||||
oldElement as HTMLStyleElement,
|
||||
(newTree as RRStyleElement).rules,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (newRRElement.shadowRoot) {
|
||||
if (!oldElement.shadowRoot) oldElement.attachShadow({ mode: 'open' });
|
||||
const oldChildren = oldElement.shadowRoot!.childNodes;
|
||||
const newChildren = newRRElement.shadowRoot.childNodes;
|
||||
if (oldChildren.length > 0 || newChildren.length > 0)
|
||||
diffChildren(
|
||||
Array.from(oldChildren),
|
||||
newChildren,
|
||||
oldElement.shadowRoot!,
|
||||
replayer,
|
||||
rrnodeMirror,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case RRNodeType.Text:
|
||||
case RRNodeType.Comment:
|
||||
case RRNodeType.CDATA:
|
||||
if (
|
||||
oldTree.textContent !==
|
||||
(newTree as IRRText | IRRComment | IRRCDATASection).data
|
||||
)
|
||||
oldTree.textContent = (newTree as
|
||||
| IRRText
|
||||
| IRRComment
|
||||
| IRRCDATASection).data;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
scrollDataToApply && replayer.applyScroll(scrollDataToApply, true);
|
||||
/**
|
||||
* Input data need to get applied after all children of this node are updated.
|
||||
* Otherwise when we set a value for a select element whose options are empty, the value won't actually update.
|
||||
*/
|
||||
inputDataToApply && replayer.applyInput(inputDataToApply);
|
||||
|
||||
// IFrame element doesn't have child nodes.
|
||||
if (newTree.nodeName === 'IFRAME') {
|
||||
const oldContentDocument = ((oldTree as Node) as HTMLIFrameElement)
|
||||
.contentDocument;
|
||||
const newIFrameElement = newTree as RRIFrameElement;
|
||||
// If the iframe is cross-origin, the contentDocument will be null.
|
||||
if (oldContentDocument) {
|
||||
const sn = rrnodeMirror.getMeta(newIFrameElement.contentDocument);
|
||||
if (sn) {
|
||||
replayer.mirror.add(oldContentDocument, { ...sn });
|
||||
}
|
||||
diff(
|
||||
oldContentDocument,
|
||||
newIFrameElement.contentDocument,
|
||||
replayer,
|
||||
rrnodeMirror,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function diffProps(
|
||||
oldTree: HTMLElement,
|
||||
newTree: IRRElement,
|
||||
rrnodeMirror: Mirror,
|
||||
) {
|
||||
const oldAttributes = oldTree.attributes;
|
||||
const newAttributes = newTree.attributes;
|
||||
|
||||
for (const name in newAttributes) {
|
||||
const newValue = newAttributes[name];
|
||||
const sn = rrnodeMirror.getMeta(newTree);
|
||||
if (sn && 'isSVG' in sn && sn.isSVG && NAMESPACES[name])
|
||||
oldTree.setAttributeNS(NAMESPACES[name], name, newValue);
|
||||
else if (newTree.tagName === 'CANVAS' && name === 'rr_dataURL') {
|
||||
const image = document.createElement('img');
|
||||
image.src = newValue;
|
||||
image.onload = () => {
|
||||
const ctx = (oldTree as HTMLCanvasElement).getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||
}
|
||||
};
|
||||
} else oldTree.setAttribute(name, newValue);
|
||||
}
|
||||
|
||||
for (const { name } of Array.from(oldAttributes))
|
||||
if (!(name in newAttributes)) oldTree.removeAttribute(name);
|
||||
|
||||
newTree.scrollLeft && (oldTree.scrollLeft = newTree.scrollLeft);
|
||||
newTree.scrollTop && (oldTree.scrollTop = newTree.scrollTop);
|
||||
}
|
||||
|
||||
function diffChildren(
|
||||
oldChildren: (Node | undefined)[],
|
||||
newChildren: IRRNode[],
|
||||
parentNode: Node,
|
||||
replayer: ReplayerHandler,
|
||||
rrnodeMirror: Mirror,
|
||||
) {
|
||||
let oldStartIndex = 0,
|
||||
oldEndIndex = oldChildren.length - 1,
|
||||
newStartIndex = 0,
|
||||
newEndIndex = newChildren.length - 1;
|
||||
let oldStartNode = oldChildren[oldStartIndex],
|
||||
oldEndNode = oldChildren[oldEndIndex],
|
||||
newStartNode = newChildren[newStartIndex],
|
||||
newEndNode = newChildren[newEndIndex];
|
||||
let oldIdToIndex: Record<number, number> | undefined = undefined,
|
||||
indexInOld;
|
||||
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
|
||||
if (oldStartNode === undefined) {
|
||||
oldStartNode = oldChildren[++oldStartIndex];
|
||||
} else if (oldEndNode === undefined) {
|
||||
oldEndNode = oldChildren[--oldEndIndex];
|
||||
} else if (
|
||||
replayer.mirror.getId(oldStartNode) === rrnodeMirror.getId(newStartNode)
|
||||
) {
|
||||
diff(oldStartNode, newStartNode, replayer, rrnodeMirror);
|
||||
oldStartNode = oldChildren[++oldStartIndex];
|
||||
newStartNode = newChildren[++newStartIndex];
|
||||
} else if (
|
||||
replayer.mirror.getId(oldEndNode) === rrnodeMirror.getId(newEndNode)
|
||||
) {
|
||||
diff(oldEndNode, newEndNode, replayer, rrnodeMirror);
|
||||
oldEndNode = oldChildren[--oldEndIndex];
|
||||
newEndNode = newChildren[--newEndIndex];
|
||||
} else if (
|
||||
replayer.mirror.getId(oldStartNode) === rrnodeMirror.getId(newEndNode)
|
||||
) {
|
||||
parentNode.insertBefore(oldStartNode, oldEndNode.nextSibling);
|
||||
diff(oldStartNode, newEndNode, replayer, rrnodeMirror);
|
||||
oldStartNode = oldChildren[++oldStartIndex];
|
||||
newEndNode = newChildren[--newEndIndex];
|
||||
} else if (
|
||||
replayer.mirror.getId(oldEndNode) === rrnodeMirror.getId(newStartNode)
|
||||
) {
|
||||
parentNode.insertBefore(oldEndNode, oldStartNode);
|
||||
diff(oldEndNode, newStartNode, replayer, rrnodeMirror);
|
||||
oldEndNode = oldChildren[--oldEndIndex];
|
||||
newStartNode = newChildren[++newStartIndex];
|
||||
} else {
|
||||
if (!oldIdToIndex) {
|
||||
oldIdToIndex = {};
|
||||
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
|
||||
const oldChild = oldChildren[i];
|
||||
if (oldChild && replayer.mirror.hasNode(oldChild))
|
||||
oldIdToIndex[replayer.mirror.getId(oldChild)] = i;
|
||||
}
|
||||
}
|
||||
indexInOld = oldIdToIndex[rrnodeMirror.getId(newStartNode)];
|
||||
if (indexInOld) {
|
||||
const nodeToMove = oldChildren[indexInOld]!;
|
||||
parentNode.insertBefore(nodeToMove, oldStartNode);
|
||||
diff(nodeToMove, newStartNode, replayer, rrnodeMirror);
|
||||
oldChildren[indexInOld] = undefined;
|
||||
} else {
|
||||
const newNode = createOrGetNode(
|
||||
newStartNode,
|
||||
replayer.mirror,
|
||||
rrnodeMirror,
|
||||
);
|
||||
|
||||
/**
|
||||
* A mounted iframe element has an automatically created HTML element.
|
||||
* We should delete it before insert a serialized one. Otherwise, an error 'Only one element on document allowed' will be thrown.
|
||||
*/
|
||||
if (
|
||||
replayer.mirror.getMeta(parentNode)?.type === RRNodeType.Document &&
|
||||
replayer.mirror.getMeta(newNode)?.type === RRNodeType.Element &&
|
||||
((parentNode as Node) as Document).documentElement
|
||||
) {
|
||||
parentNode.removeChild(
|
||||
((parentNode as Node) as Document).documentElement,
|
||||
);
|
||||
oldChildren[oldStartIndex] = undefined;
|
||||
oldStartNode = undefined;
|
||||
}
|
||||
parentNode.insertBefore(newNode, oldStartNode || null);
|
||||
diff(newNode, newStartNode, replayer, rrnodeMirror);
|
||||
}
|
||||
newStartNode = newChildren[++newStartIndex];
|
||||
}
|
||||
}
|
||||
if (oldStartIndex > oldEndIndex) {
|
||||
const referenceRRNode = newChildren[newEndIndex + 1];
|
||||
let referenceNode = null;
|
||||
if (referenceRRNode)
|
||||
parentNode.childNodes.forEach((child) => {
|
||||
if (
|
||||
replayer.mirror.getId(child) === rrnodeMirror.getId(referenceRRNode)
|
||||
)
|
||||
referenceNode = child;
|
||||
});
|
||||
for (; newStartIndex <= newEndIndex; ++newStartIndex) {
|
||||
const newNode = createOrGetNode(
|
||||
newChildren[newStartIndex],
|
||||
replayer.mirror,
|
||||
rrnodeMirror,
|
||||
);
|
||||
parentNode.insertBefore(newNode, referenceNode);
|
||||
diff(newNode, newChildren[newStartIndex], replayer, rrnodeMirror);
|
||||
}
|
||||
} else if (newStartIndex > newEndIndex) {
|
||||
for (; oldStartIndex <= oldEndIndex; oldStartIndex++) {
|
||||
const node = oldChildren[oldStartIndex];
|
||||
if (node) {
|
||||
parentNode.removeChild(node);
|
||||
replayer.mirror.removeNodeFromMap(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createOrGetNode(
|
||||
rrNode: IRRNode,
|
||||
domMirror: NodeMirror,
|
||||
rrnodeMirror: Mirror,
|
||||
): Node {
|
||||
let node = domMirror.getNode(rrnodeMirror.getId(rrNode));
|
||||
const sn = rrnodeMirror.getMeta(rrNode);
|
||||
if (node !== null) return node;
|
||||
switch (rrNode.RRNodeType) {
|
||||
case RRNodeType.Document:
|
||||
node = new Document();
|
||||
break;
|
||||
case RRNodeType.DocumentType:
|
||||
node = document.implementation.createDocumentType(
|
||||
(rrNode as IRRDocumentType).name,
|
||||
(rrNode as IRRDocumentType).publicId,
|
||||
(rrNode as IRRDocumentType).systemId,
|
||||
);
|
||||
break;
|
||||
case RRNodeType.Element:
|
||||
let tagName = (rrNode as IRRElement).tagName.toLowerCase();
|
||||
tagName = SVGTagMap[tagName] || tagName;
|
||||
if (sn && 'isSVG' in sn && sn?.isSVG) {
|
||||
node = document.createElementNS(
|
||||
NAMESPACES['svg'],
|
||||
(rrNode as IRRElement).tagName.toLowerCase(),
|
||||
);
|
||||
} else node = document.createElement((rrNode as IRRElement).tagName);
|
||||
break;
|
||||
case RRNodeType.Text:
|
||||
node = document.createTextNode((rrNode as IRRText).data);
|
||||
break;
|
||||
case RRNodeType.Comment:
|
||||
node = document.createComment((rrNode as IRRComment).data);
|
||||
break;
|
||||
case RRNodeType.CDATA:
|
||||
node = document.createCDATASection((rrNode as IRRCDATASection).data);
|
||||
break;
|
||||
}
|
||||
|
||||
if (sn) domMirror.add(node, { ...sn });
|
||||
return node;
|
||||
}
|
||||
|
||||
export function getNestedRule(
|
||||
rules: CSSRuleList,
|
||||
position: number[],
|
||||
): CSSGroupingRule {
|
||||
const rule = rules[position[0]] as CSSGroupingRule;
|
||||
if (position.length === 1) {
|
||||
return rule;
|
||||
} else {
|
||||
return getNestedRule(
|
||||
((rule as CSSGroupingRule).cssRules[position[1]] as CSSGroupingRule)
|
||||
.cssRules,
|
||||
position.slice(2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export enum StyleRuleType {
|
||||
Insert,
|
||||
Remove,
|
||||
Snapshot,
|
||||
SetProperty,
|
||||
RemoveProperty,
|
||||
}
|
||||
type InsertRule = {
|
||||
cssText: string;
|
||||
type: StyleRuleType.Insert;
|
||||
index?: number | number[];
|
||||
};
|
||||
type RemoveRule = {
|
||||
type: StyleRuleType.Remove;
|
||||
index: number | number[];
|
||||
};
|
||||
type SetPropertyRule = {
|
||||
type: StyleRuleType.SetProperty;
|
||||
index: number[];
|
||||
property: string;
|
||||
value: string | null;
|
||||
priority: string | undefined;
|
||||
};
|
||||
type RemovePropertyRule = {
|
||||
type: StyleRuleType.RemoveProperty;
|
||||
index: number[];
|
||||
property: string;
|
||||
};
|
||||
|
||||
export type VirtualStyleRules = Array<
|
||||
InsertRule | RemoveRule | SetPropertyRule | RemovePropertyRule
|
||||
>;
|
||||
|
||||
export function getPositionsAndIndex(nestedIndex: number[]) {
|
||||
const positions = [...nestedIndex];
|
||||
const index = positions.pop();
|
||||
return { positions, index };
|
||||
}
|
||||
|
||||
export function applyVirtualStyleRulesToNode(
|
||||
styleNode: HTMLStyleElement,
|
||||
virtualStyleRules: VirtualStyleRules,
|
||||
) {
|
||||
const sheet = styleNode.sheet!;
|
||||
|
||||
virtualStyleRules.forEach((rule) => {
|
||||
if (rule.type === StyleRuleType.Insert) {
|
||||
try {
|
||||
if (Array.isArray(rule.index)) {
|
||||
const { positions, index } = getPositionsAndIndex(rule.index);
|
||||
const nestedRule = getNestedRule(sheet.cssRules, positions);
|
||||
nestedRule.insertRule(rule.cssText, index);
|
||||
} else {
|
||||
sheet.insertRule(rule.cssText, rule.index);
|
||||
}
|
||||
} catch (e) {
|
||||
/**
|
||||
* sometimes we may capture rules with browser prefix
|
||||
* insert rule with prefixs in other browsers may cause Error
|
||||
*/
|
||||
}
|
||||
} else if (rule.type === StyleRuleType.Remove) {
|
||||
try {
|
||||
if (Array.isArray(rule.index)) {
|
||||
const { positions, index } = getPositionsAndIndex(rule.index);
|
||||
const nestedRule = getNestedRule(sheet.cssRules, positions);
|
||||
nestedRule.deleteRule(index || 0);
|
||||
} else {
|
||||
sheet.deleteRule(rule.index);
|
||||
}
|
||||
} catch (e) {
|
||||
/**
|
||||
* accessing styleSheet rules may cause SecurityError
|
||||
* for specific access control settings
|
||||
*/
|
||||
}
|
||||
} else if (rule.type === StyleRuleType.SetProperty) {
|
||||
const nativeRule = (getNestedRule(
|
||||
sheet.cssRules,
|
||||
rule.index,
|
||||
) as unknown) as CSSStyleRule;
|
||||
nativeRule.style.setProperty(rule.property, rule.value, rule.priority);
|
||||
} else if (rule.type === StyleRuleType.RemoveProperty) {
|
||||
const nativeRule = (getNestedRule(
|
||||
sheet.cssRules,
|
||||
rule.index,
|
||||
) as unknown) as CSSStyleRule;
|
||||
nativeRule.style.removeProperty(rule.property);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,68 +1,24 @@
|
||||
import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot';
|
||||
import { NWSAPI } from 'nwsapi';
|
||||
import { parseCSSText, camelize, toCSSText } from './style';
|
||||
import { NodeType as RRNodeType } from 'rrweb-snapshot';
|
||||
import type { NWSAPI } from 'nwsapi';
|
||||
import type { CSSStyleDeclaration as CSSStyleDeclarationType } from 'cssstyle';
|
||||
import {
|
||||
BaseRRCDATASectionImpl,
|
||||
BaseRRCommentImpl,
|
||||
BaseRRDocumentImpl,
|
||||
BaseRRDocumentTypeImpl,
|
||||
BaseRRElementImpl,
|
||||
BaseRRMediaElementImpl,
|
||||
BaseRRNode,
|
||||
BaseRRTextImpl,
|
||||
ClassList,
|
||||
IRRDocument,
|
||||
CSSStyleDeclaration,
|
||||
} from './document';
|
||||
const nwsapi = require('nwsapi');
|
||||
const cssom = require('cssom');
|
||||
const cssstyle = require('cssstyle');
|
||||
|
||||
export abstract class RRNode {
|
||||
__sn: serializedNodeWithId | undefined;
|
||||
children: Array<RRNode> = [];
|
||||
parentElement: RRElement | null = null;
|
||||
parentNode: RRNode | null = null;
|
||||
ownerDocument: RRDocument | null = null;
|
||||
ELEMENT_NODE = 1;
|
||||
TEXT_NODE = 3;
|
||||
|
||||
get firstChild() {
|
||||
return this.children[0];
|
||||
}
|
||||
|
||||
get nodeType() {
|
||||
if (this instanceof RRDocument) return NodeType.Document;
|
||||
if (this instanceof RRDocumentType) return NodeType.DocumentType;
|
||||
if (this instanceof RRElement) return NodeType.Element;
|
||||
if (this instanceof RRText) return NodeType.Text;
|
||||
if (this instanceof RRCDATASection) return NodeType.CDATA;
|
||||
if (this instanceof RRComment) return NodeType.Comment;
|
||||
}
|
||||
|
||||
get childNodes() {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
appendChild(newChild: RRNode): RRNode {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method.`,
|
||||
);
|
||||
}
|
||||
|
||||
insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method.`,
|
||||
);
|
||||
}
|
||||
|
||||
contains(node: RRNode) {
|
||||
if (node === this) return true;
|
||||
for (const child of this.children) {
|
||||
if (child.contains(node)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeChild(node: RRNode) {
|
||||
const indexOfChild = this.children.indexOf(node);
|
||||
if (indexOfChild !== -1) {
|
||||
this.children.splice(indexOfChild, 1);
|
||||
node.parentElement = null;
|
||||
node.parentNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
toString(nodeName?: string) {
|
||||
return `${JSON.stringify(this.__sn?.id) || ''} ${nodeName}`;
|
||||
}
|
||||
}
|
||||
export class RRNode extends BaseRRNode {}
|
||||
|
||||
export class RRWindow {
|
||||
scrollLeft = 0;
|
||||
@@ -74,8 +30,10 @@ export class RRWindow {
|
||||
}
|
||||
}
|
||||
|
||||
export class RRDocument extends RRNode {
|
||||
private mirror: Map<number, RRNode> = new Map();
|
||||
export class RRDocument
|
||||
extends BaseRRDocumentImpl(RRNode)
|
||||
implements IRRDocument {
|
||||
readonly nodeName: '#document' = '#document';
|
||||
private _nwsapi: NWSAPI;
|
||||
get nwsapi() {
|
||||
if (!this._nwsapi) {
|
||||
@@ -95,66 +53,32 @@ export class RRDocument extends RRNode {
|
||||
return this._nwsapi;
|
||||
}
|
||||
|
||||
get documentElement(): RRElement {
|
||||
return this.children.find(
|
||||
(node) => node instanceof RRElement && node.tagName === 'HTML',
|
||||
) as RRElement;
|
||||
get documentElement(): RRElement | null {
|
||||
return super.documentElement as RRElement | null;
|
||||
}
|
||||
|
||||
get body() {
|
||||
return (
|
||||
this.documentElement?.children.find(
|
||||
(node) => node instanceof RRElement && node.tagName === 'BODY',
|
||||
) || null
|
||||
);
|
||||
get body(): RRElement | null {
|
||||
return super.body as RRElement | null;
|
||||
}
|
||||
|
||||
get head() {
|
||||
return (
|
||||
this.documentElement?.children.find(
|
||||
(node) => node instanceof RRElement && node.tagName === 'HEAD',
|
||||
) || null
|
||||
);
|
||||
return super.head as RRElement | null;
|
||||
}
|
||||
|
||||
get implementation() {
|
||||
get implementation(): RRDocument {
|
||||
return this;
|
||||
}
|
||||
|
||||
get firstElementChild() {
|
||||
return this.documentElement;
|
||||
get firstElementChild(): RRElement | null {
|
||||
return this.documentElement as RRElement | null;
|
||||
}
|
||||
|
||||
appendChild(childNode: RRNode) {
|
||||
const nodeType = childNode.nodeType;
|
||||
if (nodeType === NodeType.Element || nodeType === NodeType.DocumentType) {
|
||||
if (this.children.some((s) => s.nodeType === nodeType)) {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one ${
|
||||
nodeType === NodeType.Element ? 'RRElement' : 'RRDoctype'
|
||||
} on RRDocument allowed.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
childNode.parentElement = null;
|
||||
childNode.parentNode = this;
|
||||
childNode.ownerDocument = this;
|
||||
this.children.push(childNode);
|
||||
return childNode;
|
||||
return super.appendChild(childNode);
|
||||
}
|
||||
|
||||
insertBefore(newChild: RRNode, refChild: RRNode | null) {
|
||||
if (refChild === null) return this.appendChild(newChild);
|
||||
const childIndex = this.children.indexOf(refChild);
|
||||
if (childIndex == -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.",
|
||||
);
|
||||
this.children.splice(childIndex, 0, newChild);
|
||||
newChild.parentElement = null;
|
||||
newChild.parentNode = this;
|
||||
newChild.ownerDocument = this;
|
||||
return newChild;
|
||||
return super.insertBefore(newChild, refChild);
|
||||
}
|
||||
|
||||
querySelectorAll(selectors: string): RRNode[] {
|
||||
@@ -216,16 +140,16 @@ export class RRDocument extends RRNode {
|
||||
element = new RRMediaElement(upperTagName);
|
||||
break;
|
||||
case 'IFRAME':
|
||||
element = new RRIframeElement(upperTagName);
|
||||
element = new RRIFrameElement(upperTagName);
|
||||
break;
|
||||
case 'IMG':
|
||||
element = new RRImageElement('IMG');
|
||||
element = new RRImageElement(upperTagName);
|
||||
break;
|
||||
case 'CANVAS':
|
||||
element = new RRCanvasElement('CANVAS');
|
||||
element = new RRCanvasElement(upperTagName);
|
||||
break;
|
||||
case 'STYLE':
|
||||
element = new RRStyleElement('STYLE');
|
||||
element = new RRStyleElement(upperTagName);
|
||||
break;
|
||||
default:
|
||||
element = new RRElement(upperTagName);
|
||||
@@ -235,10 +159,7 @@ export class RRDocument extends RRNode {
|
||||
return element;
|
||||
}
|
||||
|
||||
createElementNS(
|
||||
_namespaceURI: 'http://www.w3.org/2000/svg',
|
||||
qualifiedName: string,
|
||||
) {
|
||||
createElementNS(_namespaceURI: string, qualifiedName: string) {
|
||||
return this.createElement(qualifiedName as keyof HTMLElementTagNameMap);
|
||||
}
|
||||
|
||||
@@ -259,266 +180,40 @@ export class RRDocument extends RRNode {
|
||||
textNode.ownerDocument = this;
|
||||
return textNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* This does come with some side effects. For example:
|
||||
* 1. All event listeners currently registered on the document, nodes inside the document, or the document's window are removed.
|
||||
* 2. All existing nodes are removed from the document.
|
||||
*/
|
||||
open() {
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
close() {}
|
||||
|
||||
buildFromDom(dom: Document) {
|
||||
let notSerializedId = -1;
|
||||
const NodeTypeMap: Record<number, number> = {};
|
||||
NodeTypeMap[document.DOCUMENT_NODE] = NodeType.Document;
|
||||
NodeTypeMap[document.DOCUMENT_TYPE_NODE] = NodeType.DocumentType;
|
||||
NodeTypeMap[document.ELEMENT_NODE] = NodeType.Element;
|
||||
NodeTypeMap[document.TEXT_NODE] = NodeType.Text;
|
||||
NodeTypeMap[document.CDATA_SECTION_NODE] = NodeType.CDATA;
|
||||
NodeTypeMap[document.COMMENT_NODE] = NodeType.Comment;
|
||||
|
||||
function getValidTagName(element: HTMLElement): string {
|
||||
if (element instanceof HTMLFormElement) {
|
||||
return 'FORM';
|
||||
}
|
||||
return element.tagName.toUpperCase().trim();
|
||||
}
|
||||
|
||||
const walk = function (node: INode) {
|
||||
let serializedNodeWithId = node.__sn;
|
||||
let rrNode: RRNode;
|
||||
if (!serializedNodeWithId) {
|
||||
serializedNodeWithId = {
|
||||
type: NodeTypeMap[node.nodeType],
|
||||
textContent: '',
|
||||
id: notSerializedId,
|
||||
};
|
||||
notSerializedId -= 1;
|
||||
node.__sn = serializedNodeWithId;
|
||||
}
|
||||
if (!this.mirror.has(serializedNodeWithId.id)) {
|
||||
switch (node.nodeType) {
|
||||
case node.DOCUMENT_NODE:
|
||||
if (
|
||||
serializedNodeWithId.rootId &&
|
||||
serializedNodeWithId.rootId !== serializedNodeWithId.id
|
||||
)
|
||||
rrNode = this.createDocument();
|
||||
else rrNode = this;
|
||||
break;
|
||||
case node.DOCUMENT_TYPE_NODE:
|
||||
const documentType = (node as unknown) as DocumentType;
|
||||
rrNode = this.createDocumentType(
|
||||
documentType.name,
|
||||
documentType.publicId,
|
||||
documentType.systemId,
|
||||
);
|
||||
break;
|
||||
case node.ELEMENT_NODE:
|
||||
const elementNode = (node as unknown) as HTMLElement;
|
||||
const tagName = getValidTagName(elementNode);
|
||||
rrNode = this.createElement(tagName);
|
||||
const rrElement = rrNode as RRElement;
|
||||
for (const { name, value } of Array.from(elementNode.attributes)) {
|
||||
rrElement.attributes[name] = value;
|
||||
}
|
||||
// form fields
|
||||
if (
|
||||
tagName === 'INPUT' ||
|
||||
tagName === 'TEXTAREA' ||
|
||||
tagName === 'SELECT'
|
||||
) {
|
||||
const value = (elementNode as
|
||||
| HTMLInputElement
|
||||
| HTMLTextAreaElement).value;
|
||||
if (
|
||||
['RADIO', 'CHECKBOX', 'SUBMIT', 'BUTTON'].includes(
|
||||
rrElement.attributes.type as string,
|
||||
) &&
|
||||
value
|
||||
) {
|
||||
rrElement.attributes.value = value;
|
||||
} else if ((elementNode as HTMLInputElement).checked) {
|
||||
rrElement.attributes.checked = (elementNode as HTMLInputElement).checked;
|
||||
}
|
||||
}
|
||||
if (tagName === 'OPTION') {
|
||||
const selectValue = (elementNode as HTMLOptionElement)
|
||||
.parentElement;
|
||||
if (
|
||||
rrElement.attributes.value ===
|
||||
(selectValue as HTMLSelectElement).value
|
||||
) {
|
||||
rrElement.attributes.selected = (elementNode as HTMLOptionElement).selected;
|
||||
}
|
||||
}
|
||||
// canvas image data
|
||||
if (tagName === 'CANVAS') {
|
||||
rrElement.attributes.rr_dataURL = (elementNode as HTMLCanvasElement).toDataURL();
|
||||
}
|
||||
// media elements
|
||||
if (tagName === 'AUDIO' || tagName === 'VIDEO') {
|
||||
const rrMediaElement = rrElement as RRMediaElement;
|
||||
rrMediaElement.paused = (elementNode as HTMLMediaElement).paused;
|
||||
rrMediaElement.currentTime = (elementNode as HTMLMediaElement).currentTime;
|
||||
}
|
||||
// scroll
|
||||
if (elementNode.scrollLeft) {
|
||||
rrElement.scrollLeft = elementNode.scrollLeft;
|
||||
}
|
||||
if (elementNode.scrollTop) {
|
||||
rrElement.scrollTop = elementNode.scrollTop;
|
||||
}
|
||||
break;
|
||||
case node.TEXT_NODE:
|
||||
rrNode = this.createTextNode(
|
||||
((node as unknown) as Text).textContent,
|
||||
);
|
||||
break;
|
||||
case node.CDATA_SECTION_NODE:
|
||||
rrNode = this.createCDATASection();
|
||||
break;
|
||||
case node.COMMENT_NODE:
|
||||
rrNode = this.createComment(
|
||||
((node as unknown) as Comment).textContent || '',
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
rrNode.__sn = serializedNodeWithId;
|
||||
this.mirror.set(serializedNodeWithId.id, rrNode);
|
||||
} else {
|
||||
rrNode = this.mirror.get(serializedNodeWithId.id);
|
||||
rrNode.parentElement = null;
|
||||
rrNode.parentNode = null;
|
||||
rrNode.children = [];
|
||||
}
|
||||
const parentNode = node.parentElement || node.parentNode;
|
||||
if (parentNode) {
|
||||
const parentSN = ((parentNode as unknown) as INode).__sn;
|
||||
const parentRRNode = this.mirror.get(parentSN.id);
|
||||
parentRRNode.appendChild(rrNode);
|
||||
rrNode.parentNode = parentRRNode;
|
||||
rrNode.parentElement =
|
||||
parentRRNode instanceof RRElement ? parentRRNode : null;
|
||||
}
|
||||
|
||||
if (
|
||||
serializedNodeWithId.type === NodeType.Document ||
|
||||
serializedNodeWithId.type === NodeType.Element
|
||||
) {
|
||||
node.childNodes.forEach((node) => walk((node as unknown) as INode));
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
if (dom) {
|
||||
this.destroyTree();
|
||||
walk((dom as unknown) as INode);
|
||||
}
|
||||
}
|
||||
|
||||
destroyTree() {
|
||||
this.children = [];
|
||||
this.mirror.clear();
|
||||
}
|
||||
|
||||
toString() {
|
||||
return super.toString('RRDocument');
|
||||
}
|
||||
}
|
||||
|
||||
export class RRDocumentType extends RRNode {
|
||||
readonly name: string;
|
||||
readonly publicId: string;
|
||||
readonly systemId: string;
|
||||
|
||||
constructor(qualifiedName: string, publicId: string, systemId: string) {
|
||||
super();
|
||||
this.name = qualifiedName;
|
||||
this.publicId = publicId;
|
||||
this.systemId = systemId;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return super.toString('RRDocumentType');
|
||||
}
|
||||
}
|
||||
|
||||
export class RRElement extends RRNode {
|
||||
tagName: string;
|
||||
attributes: Record<string, string | number | boolean> = {};
|
||||
scrollLeft: number = 0;
|
||||
scrollTop: number = 0;
|
||||
shadowRoot: RRElement | null = null;
|
||||
export class RRDocumentType extends BaseRRDocumentTypeImpl(RRNode) {}
|
||||
|
||||
export class RRElement extends BaseRRElementImpl(RRNode) {
|
||||
private _style: CSSStyleDeclarationType;
|
||||
constructor(tagName: string) {
|
||||
super();
|
||||
this.tagName = tagName;
|
||||
}
|
||||
|
||||
get classList() {
|
||||
return new ClassList(
|
||||
this.attributes.class as string | undefined,
|
||||
(newClassName) => {
|
||||
this.attributes.class = newClassName;
|
||||
super(tagName);
|
||||
this._style = new cssstyle.CSSStyleDeclaration();
|
||||
const style = this._style;
|
||||
Object.defineProperty(this.attributes, 'style', {
|
||||
get() {
|
||||
return style.cssText;
|
||||
},
|
||||
);
|
||||
set(cssText: string) {
|
||||
style.cssText = cssText;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.attributes.id;
|
||||
}
|
||||
|
||||
get className() {
|
||||
return this.attributes.class || '';
|
||||
}
|
||||
|
||||
get textContent() {
|
||||
return '';
|
||||
}
|
||||
|
||||
set textContent(newText: string) {}
|
||||
|
||||
get style() {
|
||||
const style = (this.attributes.style
|
||||
? parseCSSText(this.attributes.style as string)
|
||||
: {}) as Record<string, string> & {
|
||||
setProperty: (
|
||||
name: string,
|
||||
value: string | null,
|
||||
priority?: string | null,
|
||||
) => void;
|
||||
};
|
||||
style.setProperty = (name: string, value: string | null) => {
|
||||
const normalizedName = camelize(name);
|
||||
if (!value) delete style[normalizedName];
|
||||
else style[normalizedName] = value;
|
||||
this.attributes.style = toCSSText(style);
|
||||
};
|
||||
// This is used to bypass the smoothscroll polyfill in rrweb player.
|
||||
style.scrollBehavior = '';
|
||||
return style;
|
||||
return (this._style as unknown) as CSSStyleDeclaration;
|
||||
}
|
||||
|
||||
get firstElementChild(): RRElement | null {
|
||||
for (let child of this.children)
|
||||
if (child instanceof RRElement) return child;
|
||||
return null;
|
||||
attachShadow(_init: ShadowRootInit): RRElement {
|
||||
return super.attachShadow(_init) as RRElement;
|
||||
}
|
||||
|
||||
get nextElementSibling(): RRElement | null {
|
||||
let parentNode = this.parentNode;
|
||||
if (!parentNode) return null;
|
||||
const siblings = parentNode.children;
|
||||
let index = siblings.indexOf(this);
|
||||
for (let i = index + 1; i < siblings.length; i++)
|
||||
if (siblings[i] instanceof RRElement) return siblings[i] as RRElement;
|
||||
return null;
|
||||
appendChild(newChild: RRNode): RRNode {
|
||||
return super.appendChild(newChild) as RRNode;
|
||||
}
|
||||
|
||||
insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode {
|
||||
return super.insertBefore(newChild, refChild) as RRNode;
|
||||
}
|
||||
|
||||
getAttribute(name: string) {
|
||||
@@ -531,57 +226,44 @@ export class RRElement extends RRNode {
|
||||
this.attributes[name.toLowerCase()] = attribute;
|
||||
}
|
||||
|
||||
hasAttribute(name: string) {
|
||||
return (name && name.toLowerCase()) in this.attributes;
|
||||
}
|
||||
|
||||
setAttributeNS(
|
||||
_namespace: string | null,
|
||||
qualifiedName: string,
|
||||
value: string,
|
||||
): void {
|
||||
this.setAttribute(qualifiedName, value);
|
||||
}
|
||||
|
||||
removeAttribute(name: string) {
|
||||
delete this.attributes[name];
|
||||
delete this.attributes[name.toLowerCase()];
|
||||
}
|
||||
|
||||
appendChild(newChild: RRNode): RRNode {
|
||||
this.children.push(newChild);
|
||||
newChild.parentNode = this;
|
||||
newChild.parentElement = this;
|
||||
newChild.ownerDocument = this.ownerDocument;
|
||||
return newChild;
|
||||
get firstElementChild(): RRElement | null {
|
||||
for (let child of this.childNodes)
|
||||
if (child.RRNodeType === RRNodeType.Element) return child as RRElement;
|
||||
return null;
|
||||
}
|
||||
|
||||
insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode {
|
||||
if (refChild === null) return this.appendChild(newChild);
|
||||
const childIndex = this.children.indexOf(refChild);
|
||||
if (childIndex == -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.",
|
||||
);
|
||||
this.children.splice(childIndex, 0, newChild);
|
||||
newChild.parentElement = null;
|
||||
newChild.parentNode = this;
|
||||
newChild.ownerDocument = this.ownerDocument;
|
||||
return newChild;
|
||||
get nextElementSibling(): RRElement | null {
|
||||
let parentNode = this.parentNode;
|
||||
if (!parentNode) return null;
|
||||
const siblings = parentNode.childNodes;
|
||||
let index = siblings.indexOf(this);
|
||||
for (let i = index + 1; i < siblings.length; i++)
|
||||
if (siblings[i] instanceof RRElement) return siblings[i] as RRElement;
|
||||
return null;
|
||||
}
|
||||
|
||||
querySelectorAll(selectors: string): RRNode[] {
|
||||
const result: RRElement[] = [];
|
||||
if (this.ownerDocument !== null) {
|
||||
return (this.ownerDocument.nwsapi.select(
|
||||
((this.ownerDocument as RRDocument).nwsapi.select(
|
||||
selectors,
|
||||
(this as unknown) as Element,
|
||||
(element) => {
|
||||
if (((element as unknown) as RRElement) !== this)
|
||||
result.push((element as unknown) as RRElement);
|
||||
},
|
||||
) as unknown) as RRNode[];
|
||||
}
|
||||
return [];
|
||||
return result;
|
||||
}
|
||||
|
||||
getElementById(elementId: string): RRElement | null {
|
||||
if (this instanceof RRElement && this.id === elementId) return this;
|
||||
for (const child of this.children) {
|
||||
if (this.id === elementId) return this;
|
||||
for (const child of this.childNodes) {
|
||||
if (child instanceof RRElement) {
|
||||
const result = child.getElementById(elementId);
|
||||
if (result !== null) return result;
|
||||
@@ -596,12 +278,12 @@ export class RRElement extends RRNode {
|
||||
// Make sure this element has all queried class names.
|
||||
if (
|
||||
this instanceof RRElement &&
|
||||
queryClassList.filter((queriedClassName) =>
|
||||
this.classList.some((name) => name === queriedClassName),
|
||||
).length == queryClassList.length
|
||||
queryClassList.classes.filter((queriedClassName) =>
|
||||
this.classList.classes.some((name) => name === queriedClassName),
|
||||
).length == queryClassList.classes.length
|
||||
)
|
||||
elements.push(this);
|
||||
for (const child of this.children) {
|
||||
for (const child of this.childNodes) {
|
||||
if (child instanceof RRElement)
|
||||
elements = elements.concat(child.getElementsByClassName(className));
|
||||
}
|
||||
@@ -613,32 +295,12 @@ export class RRElement extends RRNode {
|
||||
const normalizedTagName = tagName.toUpperCase();
|
||||
if (this instanceof RRElement && this.tagName === normalizedTagName)
|
||||
elements.push(this);
|
||||
for (const child of this.children) {
|
||||
for (const child of this.childNodes) {
|
||||
if (child instanceof RRElement)
|
||||
elements = elements.concat(child.getElementsByTagName(tagName));
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
dispatchEvent(_event: Event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a shadow root for element and returns it.
|
||||
*/
|
||||
attachShadow(init: ShadowRootInit): RRElement {
|
||||
this.shadowRoot = init.mode === 'open' ? this : null;
|
||||
return this;
|
||||
}
|
||||
|
||||
toString() {
|
||||
let attributeString = '';
|
||||
for (let attribute in this.attributes) {
|
||||
attributeString += `${attribute}="${this.attributes[attribute]}" `;
|
||||
}
|
||||
return `${super.toString(this.tagName)} ${attributeString}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class RRImageElement extends RRElement {
|
||||
@@ -648,16 +310,7 @@ export class RRImageElement extends RRElement {
|
||||
onload: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||
}
|
||||
|
||||
export class RRMediaElement extends RRElement {
|
||||
currentTime: number = 0;
|
||||
paused: boolean = true;
|
||||
async play() {
|
||||
this.paused = false;
|
||||
}
|
||||
async pause() {
|
||||
this.paused = true;
|
||||
}
|
||||
}
|
||||
export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {}
|
||||
|
||||
export class RRCanvasElement extends RRElement {
|
||||
/**
|
||||
@@ -675,7 +328,7 @@ export class RRStyleElement extends RRElement {
|
||||
if (!this._sheet) {
|
||||
let result = '';
|
||||
for (let child of this.childNodes)
|
||||
if (child.nodeType === NodeType.Text)
|
||||
if (child.RRNodeType === RRNodeType.Text)
|
||||
result += (child as RRText).textContent;
|
||||
this._sheet = cssom.parse(result);
|
||||
}
|
||||
@@ -683,7 +336,7 @@ export class RRStyleElement extends RRElement {
|
||||
}
|
||||
}
|
||||
|
||||
export class RRIframeElement extends RRElement {
|
||||
export class RRIFrameElement extends RRElement {
|
||||
width: string = '';
|
||||
height: string = '';
|
||||
src: string = '';
|
||||
@@ -699,89 +352,27 @@ export class RRIframeElement extends RRElement {
|
||||
}
|
||||
}
|
||||
|
||||
export class RRText extends RRNode {
|
||||
textContent: string;
|
||||
|
||||
constructor(data: string) {
|
||||
super();
|
||||
this.textContent = data;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${super.toString('RRText')} text=${JSON.stringify(
|
||||
this.textContent,
|
||||
)}`;
|
||||
}
|
||||
export class RRText extends BaseRRTextImpl(RRNode) {
|
||||
readonly nodeName: '#text' = '#text';
|
||||
}
|
||||
|
||||
export class RRComment extends RRNode {
|
||||
data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${super.toString('RRComment')} data=${JSON.stringify(this.data)}`;
|
||||
}
|
||||
export class RRComment extends BaseRRCommentImpl(RRNode) {
|
||||
readonly nodeName: '#comment' = '#comment';
|
||||
}
|
||||
export class RRCDATASection extends RRNode {
|
||||
data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${super.toString('RRCDATASection')} data=${JSON.stringify(
|
||||
this.data,
|
||||
)}`;
|
||||
}
|
||||
export class RRCDATASection extends BaseRRCDATASectionImpl(RRNode) {
|
||||
readonly nodeName: '#cdata-section' = '#cdata-section';
|
||||
}
|
||||
|
||||
interface RRElementTagNameMap {
|
||||
img: RRImageElement;
|
||||
audio: RRMediaElement;
|
||||
canvas: RRCanvasElement;
|
||||
iframe: RRIFrameElement;
|
||||
img: RRImageElement;
|
||||
style: RRStyleElement;
|
||||
video: RRMediaElement;
|
||||
}
|
||||
|
||||
type RRElementType<
|
||||
K extends keyof HTMLElementTagNameMap
|
||||
> = K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement;
|
||||
|
||||
class ClassList extends Array {
|
||||
private onChange: ((newClassText: string) => void) | undefined;
|
||||
|
||||
constructor(
|
||||
classText?: string,
|
||||
onChange?: ((newClassText: string) => void) | undefined,
|
||||
) {
|
||||
super();
|
||||
if (classText) {
|
||||
const classes = classText.trim().split(/\s+/);
|
||||
super.push(...classes);
|
||||
}
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
add = (...classNames: string[]) => {
|
||||
for (const item of classNames) {
|
||||
const className = String(item);
|
||||
if (super.indexOf(className) >= 0) continue;
|
||||
super.push(className);
|
||||
}
|
||||
this.onChange && this.onChange(super.join(' '));
|
||||
};
|
||||
|
||||
remove = (...classNames: string[]) => {
|
||||
for (const item of classNames) {
|
||||
const className = String(item);
|
||||
const index = super.indexOf(className);
|
||||
if (index < 0) continue;
|
||||
super.splice(index, 1);
|
||||
}
|
||||
this.onChange && this.onChange(super.join(' '));
|
||||
};
|
||||
}
|
||||
|
||||
723
packages/rrdom/src/document.ts
Normal file
723
packages/rrdom/src/document.ts
Normal file
@@ -0,0 +1,723 @@
|
||||
import { NodeType as RRNodeType } from 'rrweb-snapshot';
|
||||
import { parseCSSText, camelize, toCSSText } from './style';
|
||||
export interface IRRNode {
|
||||
parentElement: IRRNode | null;
|
||||
parentNode: IRRNode | null;
|
||||
childNodes: IRRNode[];
|
||||
ownerDocument: IRRDocument;
|
||||
readonly ELEMENT_NODE: number;
|
||||
readonly TEXT_NODE: number;
|
||||
// corresponding nodeType value of standard HTML Node
|
||||
readonly nodeType: number;
|
||||
readonly nodeName: string; // https://dom.spec.whatwg.org/#dom-node-nodename
|
||||
readonly RRNodeType: RRNodeType;
|
||||
|
||||
firstChild: IRRNode | null;
|
||||
|
||||
lastChild: IRRNode | null;
|
||||
|
||||
nextSibling: IRRNode | null;
|
||||
|
||||
textContent: string | null;
|
||||
|
||||
contains(node: IRRNode): boolean;
|
||||
|
||||
appendChild(newChild: IRRNode): IRRNode;
|
||||
|
||||
insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode;
|
||||
|
||||
removeChild(node: IRRNode): IRRNode;
|
||||
|
||||
toString(): string;
|
||||
}
|
||||
export interface IRRDocument extends IRRNode {
|
||||
documentElement: IRRElement | null;
|
||||
|
||||
body: IRRElement | null;
|
||||
|
||||
head: IRRElement | null;
|
||||
|
||||
implementation: IRRDocument;
|
||||
|
||||
firstElementChild: IRRElement | null;
|
||||
|
||||
readonly nodeName: '#document';
|
||||
|
||||
compatMode: 'BackCompat' | 'CSS1Compat';
|
||||
|
||||
createDocument(
|
||||
_namespace: string | null,
|
||||
_qualifiedName: string | null,
|
||||
_doctype?: DocumentType | null,
|
||||
): IRRDocument;
|
||||
|
||||
createDocumentType(
|
||||
qualifiedName: string,
|
||||
publicId: string,
|
||||
systemId: string,
|
||||
): IRRDocumentType;
|
||||
|
||||
createElement(tagName: string): IRRElement;
|
||||
|
||||
createElementNS(_namespaceURI: string, qualifiedName: string): IRRElement;
|
||||
|
||||
createTextNode(data: string): IRRText;
|
||||
|
||||
createComment(data: string): IRRComment;
|
||||
|
||||
createCDATASection(data: string): IRRCDATASection;
|
||||
|
||||
open(): void;
|
||||
|
||||
close(): void;
|
||||
|
||||
write(content: string): void;
|
||||
}
|
||||
export interface IRRElement extends IRRNode {
|
||||
tagName: string;
|
||||
attributes: Record<string, string>;
|
||||
shadowRoot: IRRElement | null;
|
||||
scrollLeft?: number;
|
||||
scrollTop?: number;
|
||||
id: string;
|
||||
className: string;
|
||||
classList: ClassList;
|
||||
style: CSSStyleDeclaration;
|
||||
|
||||
attachShadow(init: ShadowRootInit): IRRElement;
|
||||
|
||||
getAttribute(name: string): string | null;
|
||||
|
||||
setAttribute(name: string, attribute: string): void;
|
||||
|
||||
setAttributeNS(
|
||||
namespace: string | null,
|
||||
qualifiedName: string,
|
||||
value: string,
|
||||
): void;
|
||||
|
||||
removeAttribute(name: string): void;
|
||||
|
||||
dispatchEvent(event: Event): boolean;
|
||||
}
|
||||
export interface IRRDocumentType extends IRRNode {
|
||||
readonly name: string;
|
||||
readonly publicId: string;
|
||||
readonly systemId: string;
|
||||
}
|
||||
export interface IRRText extends IRRNode {
|
||||
readonly nodeName: '#text';
|
||||
data: string;
|
||||
}
|
||||
export interface IRRComment extends IRRNode {
|
||||
readonly nodeName: '#comment';
|
||||
data: string;
|
||||
}
|
||||
export interface IRRCDATASection extends IRRNode {
|
||||
readonly nodeName: '#cdata-section';
|
||||
data: string;
|
||||
}
|
||||
|
||||
type ConstrainedConstructor<T = {}> = new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
* This is designed as an abstract class so it should never be instantiated.
|
||||
*/
|
||||
export class BaseRRNode implements IRRNode {
|
||||
public childNodes: IRRNode[] = [];
|
||||
public parentElement: IRRNode | null = null;
|
||||
public parentNode: IRRNode | null = null;
|
||||
public textContent: string | null;
|
||||
public ownerDocument: IRRDocument;
|
||||
public readonly ELEMENT_NODE: number = NodeType.ELEMENT_NODE;
|
||||
public readonly TEXT_NODE: number = NodeType.TEXT_NODE;
|
||||
// corresponding nodeType value of standard HTML Node
|
||||
public readonly nodeType: number;
|
||||
public readonly nodeName: string;
|
||||
public readonly RRNodeType: RRNodeType;
|
||||
|
||||
constructor(...args: any[]) {}
|
||||
|
||||
public get firstChild(): IRRNode | null {
|
||||
return this.childNodes[0] || null;
|
||||
}
|
||||
|
||||
public get lastChild(): IRRNode | null {
|
||||
return this.childNodes[this.childNodes.length - 1] || null;
|
||||
}
|
||||
|
||||
public get nextSibling(): IRRNode | null {
|
||||
let parentNode = this.parentNode;
|
||||
if (!parentNode) return null;
|
||||
const siblings = parentNode.childNodes;
|
||||
let index = siblings.indexOf(this);
|
||||
return siblings[index + 1] || null;
|
||||
}
|
||||
|
||||
public contains(node: IRRNode) {
|
||||
if (node === this) return true;
|
||||
for (const child of this.childNodes) {
|
||||
if (child.contains(node)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public appendChild(_newChild: IRRNode): IRRNode {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method.`,
|
||||
);
|
||||
}
|
||||
|
||||
public insertBefore(_newChild: IRRNode, _refChild: IRRNode | null): IRRNode {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method.`,
|
||||
);
|
||||
}
|
||||
|
||||
public removeChild(node: IRRNode): IRRNode {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'removeChild' on 'RRNode': This RRNode type does not support this method.`,
|
||||
);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'RRNode';
|
||||
}
|
||||
}
|
||||
|
||||
export function BaseRRDocumentImpl<
|
||||
RRNode extends ConstrainedConstructor<IRRNode>
|
||||
>(RRNodeClass: RRNode) {
|
||||
return class BaseRRDocument extends RRNodeClass implements IRRDocument {
|
||||
public readonly nodeType: number = NodeType.DOCUMENT_NODE;
|
||||
public readonly nodeName: '#document' = '#document';
|
||||
public readonly compatMode: 'BackCompat' | 'CSS1Compat' = 'CSS1Compat';
|
||||
public readonly RRNodeType = RRNodeType.Document;
|
||||
public textContent: string | null = null;
|
||||
|
||||
public get documentElement(): IRRElement | null {
|
||||
return (
|
||||
(this.childNodes.find(
|
||||
(node) =>
|
||||
node.RRNodeType === RRNodeType.Element &&
|
||||
(node as IRRElement).tagName === 'HTML',
|
||||
) as IRRElement) || null
|
||||
);
|
||||
}
|
||||
|
||||
public get body(): IRRElement | null {
|
||||
return (
|
||||
(this.documentElement?.childNodes.find(
|
||||
(node) =>
|
||||
node.RRNodeType === RRNodeType.Element &&
|
||||
(node as IRRElement).tagName === 'BODY',
|
||||
) as IRRElement) || null
|
||||
);
|
||||
}
|
||||
|
||||
public get head(): IRRElement | null {
|
||||
return (
|
||||
(this.documentElement?.childNodes.find(
|
||||
(node) =>
|
||||
node.RRNodeType === RRNodeType.Element &&
|
||||
(node as IRRElement).tagName === 'HEAD',
|
||||
) as IRRElement) || null
|
||||
);
|
||||
}
|
||||
|
||||
public get implementation(): IRRDocument {
|
||||
return this;
|
||||
}
|
||||
|
||||
public get firstElementChild(): IRRElement | null {
|
||||
return this.documentElement;
|
||||
}
|
||||
|
||||
public appendChild(childNode: IRRNode): IRRNode {
|
||||
const nodeType = childNode.RRNodeType;
|
||||
if (
|
||||
nodeType === RRNodeType.Element ||
|
||||
nodeType === RRNodeType.DocumentType
|
||||
) {
|
||||
if (this.childNodes.some((s) => s.RRNodeType === nodeType)) {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one ${
|
||||
nodeType === RRNodeType.Element ? 'RRElement' : 'RRDoctype'
|
||||
} on RRDocument allowed.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
childNode.parentElement = null;
|
||||
childNode.parentNode = this;
|
||||
this.childNodes.push(childNode);
|
||||
return childNode;
|
||||
}
|
||||
|
||||
public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode {
|
||||
const nodeType = newChild.RRNodeType;
|
||||
if (
|
||||
nodeType === RRNodeType.Element ||
|
||||
nodeType === RRNodeType.DocumentType
|
||||
) {
|
||||
if (this.childNodes.some((s) => s.RRNodeType === nodeType)) {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one ${
|
||||
nodeType === RRNodeType.Element ? 'RRElement' : 'RRDoctype'
|
||||
} on RRDocument allowed.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (refChild === null) return this.appendChild(newChild);
|
||||
const childIndex = this.childNodes.indexOf(refChild);
|
||||
if (childIndex == -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(childIndex, 0, newChild);
|
||||
newChild.parentElement = null;
|
||||
newChild.parentNode = this;
|
||||
return newChild;
|
||||
}
|
||||
|
||||
public removeChild(node: IRRNode) {
|
||||
const indexOfChild = this.childNodes.indexOf(node);
|
||||
if (indexOfChild === -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'removeChild' on 'RRDocument': The RRNode to be removed is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(indexOfChild, 1);
|
||||
node.parentElement = null;
|
||||
node.parentNode = null;
|
||||
return node;
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.childNodes = [];
|
||||
}
|
||||
|
||||
public close() {}
|
||||
|
||||
/**
|
||||
* Adhoc implementation for setting xhtml namespace in rebuilt.ts (rrweb-snapshot).
|
||||
* There are two lines used this function:
|
||||
* 1. doc.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "">')
|
||||
* 2. doc.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "">')
|
||||
*/
|
||||
public write(content: string) {
|
||||
let publicId;
|
||||
if (
|
||||
content ===
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "">'
|
||||
)
|
||||
publicId = '-//W3C//DTD XHTML 1.0 Transitional//EN';
|
||||
else if (
|
||||
content ===
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "">'
|
||||
)
|
||||
publicId = '-//W3C//DTD HTML 4.0 Transitional//EN';
|
||||
if (publicId) {
|
||||
const doctype = this.createDocumentType('html', publicId, '');
|
||||
this.open();
|
||||
this.appendChild(doctype);
|
||||
}
|
||||
}
|
||||
|
||||
createDocument(
|
||||
_namespace: string | null,
|
||||
_qualifiedName: string | null,
|
||||
_doctype?: DocumentType | null,
|
||||
): IRRDocument {
|
||||
return new BaseRRDocument();
|
||||
}
|
||||
|
||||
createDocumentType(
|
||||
qualifiedName: string,
|
||||
publicId: string,
|
||||
systemId: string,
|
||||
): IRRDocumentType {
|
||||
const doctype = new (BaseRRDocumentTypeImpl(BaseRRNode))(
|
||||
qualifiedName,
|
||||
publicId,
|
||||
systemId,
|
||||
);
|
||||
doctype.ownerDocument = this;
|
||||
return doctype;
|
||||
}
|
||||
|
||||
createElement(tagName: string): IRRElement {
|
||||
const element = new (BaseRRElementImpl(BaseRRNode))(tagName);
|
||||
element.ownerDocument = this;
|
||||
return element;
|
||||
}
|
||||
|
||||
createElementNS(_namespaceURI: string, qualifiedName: string): IRRElement {
|
||||
return this.createElement(qualifiedName);
|
||||
}
|
||||
|
||||
createTextNode(data: string): IRRText {
|
||||
const text = new (BaseRRTextImpl(BaseRRNode))(data);
|
||||
text.ownerDocument = this;
|
||||
return text;
|
||||
}
|
||||
|
||||
createComment(data: string): IRRComment {
|
||||
const comment = new (BaseRRCommentImpl(BaseRRNode))(data);
|
||||
comment.ownerDocument = this;
|
||||
return comment;
|
||||
}
|
||||
|
||||
createCDATASection(data: string): IRRCDATASection {
|
||||
const CDATASection = new (BaseRRCDATASectionImpl(BaseRRNode))(data);
|
||||
CDATASection.ownerDocument = this;
|
||||
return CDATASection;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'RRDocument';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRDocumentTypeImpl<
|
||||
RRNode extends ConstrainedConstructor<IRRNode>
|
||||
>(RRNodeClass: RRNode) {
|
||||
// @ts-ignore
|
||||
return class BaseRRDocumentType
|
||||
extends RRNodeClass
|
||||
implements IRRDocumentType {
|
||||
public readonly nodeType: number = NodeType.DOCUMENT_TYPE_NODE;
|
||||
public readonly RRNodeType = RRNodeType.DocumentType;
|
||||
public readonly nodeName: string;
|
||||
public readonly name: string;
|
||||
public readonly publicId: string;
|
||||
public readonly systemId: string;
|
||||
public textContent: string | null = null;
|
||||
|
||||
constructor(qualifiedName: string, publicId: string, systemId: string) {
|
||||
super();
|
||||
this.name = qualifiedName;
|
||||
this.publicId = publicId;
|
||||
this.systemId = systemId;
|
||||
this.nodeName = qualifiedName;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'RRDocumentType';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRElementImpl<
|
||||
RRNode extends ConstrainedConstructor<IRRNode>
|
||||
>(RRNodeClass: RRNode) {
|
||||
// @ts-ignore
|
||||
return class BaseRRElement extends RRNodeClass implements IRRElement {
|
||||
public readonly nodeType: number = NodeType.ELEMENT_NODE;
|
||||
public readonly RRNodeType = RRNodeType.Element;
|
||||
public readonly nodeName: string;
|
||||
public tagName: string;
|
||||
public attributes: Record<string, string> = {};
|
||||
public shadowRoot: IRRElement | null = null;
|
||||
public scrollLeft?: number;
|
||||
public scrollTop?: number;
|
||||
|
||||
constructor(tagName: string) {
|
||||
super();
|
||||
this.tagName = tagName.toUpperCase();
|
||||
this.nodeName = tagName.toUpperCase();
|
||||
}
|
||||
|
||||
public get textContent(): string {
|
||||
let result = '';
|
||||
this.childNodes.forEach((node) => (result += node.textContent));
|
||||
return result;
|
||||
}
|
||||
|
||||
public set textContent(textContent: string) {
|
||||
this.childNodes = [this.ownerDocument.createTextNode(textContent)];
|
||||
}
|
||||
|
||||
public get classList(): ClassList {
|
||||
return new ClassList(
|
||||
this.attributes.class as string | undefined,
|
||||
(newClassName) => {
|
||||
this.attributes.class = newClassName;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public get id() {
|
||||
return this.attributes.id || '';
|
||||
}
|
||||
|
||||
public get className() {
|
||||
return this.attributes.class || '';
|
||||
}
|
||||
|
||||
public get style() {
|
||||
const style = (this.attributes.style
|
||||
? parseCSSText(this.attributes.style as string)
|
||||
: {}) as CSSStyleDeclaration;
|
||||
const hyphenateRE = /\B([A-Z])/g;
|
||||
style.setProperty = (
|
||||
name: string,
|
||||
value: string | null,
|
||||
priority?: string,
|
||||
) => {
|
||||
if (hyphenateRE.test(name)) return;
|
||||
const normalizedName = camelize(name);
|
||||
if (!value) delete style[normalizedName];
|
||||
else style[normalizedName] = value;
|
||||
if (priority === 'important') style[normalizedName] += ' !important';
|
||||
this.attributes.style = toCSSText(style);
|
||||
};
|
||||
style.removeProperty = (name: string) => {
|
||||
if (hyphenateRE.test(name)) return '';
|
||||
const normalizedName = camelize(name);
|
||||
const value = style[normalizedName] || '';
|
||||
delete style[normalizedName];
|
||||
this.attributes.style = toCSSText(style);
|
||||
return value;
|
||||
};
|
||||
return style;
|
||||
}
|
||||
|
||||
public getAttribute(name: string) {
|
||||
return this.attributes[name] || null;
|
||||
}
|
||||
|
||||
public setAttribute(name: string, attribute: string) {
|
||||
this.attributes[name] = attribute;
|
||||
}
|
||||
|
||||
public setAttributeNS(
|
||||
_namespace: string | null,
|
||||
qualifiedName: string,
|
||||
value: string,
|
||||
): void {
|
||||
this.setAttribute(qualifiedName, value);
|
||||
}
|
||||
|
||||
public removeAttribute(name: string) {
|
||||
delete this.attributes[name];
|
||||
}
|
||||
|
||||
public appendChild(newChild: IRRNode): IRRNode {
|
||||
this.childNodes.push(newChild);
|
||||
newChild.parentNode = this;
|
||||
newChild.parentElement = this;
|
||||
return newChild;
|
||||
}
|
||||
|
||||
public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode {
|
||||
if (refChild === null) return this.appendChild(newChild);
|
||||
const childIndex = this.childNodes.indexOf(refChild);
|
||||
if (childIndex == -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(childIndex, 0, newChild);
|
||||
newChild.parentElement = this;
|
||||
newChild.parentNode = this;
|
||||
return newChild;
|
||||
}
|
||||
|
||||
public removeChild(node: IRRNode): IRRNode {
|
||||
const indexOfChild = this.childNodes.indexOf(node);
|
||||
if (indexOfChild === -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'removeChild' on 'RRElement': The RRNode to be removed is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(indexOfChild, 1);
|
||||
node.parentElement = null;
|
||||
node.parentNode = null;
|
||||
return node;
|
||||
}
|
||||
|
||||
public attachShadow(_init: ShadowRootInit): IRRElement {
|
||||
const shadowRoot = this.ownerDocument.createElement('SHADOWROOT');
|
||||
this.shadowRoot = shadowRoot;
|
||||
return shadowRoot;
|
||||
}
|
||||
|
||||
public dispatchEvent(_event: Event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
toString() {
|
||||
let attributeString = '';
|
||||
for (let attribute in this.attributes) {
|
||||
attributeString += `${attribute}="${this.attributes[attribute]}" `;
|
||||
}
|
||||
return `${this.tagName} ${attributeString}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRMediaElementImpl<
|
||||
RRElement extends ConstrainedConstructor<IRRElement>
|
||||
>(RRElementClass: RRElement) {
|
||||
return class BaseRRMediaElement extends RRElementClass {
|
||||
public currentTime?: number;
|
||||
public volume?: number;
|
||||
public paused?: boolean;
|
||||
public muted?: boolean;
|
||||
attachShadow(_init: ShadowRootInit): IRRElement {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'attachShadow' on 'RRElement': This RRElement does not support attachShadow`,
|
||||
);
|
||||
}
|
||||
public play() {
|
||||
this.paused = false;
|
||||
}
|
||||
public pause() {
|
||||
this.paused = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRTextImpl<RRNode extends ConstrainedConstructor<IRRNode>>(
|
||||
RRNodeClass: RRNode,
|
||||
) {
|
||||
// @ts-ignore
|
||||
return class BaseRRText extends RRNodeClass implements IRRText {
|
||||
public readonly nodeType: number = NodeType.TEXT_NODE;
|
||||
public readonly nodeName: '#text' = '#text';
|
||||
public readonly RRNodeType = RRNodeType.Text;
|
||||
public data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public get textContent(): string {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public set textContent(textContent: string) {
|
||||
this.data = textContent;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `RRText text=${JSON.stringify(this.data)}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRCommentImpl<
|
||||
RRNode extends ConstrainedConstructor<IRRNode>
|
||||
>(RRNodeClass: RRNode) {
|
||||
// @ts-ignore
|
||||
return class BaseRRComment extends RRNodeClass implements IRRComment {
|
||||
public readonly nodeType: number = NodeType.COMMENT_NODE;
|
||||
public readonly nodeName: '#comment' = '#comment';
|
||||
public readonly RRNodeType = RRNodeType.Comment;
|
||||
public data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public get textContent(): string {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public set textContent(textContent: string) {
|
||||
this.data = textContent;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `RRComment text=${JSON.stringify(this.data)}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRCDATASectionImpl<
|
||||
RRNode extends ConstrainedConstructor<IRRNode>
|
||||
>(RRNodeClass: RRNode) {
|
||||
// @ts-ignore
|
||||
return class BaseRRCDATASection
|
||||
extends RRNodeClass
|
||||
implements IRRCDATASection {
|
||||
public readonly nodeName: '#cdata-section' = '#cdata-section';
|
||||
public readonly nodeType: number = NodeType.CDATA_SECTION_NODE;
|
||||
public readonly RRNodeType = RRNodeType.CDATA;
|
||||
public data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public get textContent(): string {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public set textContent(textContent: string) {
|
||||
this.data = textContent;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `RRCDATASection data=${JSON.stringify(this.data)}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class ClassList {
|
||||
private onChange: ((newClassText: string) => void) | undefined;
|
||||
classes: string[] = [];
|
||||
|
||||
constructor(
|
||||
classText?: string,
|
||||
onChange?: ((newClassText: string) => void) | undefined,
|
||||
) {
|
||||
if (classText) {
|
||||
const classes = classText.trim().split(/\s+/);
|
||||
this.classes.push(...classes);
|
||||
}
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
add = (...classNames: string[]) => {
|
||||
for (const item of classNames) {
|
||||
const className = String(item);
|
||||
if (this.classes.indexOf(className) >= 0) continue;
|
||||
this.classes.push(className);
|
||||
}
|
||||
this.onChange && this.onChange(this.classes.join(' '));
|
||||
};
|
||||
|
||||
remove = (...classNames: string[]) => {
|
||||
this.classes = this.classes.filter(
|
||||
(item) => classNames.indexOf(item) === -1,
|
||||
);
|
||||
this.onChange && this.onChange(this.classes.join(' '));
|
||||
};
|
||||
}
|
||||
|
||||
export type CSSStyleDeclaration = Record<string, string> & {
|
||||
setProperty: (
|
||||
name: string,
|
||||
value: string | null,
|
||||
priority?: string | null,
|
||||
) => void;
|
||||
removeProperty: (name: string) => string;
|
||||
};
|
||||
|
||||
// Enumerate nodeType value of standard HTML Node.
|
||||
export enum NodeType {
|
||||
PLACEHOLDER, // This isn't a node type. Enum type value starts from zero but NodeType value starts from 1.
|
||||
ELEMENT_NODE,
|
||||
ATTRIBUTE_NODE,
|
||||
TEXT_NODE,
|
||||
CDATA_SECTION_NODE,
|
||||
ENTITY_REFERENCE_NODE,
|
||||
ENTITY_NODE,
|
||||
PROCESSING_INSTRUCTION_NODE,
|
||||
COMMENT_NODE,
|
||||
DOCUMENT_NODE,
|
||||
DOCUMENT_TYPE_NODE,
|
||||
DOCUMENT_FRAGMENT_NODE,
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { RRDocument, RRNode } from './document-nodejs';
|
||||
|
||||
/**
|
||||
* Polyfill the performance for nodejs.
|
||||
* Note: The performance api is available through the global object from nodejs v16.0.0.
|
||||
* https://github.com/nodejs/node/pull/37970
|
||||
*/
|
||||
export function polyfillPerformance() {
|
||||
if (typeof window !== 'undefined' || 'performance' in global) return;
|
||||
@@ -80,8 +82,8 @@ export function polyfillDocument() {
|
||||
const rrdom = new RRDocument();
|
||||
(() => {
|
||||
rrdom.appendChild(rrdom.createElement('html'));
|
||||
rrdom.documentElement.appendChild(rrdom.createElement('head'));
|
||||
rrdom.documentElement.appendChild(rrdom.createElement('body'));
|
||||
rrdom.documentElement!.appendChild(rrdom.createElement('head'));
|
||||
rrdom.documentElement!.appendChild(rrdom.createElement('body'));
|
||||
})();
|
||||
global.document = (rrdom as unknown) as Document;
|
||||
}
|
||||
|
||||
@@ -1,40 +1,45 @@
|
||||
export function parseCSSText(cssText: string): Record<string, string> {
|
||||
const res: Record<string, string> = {};
|
||||
const listDelimiter = /;(?![^(]*\))/g;
|
||||
const propertyDelimiter = /:(.+)/;
|
||||
cssText.split(listDelimiter).forEach(function (item) {
|
||||
if (item) {
|
||||
const tmp = item.split(propertyDelimiter);
|
||||
tmp.length > 1 && (res[camelize(tmp[0].trim())] = tmp[1].trim());
|
||||
}
|
||||
});
|
||||
return res;
|
||||
const res: Record<string, string> = {};
|
||||
const listDelimiter = /;(?![^(]*\))/g;
|
||||
const propertyDelimiter = /:(.+)/;
|
||||
const comment = /\/\*.*?\*\//g;
|
||||
cssText
|
||||
.replace(comment, '')
|
||||
.split(listDelimiter)
|
||||
.forEach(function (item) {
|
||||
if (item) {
|
||||
const tmp = item.split(propertyDelimiter);
|
||||
tmp.length > 1 && (res[camelize(tmp[0].trim())] = tmp[1].trim());
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
export function toCSSText(style: Record<string, string>): string {
|
||||
const properties = [];
|
||||
for (let name in style) {
|
||||
const value = style[name];
|
||||
if (typeof value !== 'string') continue;
|
||||
const normalizedName = hyphenate(name);
|
||||
properties.push(`${normalizedName}: ${value};`);
|
||||
}
|
||||
|
||||
export function toCSSText(style: Record<string, string>): string {
|
||||
const properties = [];
|
||||
for (let name in style) {
|
||||
const value = style[name];
|
||||
if (typeof value !== 'string') continue;
|
||||
const normalizedName = hyphenate(name);
|
||||
properties.push(`${normalizedName}:${value};`);
|
||||
}
|
||||
return properties.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Camelize a hyphen-delimited string.
|
||||
*/
|
||||
const camelizeRE = /-(\w)/g;
|
||||
export const camelize = (str: string): string => {
|
||||
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
|
||||
};
|
||||
|
||||
/**
|
||||
* Hyphenate a camelCase string.
|
||||
*/
|
||||
const hyphenateRE = /\B([A-Z])/g;
|
||||
export const hyphenate = (str: string): string => {
|
||||
return str.replace(hyphenateRE, '-$1').toLowerCase();
|
||||
};
|
||||
|
||||
return properties.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Camelize a hyphen-delimited string.
|
||||
*/
|
||||
const camelizeRE = /-([a-z])/g;
|
||||
const CUSTOM_PROPERTY_REGEX = /^--[a-zA-Z0-9-]+$/;
|
||||
export const camelize = (str: string): string => {
|
||||
if (CUSTOM_PROPERTY_REGEX.test(str)) return str;
|
||||
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
|
||||
};
|
||||
|
||||
/**
|
||||
* Hyphenate a camelCase string.
|
||||
*/
|
||||
const hyphenateRE = /\B([A-Z])/g;
|
||||
export const hyphenate = (str: string): string => {
|
||||
return str.replace(hyphenateRE, '-$1').toLowerCase();
|
||||
};
|
||||
|
||||
450
packages/rrdom/src/virtual-dom.ts
Normal file
450
packages/rrdom/src/virtual-dom.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
import {
|
||||
NodeType as RRNodeType,
|
||||
createMirror as createNodeMirror,
|
||||
} from 'rrweb-snapshot';
|
||||
import type {
|
||||
Mirror as NodeMirror,
|
||||
IMirror,
|
||||
serializedNodeWithId,
|
||||
} from 'rrweb-snapshot';
|
||||
import type {
|
||||
canvasMutationData,
|
||||
canvasEventWithTime,
|
||||
inputData,
|
||||
scrollData,
|
||||
} from 'rrweb/src/types';
|
||||
import {
|
||||
BaseRRNode as RRNode,
|
||||
BaseRRCDATASectionImpl,
|
||||
BaseRRCommentImpl,
|
||||
BaseRRDocumentImpl,
|
||||
BaseRRDocumentTypeImpl,
|
||||
BaseRRElementImpl,
|
||||
BaseRRMediaElementImpl,
|
||||
BaseRRTextImpl,
|
||||
IRRDocument,
|
||||
IRRElement,
|
||||
IRRNode,
|
||||
NodeType,
|
||||
IRRDocumentType,
|
||||
IRRText,
|
||||
IRRComment,
|
||||
} from './document';
|
||||
import type { VirtualStyleRules } from './diff';
|
||||
|
||||
export class RRDocument extends BaseRRDocumentImpl(RRNode) {
|
||||
// In the rrweb replayer, there are some unserialized nodes like the element that stores the injected style rules.
|
||||
// These unserialized nodes may interfere the execution of the diff algorithm.
|
||||
// The id of serialized node is larger than 0. So this value less than 0 is used as id for these unserialized nodes.
|
||||
private _unserializedId = -1;
|
||||
|
||||
/**
|
||||
* Every time the id is used, it will minus 1 automatically to avoid collisions.
|
||||
*/
|
||||
public get unserializedId(): number {
|
||||
return this._unserializedId--;
|
||||
}
|
||||
|
||||
public mirror: Mirror = createMirror();
|
||||
|
||||
public scrollData: scrollData | null = null;
|
||||
|
||||
constructor(mirror?: Mirror) {
|
||||
super();
|
||||
if (mirror) {
|
||||
this.mirror = mirror;
|
||||
}
|
||||
}
|
||||
|
||||
createDocument(
|
||||
_namespace: string | null,
|
||||
_qualifiedName: string | null,
|
||||
_doctype?: DocumentType | null,
|
||||
) {
|
||||
return new RRDocument();
|
||||
}
|
||||
|
||||
createDocumentType(
|
||||
qualifiedName: string,
|
||||
publicId: string,
|
||||
systemId: string,
|
||||
) {
|
||||
const documentTypeNode = new RRDocumentType(
|
||||
qualifiedName,
|
||||
publicId,
|
||||
systemId,
|
||||
);
|
||||
documentTypeNode.ownerDocument = this;
|
||||
return documentTypeNode;
|
||||
}
|
||||
|
||||
createElement<K extends keyof HTMLElementTagNameMap>(
|
||||
tagName: K,
|
||||
): RRElementType<K>;
|
||||
createElement(tagName: string): RRElement;
|
||||
createElement(tagName: string) {
|
||||
const upperTagName = tagName.toUpperCase();
|
||||
let element;
|
||||
switch (upperTagName) {
|
||||
case 'AUDIO':
|
||||
case 'VIDEO':
|
||||
element = new RRMediaElement(upperTagName);
|
||||
break;
|
||||
case 'IFRAME':
|
||||
element = new RRIFrameElement(upperTagName, this.mirror);
|
||||
break;
|
||||
case 'CANVAS':
|
||||
element = new RRCanvasElement(upperTagName);
|
||||
break;
|
||||
case 'STYLE':
|
||||
element = new RRStyleElement(upperTagName);
|
||||
break;
|
||||
default:
|
||||
element = new RRElement(upperTagName);
|
||||
break;
|
||||
}
|
||||
element.ownerDocument = this;
|
||||
return element;
|
||||
}
|
||||
|
||||
createComment(data: string) {
|
||||
const commentNode = new RRComment(data);
|
||||
commentNode.ownerDocument = this;
|
||||
return commentNode;
|
||||
}
|
||||
|
||||
createCDATASection(data: string) {
|
||||
const sectionNode = new RRCDATASection(data);
|
||||
sectionNode.ownerDocument = this;
|
||||
return sectionNode;
|
||||
}
|
||||
|
||||
createTextNode(data: string) {
|
||||
const textNode = new RRText(data);
|
||||
textNode.ownerDocument = this;
|
||||
return textNode;
|
||||
}
|
||||
|
||||
destroyTree() {
|
||||
this.childNodes = [];
|
||||
this.mirror.reset();
|
||||
}
|
||||
|
||||
open() {
|
||||
super.open();
|
||||
this._unserializedId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
export const RRDocumentType = BaseRRDocumentTypeImpl(RRNode);
|
||||
|
||||
export class RRElement extends BaseRRElementImpl(RRNode) {
|
||||
inputData: inputData | null = null;
|
||||
scrollData: scrollData | null = null;
|
||||
}
|
||||
|
||||
export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {}
|
||||
|
||||
export class RRCanvasElement extends RRElement implements IRRElement {
|
||||
public canvasMutations: {
|
||||
event: canvasEventWithTime;
|
||||
mutation: canvasMutationData;
|
||||
}[] = [];
|
||||
/**
|
||||
* This is a dummy implementation to distinguish RRCanvasElement from real HTMLCanvasElement.
|
||||
*/
|
||||
getContext(): RenderingContext | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class RRStyleElement extends RRElement {
|
||||
public rules: VirtualStyleRules = [];
|
||||
}
|
||||
|
||||
export class RRIFrameElement extends RRElement {
|
||||
contentDocument: RRDocument = new RRDocument();
|
||||
constructor(upperTagName: string, mirror: Mirror) {
|
||||
super(upperTagName);
|
||||
this.contentDocument.mirror = mirror;
|
||||
}
|
||||
}
|
||||
|
||||
export const RRText = BaseRRTextImpl(RRNode);
|
||||
export type RRText = typeof RRText;
|
||||
|
||||
export const RRComment = BaseRRCommentImpl(RRNode);
|
||||
export type RRComment = typeof RRComment;
|
||||
|
||||
export const RRCDATASection = BaseRRCDATASectionImpl(RRNode);
|
||||
export type RRCDATASection = typeof RRCDATASection;
|
||||
|
||||
interface RRElementTagNameMap {
|
||||
audio: RRMediaElement;
|
||||
canvas: RRCanvasElement;
|
||||
iframe: RRIFrameElement;
|
||||
style: RRStyleElement;
|
||||
video: RRMediaElement;
|
||||
}
|
||||
|
||||
type RRElementType<
|
||||
K extends keyof HTMLElementTagNameMap
|
||||
> = K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement;
|
||||
|
||||
function getValidTagName(element: HTMLElement): string {
|
||||
// https://github.com/rrweb-io/rrweb-snapshot/issues/56
|
||||
if (element instanceof HTMLFormElement) {
|
||||
return 'FORM';
|
||||
}
|
||||
return element.tagName.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a RRNode from a real Node.
|
||||
* @param node the real Node
|
||||
* @param rrdom the RRDocument
|
||||
* @param domMirror the NodeMirror that records the real document tree
|
||||
* @returns the built RRNode
|
||||
*/
|
||||
export function buildFromNode(
|
||||
node: Node,
|
||||
rrdom: IRRDocument,
|
||||
domMirror: NodeMirror,
|
||||
parentRRNode?: IRRNode | null,
|
||||
): IRRNode | null {
|
||||
let rrNode: IRRNode;
|
||||
|
||||
switch (node.nodeType) {
|
||||
case NodeType.DOCUMENT_NODE:
|
||||
if (parentRRNode && parentRRNode.nodeName === 'IFRAME')
|
||||
rrNode = (parentRRNode as RRIFrameElement).contentDocument;
|
||||
else {
|
||||
rrNode = rrdom;
|
||||
(rrNode as IRRDocument).compatMode = (node as Document).compatMode as
|
||||
| 'BackCompat'
|
||||
| 'CSS1Compat';
|
||||
}
|
||||
break;
|
||||
case NodeType.DOCUMENT_TYPE_NODE:
|
||||
const documentType = (node as Node) as DocumentType;
|
||||
rrNode = rrdom.createDocumentType(
|
||||
documentType.name,
|
||||
documentType.publicId,
|
||||
documentType.systemId,
|
||||
);
|
||||
break;
|
||||
case NodeType.ELEMENT_NODE:
|
||||
const elementNode = (node as Node) as HTMLElement;
|
||||
const tagName = getValidTagName(elementNode);
|
||||
rrNode = rrdom.createElement(tagName);
|
||||
const rrElement = rrNode as IRRElement;
|
||||
for (const { name, value } of Array.from(elementNode.attributes)) {
|
||||
rrElement.attributes[name] = value;
|
||||
}
|
||||
elementNode.scrollLeft && (rrElement.scrollLeft = elementNode.scrollLeft);
|
||||
elementNode.scrollTop && (rrElement.scrollTop = elementNode.scrollTop);
|
||||
/**
|
||||
* We don't have to record special values of input elements at the beginning.
|
||||
* Because if these values are changed later, the mutation will be applied through the batched input events on its RRElement after the diff algorithm is executed.
|
||||
*/
|
||||
break;
|
||||
case NodeType.TEXT_NODE:
|
||||
rrNode = rrdom.createTextNode(((node as Node) as Text).textContent || '');
|
||||
break;
|
||||
case NodeType.CDATA_SECTION_NODE:
|
||||
rrNode = rrdom.createCDATASection(((node as Node) as CDATASection).data);
|
||||
break;
|
||||
case NodeType.COMMENT_NODE:
|
||||
rrNode = rrdom.createComment(
|
||||
((node as Node) as Comment).textContent || '',
|
||||
);
|
||||
break;
|
||||
// if node is a shadow root
|
||||
case NodeType.DOCUMENT_FRAGMENT_NODE:
|
||||
rrNode = (parentRRNode as IRRElement).attachShadow({ mode: 'open' });
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
let sn: serializedNodeWithId | null = domMirror.getMeta(node);
|
||||
|
||||
if (rrdom instanceof RRDocument) {
|
||||
if (!sn) {
|
||||
sn = getDefaultSN(rrNode, rrdom.unserializedId);
|
||||
domMirror.add(node, sn);
|
||||
}
|
||||
rrdom.mirror.add(rrNode, { ...sn });
|
||||
}
|
||||
|
||||
return rrNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a RRDocument from a real document tree.
|
||||
* @param dom the real document tree
|
||||
* @param domMirror the NodeMirror that records the real document tree
|
||||
* @param rrdom the rrdom object to be constructed
|
||||
* @returns the build rrdom
|
||||
*/
|
||||
export function buildFromDom(
|
||||
dom: Document,
|
||||
domMirror: NodeMirror = createNodeMirror(),
|
||||
rrdom: IRRDocument = new RRDocument(),
|
||||
) {
|
||||
function walk(node: Node, parentRRNode: IRRNode | null) {
|
||||
const rrNode = buildFromNode(node, rrdom, domMirror, parentRRNode);
|
||||
if (rrNode === null) return;
|
||||
if (
|
||||
// if the parentRRNode isn't a RRIFrameElement
|
||||
parentRRNode?.nodeName !== 'IFRAME' &&
|
||||
// if node isn't a shadow root
|
||||
node.nodeType !== NodeType.DOCUMENT_FRAGMENT_NODE
|
||||
) {
|
||||
parentRRNode?.appendChild(rrNode);
|
||||
rrNode.parentNode = parentRRNode;
|
||||
rrNode.parentElement = parentRRNode as RRElement;
|
||||
}
|
||||
|
||||
if (node.nodeName === 'IFRAME') {
|
||||
walk((node as HTMLIFrameElement).contentDocument!, rrNode);
|
||||
} else if (
|
||||
node.nodeType === NodeType.DOCUMENT_NODE ||
|
||||
node.nodeType === NodeType.ELEMENT_NODE ||
|
||||
node.nodeType === NodeType.DOCUMENT_FRAGMENT_NODE
|
||||
) {
|
||||
// if the node is a shadow dom
|
||||
if (
|
||||
node.nodeType === NodeType.ELEMENT_NODE &&
|
||||
((node as Node) as HTMLElement).shadowRoot
|
||||
)
|
||||
walk(((node as Node) as HTMLElement).shadowRoot!, rrNode);
|
||||
node.childNodes.forEach((childNode) => walk(childNode, rrNode));
|
||||
}
|
||||
}
|
||||
walk(dom, null);
|
||||
return rrdom;
|
||||
}
|
||||
|
||||
export function createMirror(): Mirror {
|
||||
return new Mirror();
|
||||
}
|
||||
|
||||
// based on Mirror from rrweb-snapshots
|
||||
export class Mirror implements IMirror<RRNode> {
|
||||
private idNodeMap: Map<number, RRNode> = new Map();
|
||||
private nodeMetaMap: WeakMap<RRNode, serializedNodeWithId> = new WeakMap();
|
||||
|
||||
getId(n: RRNode | undefined | null): number {
|
||||
if (!n) return -1;
|
||||
|
||||
const id = this.getMeta(n)?.id;
|
||||
|
||||
// if n is not a serialized Node, use -1 as its id.
|
||||
return id ?? -1;
|
||||
}
|
||||
|
||||
getNode(id: number): RRNode | null {
|
||||
return this.idNodeMap.get(id) || null;
|
||||
}
|
||||
|
||||
getIds(): number[] {
|
||||
return Array.from(this.idNodeMap.keys());
|
||||
}
|
||||
|
||||
getMeta(n: RRNode): serializedNodeWithId | null {
|
||||
return this.nodeMetaMap.get(n) || null;
|
||||
}
|
||||
|
||||
// removes the node from idNodeMap
|
||||
// doesn't remove the node from nodeMetaMap
|
||||
removeNodeFromMap(n: RRNode) {
|
||||
const id = this.getId(n);
|
||||
this.idNodeMap.delete(id);
|
||||
|
||||
if (n.childNodes) {
|
||||
n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode));
|
||||
}
|
||||
}
|
||||
has(id: number): boolean {
|
||||
return this.idNodeMap.has(id);
|
||||
}
|
||||
|
||||
hasNode(node: RRNode): boolean {
|
||||
return this.nodeMetaMap.has(node);
|
||||
}
|
||||
|
||||
add(n: RRNode, meta: serializedNodeWithId) {
|
||||
const id = meta.id;
|
||||
this.idNodeMap.set(id, n);
|
||||
this.nodeMetaMap.set(n, meta);
|
||||
}
|
||||
|
||||
replace(id: number, n: RRNode) {
|
||||
this.idNodeMap.set(id, n);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.idNodeMap = new Map();
|
||||
this.nodeMetaMap = new WeakMap();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a default serializedNodeWithId value for a RRNode.
|
||||
* @param id the serialized id to assign
|
||||
*/
|
||||
export function getDefaultSN(node: IRRNode, id: number): serializedNodeWithId {
|
||||
switch (node.RRNodeType) {
|
||||
case RRNodeType.Document:
|
||||
return {
|
||||
id,
|
||||
type: node.RRNodeType,
|
||||
childNodes: [],
|
||||
};
|
||||
case RRNodeType.DocumentType:
|
||||
const doctype = node as IRRDocumentType;
|
||||
return {
|
||||
id,
|
||||
type: node.RRNodeType,
|
||||
name: doctype.name,
|
||||
publicId: doctype.publicId,
|
||||
systemId: doctype.systemId,
|
||||
};
|
||||
case RRNodeType.Element:
|
||||
return {
|
||||
id,
|
||||
type: node.RRNodeType,
|
||||
tagName: (node as IRRElement).tagName.toLowerCase(), // In rrweb data, all tagNames are lowercase.
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
};
|
||||
case RRNodeType.Text:
|
||||
return {
|
||||
id,
|
||||
type: node.RRNodeType,
|
||||
textContent: (node as IRRText).textContent || '',
|
||||
};
|
||||
case RRNodeType.Comment:
|
||||
return {
|
||||
id,
|
||||
type: node.RRNodeType,
|
||||
textContent: (node as IRRComment).textContent || '',
|
||||
};
|
||||
case RRNodeType.CDATA:
|
||||
return {
|
||||
id,
|
||||
type: node.RRNodeType,
|
||||
textContent: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { RRNode };
|
||||
export {
|
||||
diff,
|
||||
createOrGetNode,
|
||||
StyleRuleType,
|
||||
VirtualStyleRules,
|
||||
ReplayerHandler,
|
||||
} from './diff';
|
||||
@@ -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');
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "ES6",
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
@@ -11,9 +11,10 @@
|
||||
"outDir": "build",
|
||||
"lib": ["es6", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"importsNotUsedAsValues": "error"
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"exclude": ["test"],
|
||||
"include": ["src", "test.d.ts"]
|
||||
"include": ["src", "test.d.ts", "../rrweb/src/record/workers/workers.d.ts"]
|
||||
}
|
||||
|
||||
@@ -2,26 +2,28 @@
|
||||
"name": "rrweb-player",
|
||||
"version": "0.7.14",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^21.0.2",
|
||||
"@rollup/plugin-node-resolve": "^7.0.0",
|
||||
"@rollup/plugin-typescript": "^4.0.0",
|
||||
"@rollup/plugin-commonjs": "^22.0.0",
|
||||
"@rollup/plugin-node-resolve": "^13.2.1",
|
||||
"@rollup/plugin-typescript": "^8.3.2",
|
||||
"@types/offscreencanvas": "^2019.6.4",
|
||||
"@typescript-eslint/eslint-plugin": "^3.7.0",
|
||||
"@typescript-eslint/parser": "^3.7.0",
|
||||
"eslint": "^7.5.0",
|
||||
"eslint-config-google": "^0.11.0",
|
||||
"eslint-plugin-svelte3": "^2.7.3",
|
||||
"postcss-easy-import": "^3.0.0",
|
||||
"rollup": "^2.45.2",
|
||||
"rollup": "^2.71.1",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^7.1.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"sirv-cli": "^0.4.4",
|
||||
"svelte": "^3.2.0",
|
||||
"svelte-check": "^1.4.0",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.7"
|
||||
"typescript": "^4.6.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tsconfig/svelte": "^1.0.0",
|
||||
|
||||
@@ -4,6 +4,7 @@ import commonjs from '@rollup/plugin-commonjs';
|
||||
import livereload from 'rollup-plugin-livereload';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import sveltePreprocess from 'svelte-preprocess';
|
||||
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import pkg from './package.json';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
@@ -64,8 +65,12 @@ export default entries.map((output) => ({
|
||||
browser: true,
|
||||
dedupe: ['svelte'],
|
||||
}),
|
||||
|
||||
commonjs(),
|
||||
|
||||
// supports bundling `web-worker:..filename` from rrweb
|
||||
webWorkerLoader(),
|
||||
|
||||
typescript(),
|
||||
|
||||
css({
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"],
|
||||
}
|
||||
"exclude": [
|
||||
"node_modules/*",
|
||||
"__sapper__/*",
|
||||
"public/*",
|
||||
"../rrweb/src/record/workers/workers.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ export function buildNodeWithSN(
|
||||
if (n.rootId) {
|
||||
console.assert(
|
||||
(mirror.getNode(n.rootId) as Document) === doc,
|
||||
'Target document should has the same root id.',
|
||||
'Target document should have the same root id.',
|
||||
);
|
||||
}
|
||||
// use target document as root document
|
||||
|
||||
@@ -76,6 +76,28 @@ export interface ICanvas extends HTMLCanvasElement {
|
||||
__context: string;
|
||||
}
|
||||
|
||||
export interface IMirror<TNode> {
|
||||
getId(n: TNode | undefined | null): number;
|
||||
|
||||
getNode(id: number): TNode | null;
|
||||
|
||||
getIds(): number[];
|
||||
|
||||
getMeta(n: TNode): serializedNodeWithId | null;
|
||||
|
||||
removeNodeFromMap(n: TNode): void;
|
||||
|
||||
has(id: number): boolean;
|
||||
|
||||
hasNode(node: TNode): boolean;
|
||||
|
||||
add(n: TNode, meta: serializedNodeWithId): void;
|
||||
|
||||
replace(id: number, n: TNode): void;
|
||||
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export type idNodeMap = Map<number, Node>;
|
||||
|
||||
export type nodeMetaMap = WeakMap<Node, serializedNodeWithId>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
MaskInputFn,
|
||||
MaskInputOptions,
|
||||
nodeMetaMap,
|
||||
IMirror,
|
||||
serializedNodeWithId,
|
||||
} from './types';
|
||||
|
||||
@@ -15,7 +16,7 @@ export function isShadowRoot(n: Node): n is ShadowRoot {
|
||||
return Boolean(host && host.shadowRoot && host.shadowRoot === n);
|
||||
}
|
||||
|
||||
export class Mirror {
|
||||
export class Mirror implements IMirror<Node> {
|
||||
private idNodeMap: idNodeMap = new Map();
|
||||
private nodeMetaMap: nodeMetaMap = new WeakMap();
|
||||
|
||||
@@ -47,7 +48,9 @@ export class Mirror {
|
||||
this.idNodeMap.delete(id);
|
||||
|
||||
if (n.childNodes) {
|
||||
n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode));
|
||||
n.childNodes.forEach((childNode) =>
|
||||
this.removeNodeFromMap((childNode as unknown) as Node),
|
||||
);
|
||||
}
|
||||
}
|
||||
has(id: number): boolean {
|
||||
|
||||
12
packages/rrweb-snapshot/typings/types.d.ts
vendored
12
packages/rrweb-snapshot/typings/types.d.ts
vendored
@@ -58,6 +58,18 @@ export interface INode extends Node {
|
||||
export interface ICanvas extends HTMLCanvasElement {
|
||||
__context: string;
|
||||
}
|
||||
export interface IMirror<TNode> {
|
||||
getId(n: TNode | undefined | null): number;
|
||||
getNode(id: number): TNode | null;
|
||||
getIds(): number[];
|
||||
getMeta(n: TNode): serializedNodeWithId | null;
|
||||
removeNodeFromMap(n: TNode): void;
|
||||
has(id: number): boolean;
|
||||
hasNode(node: TNode): boolean;
|
||||
add(n: TNode, meta: serializedNodeWithId): void;
|
||||
replace(id: number, n: TNode): void;
|
||||
reset(): void;
|
||||
}
|
||||
export declare type idNodeMap = Map<number, Node>;
|
||||
export declare type nodeMetaMap = WeakMap<Node, serializedNodeWithId>;
|
||||
export declare type MaskInputOptions = Partial<{
|
||||
|
||||
4
packages/rrweb-snapshot/typings/utils.d.ts
vendored
4
packages/rrweb-snapshot/typings/utils.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
import { MaskInputFn, MaskInputOptions, serializedNodeWithId } from './types';
|
||||
import { MaskInputFn, MaskInputOptions, IMirror, serializedNodeWithId } from './types';
|
||||
export declare function isElement(n: Node): n is Element;
|
||||
export declare function isShadowRoot(n: Node): n is ShadowRoot;
|
||||
export declare class Mirror {
|
||||
export declare class Mirror implements IMirror<Node> {
|
||||
private idNodeMap;
|
||||
private nodeMetaMap;
|
||||
getId(n: Node | undefined | null): number;
|
||||
|
||||
@@ -5,5 +5,6 @@ module.exports = {
|
||||
testMatch: ['**/**.test.ts'],
|
||||
moduleNameMapper: {
|
||||
'\\.css$': 'identity-obj-proxy',
|
||||
'rrdom/es/(.*)': 'rrdom/lib/$1',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,12 +46,12 @@
|
||||
"@types/inquirer": "0.0.43",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/jest-image-snapshot": "^4.3.1",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/offscreencanvas": "^2019.6.4",
|
||||
"@types/prettier": "^2.3.2",
|
||||
"@types/puppeteer": "^5.4.4",
|
||||
"cross-env": "^5.2.0",
|
||||
"esbuild": "^0.14.38",
|
||||
"fast-mhtml": "^1.1.9",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"ignore-styles": "^5.0.1",
|
||||
@@ -59,14 +59,12 @@
|
||||
"jest": "^27.5.1",
|
||||
"jest-image-snapshot": "^4.5.1",
|
||||
"jest-snapshot": "^23.6.0",
|
||||
"jsdom": "^17.0.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"prettier": "2.2.1",
|
||||
"puppeteer": "^9.1.1",
|
||||
"rollup": "^2.68.0",
|
||||
"rollup-plugin-esbuild": "^4.9.1",
|
||||
"rollup-plugin-postcss": "^3.1.1",
|
||||
"rollup-plugin-rename-node-modules": "^1.3.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.31.2",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"ts-jest": "^27.1.3",
|
||||
@@ -81,6 +79,7 @@
|
||||
"base64-arraybuffer": "^1.0.1",
|
||||
"fflate": "^0.4.4",
|
||||
"mitt": "^1.1.3",
|
||||
"rrdom": "^0.1.2",
|
||||
"rrweb-snapshot": "^1.1.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import postcss from 'rollup-plugin-postcss';
|
||||
import renameNodeModules from 'rollup-plugin-rename-node-modules';
|
||||
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
|
||||
@@ -108,10 +108,34 @@ const baseConfigs = [
|
||||
|
||||
let configs = [];
|
||||
|
||||
function getPlugins(options = {}) {
|
||||
const { minify = false, sourceMap = false } = options;
|
||||
return [
|
||||
resolve({ browser: true }),
|
||||
webWorkerLoader({
|
||||
targetPlatform: 'browser',
|
||||
inline: true,
|
||||
sourceMap,
|
||||
}),
|
||||
esbuild({
|
||||
minify,
|
||||
}),
|
||||
postcss({
|
||||
extract: false,
|
||||
inject: false,
|
||||
minimize: minify,
|
||||
sourceMap,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
for (const c of baseConfigs) {
|
||||
const basePlugins = [
|
||||
resolve({ browser: true }),
|
||||
|
||||
// supports bundling `web-worker:..filename`
|
||||
webWorkerLoader(),
|
||||
|
||||
typescript(),
|
||||
];
|
||||
const plugins = basePlugins.concat(
|
||||
@@ -123,7 +147,7 @@ for (const c of baseConfigs) {
|
||||
// browser
|
||||
configs.push({
|
||||
input: c.input,
|
||||
plugins,
|
||||
plugins: getPlugins(),
|
||||
output: [
|
||||
{
|
||||
name: c.name,
|
||||
@@ -135,14 +159,7 @@ for (const c of baseConfigs) {
|
||||
// browser + minify
|
||||
configs.push({
|
||||
input: c.input,
|
||||
plugins: basePlugins.concat(
|
||||
postcss({
|
||||
extract: true,
|
||||
minimize: true,
|
||||
sourceMap: true,
|
||||
}),
|
||||
terser(),
|
||||
),
|
||||
plugins: getPlugins({ minify: true, sourceMap: true }),
|
||||
output: [
|
||||
{
|
||||
name: c.name,
|
||||
@@ -197,23 +214,9 @@ if (process.env.BROWSER_ONLY) {
|
||||
configs = [];
|
||||
|
||||
for (const c of browserOnlyBaseConfigs) {
|
||||
const plugins = [
|
||||
resolve({ browser: true }),
|
||||
webWorkerLoader(),
|
||||
typescript({
|
||||
outDir: null,
|
||||
}),
|
||||
postcss({
|
||||
extract: false,
|
||||
inject: false,
|
||||
sourceMap: true,
|
||||
}),
|
||||
terser(),
|
||||
];
|
||||
|
||||
configs.push({
|
||||
input: c.input,
|
||||
plugins,
|
||||
plugins: getPlugins({ sourceMap: true, minify: true }),
|
||||
output: [
|
||||
{
|
||||
name: c.name,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { eventWithTime } from '../types';
|
||||
import type { eventWithTime } from '../types';
|
||||
|
||||
export type PackFn = (event: eventWithTime) => string;
|
||||
export type UnpackFn = (raw: string) => eventWithTime;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { strFromU8, strToU8, unzlibSync } from 'fflate';
|
||||
import { UnpackFn, eventWithTimeAndPacker, MARK } from './base';
|
||||
import { eventWithTime } from '../types';
|
||||
import type { eventWithTime } from '../types';
|
||||
|
||||
export const unpack: UnpackFn = (raw: string) => {
|
||||
if (typeof raw !== 'string') {
|
||||
@@ -16,7 +16,7 @@ export const unpack: UnpackFn = (raw: string) => {
|
||||
}
|
||||
try {
|
||||
const e: eventWithTimeAndPacker = JSON.parse(
|
||||
strFromU8(unzlibSync(strToU8(raw, true)))
|
||||
strFromU8(unzlibSync(strToU8(raw, true))),
|
||||
);
|
||||
if (e.v === MARK) {
|
||||
return e;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { listenerHandler, RecordPlugin, IWindow } from '../../../types';
|
||||
import type { listenerHandler, RecordPlugin, IWindow } from '../../../types';
|
||||
import { patch } from '../../../utils';
|
||||
import { ErrorStackParser, StackFrame } from './error-stack-parser';
|
||||
import { stringify } from './stringify';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { StringifyOptions } from './index';
|
||||
import type { StringifyOptions } from './index';
|
||||
|
||||
/**
|
||||
* transfer the node path in Event to string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RecordPlugin } from '../../../types';
|
||||
import type { RecordPlugin } from '../../../types';
|
||||
|
||||
export type SequentialIdOptions = {
|
||||
key: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SequentialIdOptions } from '../record';
|
||||
import { ReplayPlugin, eventWithTime } from '../../../types';
|
||||
import type { ReplayPlugin, eventWithTime } from '../../../types';
|
||||
|
||||
type Options = SequentialIdOptions & {
|
||||
warnOnMissingId: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
||||
import { mutationCallBack } from '../types';
|
||||
import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
||||
import type { mutationCallBack } from '../types';
|
||||
|
||||
export class IframeManager {
|
||||
private iframes: WeakMap<HTMLIFrameElement, true> = new WeakMap();
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
maskInputValue,
|
||||
Mirror,
|
||||
} from 'rrweb-snapshot';
|
||||
import {
|
||||
import type {
|
||||
mutationRecord,
|
||||
textCursor,
|
||||
attributeCursor,
|
||||
@@ -298,7 +298,7 @@ export default class MutationBuffer {
|
||||
inlineImages: this.inlineImages,
|
||||
onSerialize: (currentN) => {
|
||||
if (isSerializedIframe(currentN, this.mirror)) {
|
||||
this.iframeManager.addIframe(currentN);
|
||||
this.iframeManager.addIframe(currentN as HTMLIFrameElement);
|
||||
}
|
||||
if (hasShadowRoot(n)) {
|
||||
this.shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
||||
@@ -322,7 +322,7 @@ export default class MutationBuffer {
|
||||
this.mirror.removeNodeFromMap(this.mapRemoves.shift()!);
|
||||
}
|
||||
|
||||
for (const n of this.movedSet) {
|
||||
for (const n of Array.from(this.movedSet.values())) {
|
||||
if (
|
||||
isParentRemoved(this.removes, n, this.mirror) &&
|
||||
!this.movedSet.has(n.parentNode!)
|
||||
@@ -332,7 +332,7 @@ export default class MutationBuffer {
|
||||
pushAdd(n);
|
||||
}
|
||||
|
||||
for (const n of this.addedSet) {
|
||||
for (const n of Array.from(this.addedSet.values())) {
|
||||
if (
|
||||
!isAncestorInSet(this.droppedSet, n) &&
|
||||
!isParentRemoved(this.removes, n, this.mirror)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MaskInputOptions, maskInputValue } from 'rrweb-snapshot';
|
||||
import { FontFaceSet } from 'css-font-loading-module';
|
||||
import type { FontFaceSet } from 'css-font-loading-module';
|
||||
import {
|
||||
throttle,
|
||||
on,
|
||||
@@ -108,9 +108,9 @@ export function initMutationObserver(
|
||||
typeof MutationObserver
|
||||
>)[angularZoneSymbol];
|
||||
}
|
||||
const observer = new mutationObserverCtor(
|
||||
mutationBuffer.processMutations.bind(mutationBuffer),
|
||||
);
|
||||
const observer = new (mutationObserverCtor as new (
|
||||
callback: MutationCallback,
|
||||
) => MutationObserver)(mutationBuffer.processMutations.bind(mutationBuffer));
|
||||
observer.observe(rootEl, {
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Mirror } from 'rrweb-snapshot';
|
||||
import type { Mirror } from 'rrweb-snapshot';
|
||||
import {
|
||||
blockClass,
|
||||
CanvasContext,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ICanvas, Mirror } from 'rrweb-snapshot';
|
||||
import {
|
||||
import type { ICanvas, Mirror } from 'rrweb-snapshot';
|
||||
import type {
|
||||
blockClass,
|
||||
CanvasContext,
|
||||
canvasManagerMutationCallback,
|
||||
canvasMutationCallback,
|
||||
canvasMutationCommand,
|
||||
@@ -10,11 +9,12 @@ import {
|
||||
listenerHandler,
|
||||
CanvasArg,
|
||||
} from '../../../types';
|
||||
import { CanvasContext } from '../../../types';
|
||||
import initCanvas2DMutationObserver from './2d';
|
||||
import initCanvasContextObserver from './canvas';
|
||||
import initCanvasWebGLMutationObserver from './webgl';
|
||||
import ImageBitmapDataURLWorker from 'web-worker:../../workers/image-bitmap-data-url-worker.ts';
|
||||
import { ImageBitmapDataURLRequestWorker } from '../../workers/image-bitmap-data-url-worker';
|
||||
import type { ImageBitmapDataURLRequestWorker } from '../../workers/image-bitmap-data-url-worker';
|
||||
|
||||
export type RafStamps = { latestId: number; invokeId: number | null };
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ICanvas } from 'rrweb-snapshot';
|
||||
import { blockClass, IWindow, listenerHandler } from '../../../types';
|
||||
import type { ICanvas } from 'rrweb-snapshot';
|
||||
import type { blockClass, IWindow, listenerHandler } from '../../../types';
|
||||
import { isBlocked, patch } from '../../../utils';
|
||||
|
||||
export default function initCanvasContextObserver(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { encode } from 'base64-arraybuffer';
|
||||
import { IWindow, CanvasArg } from '../../../types';
|
||||
import type { IWindow, CanvasArg } from '../../../types';
|
||||
|
||||
// TODO: unify with `replay/webgl.ts`
|
||||
type CanvasVarMap = Map<string, any[]>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Mirror } from 'rrweb-snapshot';
|
||||
import type { Mirror } from 'rrweb-snapshot';
|
||||
import {
|
||||
blockClass,
|
||||
CanvasContext,
|
||||
@@ -31,8 +31,8 @@ function patchGLPrototype(
|
||||
return function (this: typeof prototype, ...args: Array<unknown>) {
|
||||
const result = original.apply(this, args);
|
||||
saveWebGLVar(result, win, prototype);
|
||||
if (!isBlocked(this.canvas, blockClass)) {
|
||||
const id = mirror.getId(this.canvas);
|
||||
if (!isBlocked(this.canvas as HTMLCanvasElement, blockClass)) {
|
||||
const id = mirror.getId(this.canvas as HTMLCanvasElement);
|
||||
|
||||
const recordArgs = serializeArgs([...args], win, prototype);
|
||||
const mutation: canvasMutationWithType = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {
|
||||
import type {
|
||||
mutationCallBack,
|
||||
scrollCallback,
|
||||
MutationBufferParam,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '../types';
|
||||
import { initMutationObserver, initScrollObserver } from './observer';
|
||||
import { patch } from '../utils';
|
||||
import { Mirror } from 'rrweb-snapshot';
|
||||
import type { Mirror } from 'rrweb-snapshot';
|
||||
|
||||
type BypassOptions = Omit<
|
||||
MutationBufferParam,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { encode } from 'base64-arraybuffer';
|
||||
import {
|
||||
import type {
|
||||
ImageBitmapDataURLWorkerParams,
|
||||
ImageBitmapDataURLWorkerResponse,
|
||||
} from '../../types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Replayer } from '../';
|
||||
import { canvasMutationCommand } from '../../types';
|
||||
import type { Replayer } from '../';
|
||||
import type { canvasMutationCommand } from '../../types';
|
||||
import { deserializeArg } from './deserialize-args';
|
||||
|
||||
export default async function canvasMutation({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { decode } from 'base64-arraybuffer';
|
||||
import type { Replayer } from '../';
|
||||
import { CanvasArg, SerializedCanvasArg } from '../../types';
|
||||
import type { CanvasArg, SerializedCanvasArg } from '../../types';
|
||||
|
||||
// TODO: add ability to wipe this list
|
||||
type GLVarMap = Map<string, any[]>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Replayer } from '..';
|
||||
import type { Replayer } from '..';
|
||||
import {
|
||||
CanvasContext,
|
||||
canvasMutationCommand,
|
||||
|
||||
@@ -11,9 +11,8 @@ function getContext(
|
||||
// you might have to do `ctx.flush()` before every webgl canvas event
|
||||
try {
|
||||
if (type === CanvasContext.WebGL) {
|
||||
return (
|
||||
target.getContext('webgl')! || target.getContext('experimental-webgl')
|
||||
);
|
||||
return (target.getContext('webgl')! ||
|
||||
target.getContext('experimental-webgl')) as WebGLRenderingContext;
|
||||
}
|
||||
return target.getContext('webgl2')!;
|
||||
} catch (e) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,186 +0,0 @@
|
||||
export enum StyleRuleType {
|
||||
Insert,
|
||||
Remove,
|
||||
Snapshot,
|
||||
SetProperty,
|
||||
RemoveProperty,
|
||||
}
|
||||
|
||||
type InsertRule = {
|
||||
cssText: string;
|
||||
type: StyleRuleType.Insert;
|
||||
index?: number | number[];
|
||||
};
|
||||
type RemoveRule = {
|
||||
type: StyleRuleType.Remove;
|
||||
index: number | number[];
|
||||
};
|
||||
type SnapshotRule = {
|
||||
type: StyleRuleType.Snapshot;
|
||||
cssTexts: string[];
|
||||
};
|
||||
type SetPropertyRule = {
|
||||
type: StyleRuleType.SetProperty;
|
||||
index: number[];
|
||||
property: string;
|
||||
value: string | null;
|
||||
priority: string | undefined;
|
||||
};
|
||||
type RemovePropertyRule = {
|
||||
type: StyleRuleType.RemoveProperty;
|
||||
index: number[];
|
||||
property: string;
|
||||
};
|
||||
|
||||
export type VirtualStyleRules = Array<
|
||||
InsertRule | RemoveRule | SnapshotRule | SetPropertyRule | RemovePropertyRule
|
||||
>;
|
||||
export type VirtualStyleRulesMap = Map<Node, VirtualStyleRules>;
|
||||
|
||||
export function getNestedRule(
|
||||
rules: CSSRuleList,
|
||||
position: number[],
|
||||
): CSSGroupingRule {
|
||||
const rule = rules[position[0]] as CSSGroupingRule;
|
||||
if (position.length === 1) {
|
||||
return rule;
|
||||
} else {
|
||||
return getNestedRule(
|
||||
((rule as CSSGroupingRule).cssRules[position[1]] as CSSGroupingRule)
|
||||
.cssRules,
|
||||
position.slice(2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getPositionsAndIndex(nestedIndex: number[]) {
|
||||
const positions = [...nestedIndex];
|
||||
const index = positions.pop();
|
||||
return { positions, index };
|
||||
}
|
||||
|
||||
export function applyVirtualStyleRulesToNode(
|
||||
storedRules: VirtualStyleRules,
|
||||
styleNode: HTMLStyleElement,
|
||||
) {
|
||||
const { sheet } = styleNode;
|
||||
if (!sheet) {
|
||||
// styleNode without sheet means the DOM has been removed
|
||||
// so the rules no longer need to be applied
|
||||
return;
|
||||
}
|
||||
|
||||
storedRules.forEach((rule) => {
|
||||
if (rule.type === StyleRuleType.Insert) {
|
||||
try {
|
||||
if (Array.isArray(rule.index)) {
|
||||
const { positions, index } = getPositionsAndIndex(rule.index);
|
||||
const nestedRule = getNestedRule(sheet.cssRules, positions);
|
||||
nestedRule.insertRule(rule.cssText, index);
|
||||
} else {
|
||||
sheet.insertRule(rule.cssText, rule.index);
|
||||
}
|
||||
} catch (e) {
|
||||
/**
|
||||
* sometimes we may capture rules with browser prefix
|
||||
* insert rule with prefixs in other browsers may cause Error
|
||||
*/
|
||||
}
|
||||
} else if (rule.type === StyleRuleType.Remove) {
|
||||
try {
|
||||
if (Array.isArray(rule.index)) {
|
||||
const { positions, index } = getPositionsAndIndex(rule.index);
|
||||
const nestedRule = getNestedRule(sheet.cssRules, positions);
|
||||
nestedRule.deleteRule(index || 0);
|
||||
} else {
|
||||
sheet.deleteRule(rule.index);
|
||||
}
|
||||
} catch (e) {
|
||||
/**
|
||||
* accessing styleSheet rules may cause SecurityError
|
||||
* for specific access control settings
|
||||
*/
|
||||
}
|
||||
} else if (rule.type === StyleRuleType.Snapshot) {
|
||||
restoreSnapshotOfStyleRulesToNode(rule.cssTexts, styleNode);
|
||||
} else if (rule.type === StyleRuleType.SetProperty) {
|
||||
const nativeRule = (getNestedRule(
|
||||
sheet.cssRules,
|
||||
rule.index,
|
||||
) as unknown) as CSSStyleRule;
|
||||
nativeRule.style.setProperty(rule.property, rule.value, rule.priority);
|
||||
} else if (rule.type === StyleRuleType.RemoveProperty) {
|
||||
const nativeRule = (getNestedRule(
|
||||
sheet.cssRules,
|
||||
rule.index,
|
||||
) as unknown) as CSSStyleRule;
|
||||
nativeRule.style.removeProperty(rule.property);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function restoreSnapshotOfStyleRulesToNode(
|
||||
cssTexts: string[],
|
||||
styleNode: HTMLStyleElement,
|
||||
) {
|
||||
try {
|
||||
const existingRules = Array.from(styleNode.sheet?.cssRules || []).map(
|
||||
(rule) => rule.cssText,
|
||||
);
|
||||
const existingRulesReversed = Object.entries(existingRules).reverse();
|
||||
let lastMatch = existingRules.length;
|
||||
existingRulesReversed.forEach(([index, rule]) => {
|
||||
const indexOf = cssTexts.indexOf(rule);
|
||||
if (indexOf === -1 || indexOf > lastMatch) {
|
||||
try {
|
||||
styleNode.sheet?.deleteRule(Number(index));
|
||||
} catch (e) {
|
||||
/**
|
||||
* accessing styleSheet rules may cause SecurityError
|
||||
* for specific access control settings
|
||||
*/
|
||||
}
|
||||
}
|
||||
lastMatch = indexOf;
|
||||
});
|
||||
cssTexts.forEach((cssText, index) => {
|
||||
try {
|
||||
if (styleNode.sheet?.cssRules[index]?.cssText !== cssText) {
|
||||
styleNode.sheet?.insertRule(cssText, index);
|
||||
}
|
||||
} catch (e) {
|
||||
/**
|
||||
* sometimes we may capture rules with browser prefix
|
||||
* insert rule with prefixs in other browsers may cause Error
|
||||
*/
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
/**
|
||||
* accessing styleSheet rules may cause SecurityError
|
||||
* for specific access control settings
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
export function storeCSSRules(
|
||||
parentElement: HTMLStyleElement,
|
||||
virtualStyleRulesMap: VirtualStyleRulesMap,
|
||||
) {
|
||||
try {
|
||||
const cssTexts = Array.from(
|
||||
(parentElement as HTMLStyleElement).sheet?.cssRules || [],
|
||||
).map((rule) => rule.cssText);
|
||||
virtualStyleRulesMap.set(parentElement, [
|
||||
{
|
||||
type: StyleRuleType.Snapshot,
|
||||
cssTexts,
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
/**
|
||||
* accessing styleSheet rules may cause SecurityError
|
||||
* for specific access control settings
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {
|
||||
import type {
|
||||
serializedNodeWithId,
|
||||
Mirror,
|
||||
INode,
|
||||
@@ -7,11 +7,12 @@ import {
|
||||
MaskInputFn,
|
||||
MaskTextFn,
|
||||
} from 'rrweb-snapshot';
|
||||
import { PackFn, UnpackFn } from './packer/base';
|
||||
import { IframeManager } from './record/iframe-manager';
|
||||
import { ShadowDomManager } from './record/shadow-dom-manager';
|
||||
import type { PackFn, UnpackFn } from './packer/base';
|
||||
import type { IframeManager } from './record/iframe-manager';
|
||||
import type { ShadowDomManager } from './record/shadow-dom-manager';
|
||||
import type { Replayer } from './replay';
|
||||
import { CanvasManager } from './record/observers/canvas/canvas-manager';
|
||||
import type { RRNode } from 'rrdom/es/virtual-dom';
|
||||
import type { CanvasManager } from './record/observers/canvas/canvas-manager';
|
||||
|
||||
export enum EventType {
|
||||
DomContentLoaded,
|
||||
@@ -169,6 +170,11 @@ export type eventWithTime = event & {
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
export type canvasEventWithTime = eventWithTime & {
|
||||
type: EventType.IncrementalSnapshot;
|
||||
data: canvasMutationData;
|
||||
};
|
||||
|
||||
export type blockClass = string | RegExp;
|
||||
|
||||
export type maskTextClass = string | RegExp;
|
||||
@@ -653,6 +659,7 @@ export type playerConfig = {
|
||||
strokeStyle?: string;
|
||||
};
|
||||
unpackFn?: UnpackFn;
|
||||
useVirtualDom: boolean;
|
||||
plugins?: ReplayPlugin[];
|
||||
};
|
||||
|
||||
@@ -663,7 +670,7 @@ export type playerMetaData = {
|
||||
};
|
||||
|
||||
export type missingNode = {
|
||||
node: Node;
|
||||
node: Node | RRNode;
|
||||
mutation: addedNodeMutation;
|
||||
};
|
||||
export type missingNodeMap = {
|
||||
@@ -706,12 +713,6 @@ export enum ReplayerEvents {
|
||||
PlayBack = 'play-back',
|
||||
}
|
||||
|
||||
// store the state that would be changed during the process(unmount from dom and mount again)
|
||||
export type ElementState = {
|
||||
// [scrollLeft,scrollTop]
|
||||
scroll?: [number, number];
|
||||
};
|
||||
|
||||
export type KeepIframeSrcFn = (src: string) => boolean;
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import {
|
||||
import type {
|
||||
throttleOptions,
|
||||
listenerHandler,
|
||||
hookResetter,
|
||||
blockClass,
|
||||
IncrementalSource,
|
||||
addedNodeMutation,
|
||||
removedNodeMutation,
|
||||
textMutation,
|
||||
attributeMutation,
|
||||
mutationData,
|
||||
scrollData,
|
||||
inputData,
|
||||
DocumentDimension,
|
||||
IWindow,
|
||||
DeprecatedMirror,
|
||||
textMutation,
|
||||
} from './types';
|
||||
import { Mirror, IGNORED_NODE, isShadowRoot } from 'rrweb-snapshot';
|
||||
import type { IMirror, Mirror } from 'rrweb-snapshot';
|
||||
import { isShadowRoot, IGNORED_NODE } from 'rrweb-snapshot';
|
||||
import type { RRNode, RRIFrameElement } from 'rrdom/es/virtual-dom';
|
||||
|
||||
export function on(
|
||||
type: string,
|
||||
@@ -280,201 +276,6 @@ export function polyfill(win = window) {
|
||||
}
|
||||
}
|
||||
|
||||
export type TreeNode = {
|
||||
id: number;
|
||||
mutation: addedNodeMutation;
|
||||
parent?: TreeNode;
|
||||
children: Record<number, TreeNode>;
|
||||
texts: textMutation[];
|
||||
attributes: attributeMutation[];
|
||||
};
|
||||
|
||||
export class TreeIndex {
|
||||
public tree!: Record<number, TreeNode>;
|
||||
|
||||
private removeNodeMutations!: removedNodeMutation[];
|
||||
private textMutations!: textMutation[];
|
||||
private attributeMutations!: attributeMutation[];
|
||||
private indexes!: Map<number, TreeNode>;
|
||||
private removeIdSet!: Set<number>;
|
||||
private scrollMap!: Map<number, scrollData>;
|
||||
private inputMap!: Map<number, inputData>;
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public add(mutation: addedNodeMutation) {
|
||||
const parentTreeNode = this.indexes.get(mutation.parentId);
|
||||
const treeNode: TreeNode = {
|
||||
id: mutation.node.id,
|
||||
mutation,
|
||||
children: [],
|
||||
texts: [],
|
||||
attributes: [],
|
||||
};
|
||||
if (!parentTreeNode) {
|
||||
this.tree[treeNode.id] = treeNode;
|
||||
} else {
|
||||
treeNode.parent = parentTreeNode;
|
||||
parentTreeNode.children[treeNode.id] = treeNode;
|
||||
}
|
||||
this.indexes.set(treeNode.id, treeNode);
|
||||
}
|
||||
|
||||
public remove(mutation: removedNodeMutation, mirror: Mirror) {
|
||||
const parentTreeNode = this.indexes.get(mutation.parentId);
|
||||
const treeNode = this.indexes.get(mutation.id);
|
||||
|
||||
const deepRemoveFromMirror = (id: number) => {
|
||||
if (id === -1) return;
|
||||
|
||||
this.removeIdSet.add(id);
|
||||
const node = mirror.getNode(id);
|
||||
node?.childNodes.forEach((childNode) => {
|
||||
deepRemoveFromMirror(mirror.getId(childNode));
|
||||
});
|
||||
};
|
||||
const deepRemoveFromTreeIndex = (node: TreeNode) => {
|
||||
this.removeIdSet.add(node.id);
|
||||
Object.values(node.children).forEach((n) => deepRemoveFromTreeIndex(n));
|
||||
const _treeNode = this.indexes.get(node.id);
|
||||
if (_treeNode) {
|
||||
const _parentTreeNode = _treeNode.parent;
|
||||
if (_parentTreeNode) {
|
||||
delete _treeNode.parent;
|
||||
delete _parentTreeNode.children[_treeNode.id];
|
||||
this.indexes.delete(mutation.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!treeNode) {
|
||||
this.removeNodeMutations.push(mutation);
|
||||
deepRemoveFromMirror(mutation.id);
|
||||
} else if (!parentTreeNode) {
|
||||
delete this.tree[treeNode.id];
|
||||
this.indexes.delete(treeNode.id);
|
||||
deepRemoveFromTreeIndex(treeNode);
|
||||
} else {
|
||||
delete treeNode.parent;
|
||||
delete parentTreeNode.children[treeNode.id];
|
||||
this.indexes.delete(mutation.id);
|
||||
deepRemoveFromTreeIndex(treeNode);
|
||||
}
|
||||
}
|
||||
|
||||
public text(mutation: textMutation) {
|
||||
const treeNode = this.indexes.get(mutation.id);
|
||||
if (treeNode) {
|
||||
treeNode.texts.push(mutation);
|
||||
} else {
|
||||
this.textMutations.push(mutation);
|
||||
}
|
||||
}
|
||||
|
||||
public attribute(mutation: attributeMutation) {
|
||||
const treeNode = this.indexes.get(mutation.id);
|
||||
if (treeNode) {
|
||||
treeNode.attributes.push(mutation);
|
||||
} else {
|
||||
this.attributeMutations.push(mutation);
|
||||
}
|
||||
}
|
||||
|
||||
public scroll(d: scrollData) {
|
||||
this.scrollMap.set(d.id, d);
|
||||
}
|
||||
|
||||
public input(d: inputData) {
|
||||
this.inputMap.set(d.id, d);
|
||||
}
|
||||
|
||||
public flush(): {
|
||||
mutationData: mutationData;
|
||||
scrollMap: TreeIndex['scrollMap'];
|
||||
inputMap: TreeIndex['inputMap'];
|
||||
} {
|
||||
const {
|
||||
tree,
|
||||
removeNodeMutations,
|
||||
textMutations,
|
||||
attributeMutations,
|
||||
} = this;
|
||||
|
||||
const batchMutationData: mutationData = {
|
||||
source: IncrementalSource.Mutation,
|
||||
removes: removeNodeMutations,
|
||||
texts: textMutations,
|
||||
attributes: attributeMutations,
|
||||
adds: [],
|
||||
};
|
||||
|
||||
const walk = (treeNode: TreeNode, removed: boolean) => {
|
||||
if (removed) {
|
||||
this.removeIdSet.add(treeNode.id);
|
||||
}
|
||||
batchMutationData.texts = batchMutationData.texts
|
||||
.concat(removed ? [] : treeNode.texts)
|
||||
.filter((m) => !this.removeIdSet.has(m.id));
|
||||
batchMutationData.attributes = batchMutationData.attributes
|
||||
.concat(removed ? [] : treeNode.attributes)
|
||||
.filter((m) => !this.removeIdSet.has(m.id));
|
||||
if (
|
||||
!this.removeIdSet.has(treeNode.id) &&
|
||||
!this.removeIdSet.has(treeNode.mutation.parentId) &&
|
||||
!removed
|
||||
) {
|
||||
batchMutationData.adds.push(treeNode.mutation);
|
||||
if (treeNode.children) {
|
||||
Object.values(treeNode.children).forEach((n) => walk(n, false));
|
||||
}
|
||||
} else {
|
||||
Object.values(treeNode.children).forEach((n) => walk(n, true));
|
||||
}
|
||||
};
|
||||
|
||||
Object.values(tree).forEach((n) => walk(n, false));
|
||||
|
||||
for (const id of this.scrollMap.keys()) {
|
||||
if (this.removeIdSet.has(id)) {
|
||||
this.scrollMap.delete(id);
|
||||
}
|
||||
}
|
||||
for (const id of this.inputMap.keys()) {
|
||||
if (this.removeIdSet.has(id)) {
|
||||
this.inputMap.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
const scrollMap = new Map(this.scrollMap);
|
||||
const inputMap = new Map(this.inputMap);
|
||||
|
||||
this.reset();
|
||||
|
||||
return {
|
||||
mutationData: batchMutationData,
|
||||
scrollMap,
|
||||
inputMap,
|
||||
};
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.tree = [];
|
||||
this.indexes = new Map();
|
||||
this.removeNodeMutations = [];
|
||||
this.textMutations = [];
|
||||
this.attributeMutations = [];
|
||||
this.removeIdSet = new Set();
|
||||
this.scrollMap = new Map();
|
||||
this.inputMap = new Map();
|
||||
}
|
||||
|
||||
public idRemoved(id: number): boolean {
|
||||
return this.removeIdSet.has(id);
|
||||
}
|
||||
}
|
||||
|
||||
type ResolveTree = {
|
||||
value: addedNodeMutation;
|
||||
children: ResolveTree[];
|
||||
@@ -542,13 +343,13 @@ export function iterateResolveTree(
|
||||
|
||||
export type AppendedIframe = {
|
||||
mutationInQueue: addedNodeMutation;
|
||||
builtNode: HTMLIFrameElement;
|
||||
builtNode: HTMLIFrameElement | RRIFrameElement;
|
||||
};
|
||||
|
||||
export function isSerializedIframe(
|
||||
n: Node,
|
||||
mirror: Mirror,
|
||||
): n is HTMLIFrameElement {
|
||||
export function isSerializedIframe<TNode extends Node | RRNode>(
|
||||
n: TNode,
|
||||
mirror: IMirror<TNode>,
|
||||
): boolean {
|
||||
return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n));
|
||||
}
|
||||
|
||||
@@ -582,12 +383,34 @@ export function getBaseDimension(
|
||||
};
|
||||
}
|
||||
|
||||
export function hasShadowRoot<T extends Node>(
|
||||
export function hasShadowRoot<T extends Node | RRNode>(
|
||||
n: T,
|
||||
): n is T & { shadowRoot: ShadowRoot } {
|
||||
return Boolean(((n as unknown) as Element)?.shadowRoot);
|
||||
}
|
||||
|
||||
export function getNestedRule(
|
||||
rules: CSSRuleList,
|
||||
position: number[],
|
||||
): CSSGroupingRule {
|
||||
const rule = rules[position[0]] as CSSGroupingRule;
|
||||
if (position.length === 1) {
|
||||
return rule;
|
||||
} else {
|
||||
return getNestedRule(
|
||||
((rule as CSSGroupingRule).cssRules[position[1]] as CSSGroupingRule)
|
||||
.cssRules,
|
||||
position.slice(2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getPositionsAndIndex(nestedIndex: number[]) {
|
||||
const positions = [...nestedIndex];
|
||||
const index = positions.pop();
|
||||
return { positions, index };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest mutation in the queue for each node.
|
||||
* @param {textMutation[]} mutations The text mutations to filter.
|
||||
|
||||
@@ -8362,7 +8362,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"assert\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:2:37\\"
|
||||
\\"__puppeteer_evaluation_script__:2:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"true\\",
|
||||
@@ -8378,7 +8378,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"count\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:3:37\\"
|
||||
\\"__puppeteer_evaluation_script__:3:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"count\\\\\\"\\"
|
||||
@@ -8393,7 +8393,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"countReset\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:4:37\\"
|
||||
\\"__puppeteer_evaluation_script__:4:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"count\\\\\\"\\"
|
||||
@@ -8408,7 +8408,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"debug\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:5:37\\"
|
||||
\\"__puppeteer_evaluation_script__:5:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"debug\\\\\\"\\"
|
||||
@@ -8423,7 +8423,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"dir\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:6:37\\"
|
||||
\\"__puppeteer_evaluation_script__:6:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"dir\\\\\\"\\"
|
||||
@@ -8438,7 +8438,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"dirxml\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:7:37\\"
|
||||
\\"__puppeteer_evaluation_script__:7:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"dirxml\\\\\\"\\"
|
||||
@@ -8453,7 +8453,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"group\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:8:37\\"
|
||||
\\"__puppeteer_evaluation_script__:8:21\\"
|
||||
],
|
||||
\\"payload\\": []
|
||||
}
|
||||
@@ -8466,7 +8466,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"groupCollapsed\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:9:37\\"
|
||||
\\"__puppeteer_evaluation_script__:9:21\\"
|
||||
],
|
||||
\\"payload\\": []
|
||||
}
|
||||
@@ -8479,7 +8479,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"info\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:10:37\\"
|
||||
\\"__puppeteer_evaluation_script__:10:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"info\\\\\\"\\"
|
||||
@@ -8494,7 +8494,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"log\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:11:37\\"
|
||||
\\"__puppeteer_evaluation_script__:11:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"log\\\\\\"\\"
|
||||
@@ -8509,7 +8509,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"table\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:12:37\\"
|
||||
\\"__puppeteer_evaluation_script__:12:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"table\\\\\\"\\"
|
||||
@@ -8524,7 +8524,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"time\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:13:37\\"
|
||||
\\"__puppeteer_evaluation_script__:13:21\\"
|
||||
],
|
||||
\\"payload\\": []
|
||||
}
|
||||
@@ -8537,7 +8537,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"timeEnd\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:14:37\\"
|
||||
\\"__puppeteer_evaluation_script__:14:21\\"
|
||||
],
|
||||
\\"payload\\": []
|
||||
}
|
||||
@@ -8550,7 +8550,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"timeLog\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:15:37\\"
|
||||
\\"__puppeteer_evaluation_script__:15:21\\"
|
||||
],
|
||||
\\"payload\\": []
|
||||
}
|
||||
@@ -8563,7 +8563,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"trace\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:16:37\\"
|
||||
\\"__puppeteer_evaluation_script__:16:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"trace\\\\\\"\\"
|
||||
@@ -8578,7 +8578,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"warn\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:17:37\\"
|
||||
\\"__puppeteer_evaluation_script__:17:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"warn\\\\\\"\\"
|
||||
@@ -8593,7 +8593,7 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"clear\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:18:37\\"
|
||||
\\"__puppeteer_evaluation_script__:18:21\\"
|
||||
],
|
||||
\\"payload\\": []
|
||||
}
|
||||
@@ -8606,10 +8606,10 @@ exports[`record integration tests should record console messages 1`] = `
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"log\\",
|
||||
\\"trace\\": [
|
||||
\\"__puppeteer_evaluation_script__:19:37\\"
|
||||
\\"__puppeteer_evaluation_script__:19:21\\"
|
||||
],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"TypeError: a message\\\\\\\\n at __puppeteer_evaluation_script__:19:41\\\\\\\\nEnd of stack for Error object\\\\\\"\\"
|
||||
\\"\\\\\\"TypeError: a message\\\\\\\\n at __puppeteer_evaluation_script__:19:25\\\\\\\\nEnd of stack for Error object\\\\\\"\\"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ html.rrweb-paused *, html.rrweb-paused ::before, html.rrweb-paused ::after { ani
|
||||
file-cid-1
|
||||
@charset \\"utf-8\\";
|
||||
|
||||
.css-added-at-500 { padding: 1.3125rem; flex: 0 0 auto; width: 100%; }
|
||||
.css-added-at-400 { padding: 1.3125rem; flex: 0 0 auto; width: 100%; }
|
||||
|
||||
|
||||
file-cid-2
|
||||
@@ -64,7 +64,7 @@ file-cid-2
|
||||
|
||||
.css-added-at-200-overwritten-at-3000 { opacity: 1; transform: translateX(0px); }
|
||||
|
||||
.css-added-at-400-overwritten-at-3000 { border: 1px solid blue; }
|
||||
.css-added-at-500-overwritten-at-3000 { border: 1px solid blue; }
|
||||
|
||||
|
||||
file-cid-3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as http from 'http';
|
||||
import type * as http from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import type * as puppeteer from 'puppeteer';
|
||||
import {
|
||||
startServer,
|
||||
launchPuppeteer,
|
||||
@@ -9,12 +9,7 @@ import {
|
||||
replaceLast,
|
||||
waitForRAF,
|
||||
} from '../utils';
|
||||
import {
|
||||
recordOptions,
|
||||
eventWithTime,
|
||||
EventType,
|
||||
IncrementalSource,
|
||||
} from '../../src/types';
|
||||
import type { recordOptions, eventWithTime } from '../../src/types';
|
||||
import { toMatchImageSnapshot } from 'jest-image-snapshot';
|
||||
expect.extend({ toMatchImageSnapshot });
|
||||
|
||||
|
||||
@@ -486,6 +486,30 @@ const events: eventWithTime[] = [
|
||||
timestamp: now + 1500,
|
||||
},
|
||||
// add iframe five
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [],
|
||||
adds: [
|
||||
{
|
||||
parentId: 75,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'iframe',
|
||||
attributes: { id: 'five' },
|
||||
childNodes: [],
|
||||
rootId: 62,
|
||||
id: 80,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: now + 2000,
|
||||
},
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
@@ -550,30 +574,6 @@ const events: eventWithTime[] = [
|
||||
},
|
||||
timestamp: now + 2000,
|
||||
},
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [],
|
||||
adds: [
|
||||
{
|
||||
parentId: 75,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'iframe',
|
||||
attributes: { id: 'five' },
|
||||
childNodes: [],
|
||||
rootId: 62,
|
||||
id: 80,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: now + 2000,
|
||||
},
|
||||
// remove the html element of iframe four
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
|
||||
215
packages/rrweb/test/events/input.ts
Normal file
215
packages/rrweb/test/events/input.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { EventType, eventWithTime, IncrementalSource } from '../../src/types';
|
||||
|
||||
const now = Date.now();
|
||||
const events: eventWithTime[] = [
|
||||
{
|
||||
type: EventType.DomContentLoaded,
|
||||
data: {},
|
||||
timestamp: now,
|
||||
},
|
||||
{
|
||||
type: EventType.Load,
|
||||
data: {},
|
||||
timestamp: now + 100,
|
||||
},
|
||||
{
|
||||
type: EventType.Meta,
|
||||
data: {
|
||||
href: 'http://localhost',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
},
|
||||
timestamp: now + 100,
|
||||
},
|
||||
// full snapshot:
|
||||
{
|
||||
data: {
|
||||
node: {
|
||||
id: 1,
|
||||
type: 0,
|
||||
childNodes: [
|
||||
{ id: 2, name: 'html', type: 1, publicId: '', systemId: '' },
|
||||
{
|
||||
id: 3,
|
||||
type: 2,
|
||||
tagName: 'html',
|
||||
attributes: { lang: 'en' },
|
||||
childNodes: [
|
||||
{
|
||||
id: 4,
|
||||
type: 2,
|
||||
tagName: 'head',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 2,
|
||||
tagName: 'body',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
initialOffset: { top: 0, left: 0 },
|
||||
},
|
||||
type: EventType.FullSnapshot,
|
||||
timestamp: now + 100,
|
||||
},
|
||||
// mutation that adds select elements
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [],
|
||||
adds: [
|
||||
{
|
||||
parentId: 5,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'select',
|
||||
childNodes: [],
|
||||
id: 26,
|
||||
},
|
||||
},
|
||||
{
|
||||
parentId: 26,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'option',
|
||||
attributes: { value: 'valueC' },
|
||||
childNodes: [],
|
||||
id: 27,
|
||||
},
|
||||
},
|
||||
{
|
||||
parentId: 27,
|
||||
nextId: null,
|
||||
node: { type: 3, textContent: 'C', id: 28 },
|
||||
},
|
||||
{
|
||||
parentId: 26,
|
||||
nextId: 27,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'option',
|
||||
attributes: { value: 'valueB', selected: true },
|
||||
childNodes: [],
|
||||
id: 29,
|
||||
},
|
||||
},
|
||||
{
|
||||
parentId: 26,
|
||||
nextId: 29,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'option',
|
||||
attributes: { value: 'valueA' },
|
||||
childNodes: [],
|
||||
id: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
parentId: 30,
|
||||
nextId: null,
|
||||
node: { type: 3, textContent: 'A', id: 31 },
|
||||
},
|
||||
{
|
||||
parentId: 29,
|
||||
nextId: null,
|
||||
node: { type: 3, textContent: 'B', id: 32 },
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: now + 1000,
|
||||
},
|
||||
// input event
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Input,
|
||||
text: 'valueA',
|
||||
isChecked: false,
|
||||
id: 26,
|
||||
},
|
||||
timestamp: now + 1500,
|
||||
},
|
||||
// input event
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Input,
|
||||
text: 'valueC',
|
||||
isChecked: false,
|
||||
id: 26,
|
||||
},
|
||||
timestamp: now + 2000,
|
||||
},
|
||||
// mutation that adds an input element
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [],
|
||||
adds: [
|
||||
{
|
||||
parentId: 5,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'input',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
id: 33,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: now + 2500,
|
||||
},
|
||||
// an input event
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Input,
|
||||
text: 'test input',
|
||||
isChecked: false,
|
||||
id: 33,
|
||||
},
|
||||
timestamp: now + 3000,
|
||||
},
|
||||
// remove the select element
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [{ parentId: 5, id: 26 }],
|
||||
adds: [],
|
||||
},
|
||||
timestamp: now + 3500,
|
||||
},
|
||||
// remove the input element
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [{ parentId: 5, id: 33 }],
|
||||
adds: [],
|
||||
},
|
||||
timestamp: now + 4000,
|
||||
},
|
||||
];
|
||||
|
||||
export default events;
|
||||
128
packages/rrweb/test/events/scroll.ts
Normal file
128
packages/rrweb/test/events/scroll.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { EventType, eventWithTime, IncrementalSource } from '../../src/types';
|
||||
|
||||
const now = Date.now();
|
||||
const events: eventWithTime[] = [
|
||||
{
|
||||
type: EventType.DomContentLoaded,
|
||||
data: {},
|
||||
timestamp: now,
|
||||
},
|
||||
{
|
||||
type: EventType.Load,
|
||||
data: {},
|
||||
timestamp: now + 100,
|
||||
},
|
||||
{
|
||||
type: EventType.Meta,
|
||||
data: {
|
||||
href: 'http://localhost',
|
||||
width: 1200,
|
||||
height: 500,
|
||||
},
|
||||
timestamp: now + 100,
|
||||
},
|
||||
// full snapshot:
|
||||
{
|
||||
data: {
|
||||
node: {
|
||||
id: 1,
|
||||
type: 0,
|
||||
childNodes: [
|
||||
{ id: 2, name: 'html', type: 1, publicId: '', systemId: '' },
|
||||
{
|
||||
id: 3,
|
||||
type: 2,
|
||||
tagName: 'html',
|
||||
attributes: { lang: 'en' },
|
||||
childNodes: [
|
||||
{
|
||||
id: 4,
|
||||
type: 2,
|
||||
tagName: 'head',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 2,
|
||||
tagName: 'body',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
initialOffset: { top: 0, left: 0 },
|
||||
},
|
||||
type: EventType.FullSnapshot,
|
||||
timestamp: now + 100,
|
||||
},
|
||||
// mutation that adds two div elements
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [],
|
||||
adds: [
|
||||
{
|
||||
parentId: 5,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
id: 'container',
|
||||
style: 'height: 1000px; overflow: scroll;',
|
||||
},
|
||||
childNodes: [],
|
||||
id: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
parentId: 6,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
id: 'block',
|
||||
style: 'height: 10000px; background-color: yellow;',
|
||||
},
|
||||
childNodes: [],
|
||||
id: 7,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: now + 500,
|
||||
},
|
||||
// scroll event on the "#container" div
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: { source: IncrementalSource.Scroll, id: 6, x: 0, y: 2500 },
|
||||
timestamp: now + 1000,
|
||||
},
|
||||
// scroll event on document
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: { source: IncrementalSource.Scroll, id: 1, x: 0, y: 250 },
|
||||
timestamp: now + 1500,
|
||||
},
|
||||
// remove the "#container" div
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [{ parentId: 5, id: 6 }],
|
||||
adds: [],
|
||||
},
|
||||
timestamp: now + 2000,
|
||||
},
|
||||
];
|
||||
|
||||
export default events;
|
||||
172
packages/rrweb/test/events/shadow-dom.ts
Normal file
172
packages/rrweb/test/events/shadow-dom.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { EventType, eventWithTime, IncrementalSource } from '../../src/types';
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const events: eventWithTime[] = [
|
||||
{
|
||||
type: EventType.DomContentLoaded,
|
||||
data: {},
|
||||
timestamp: now,
|
||||
},
|
||||
{
|
||||
type: EventType.Load,
|
||||
data: {},
|
||||
timestamp: now + 100,
|
||||
},
|
||||
{
|
||||
type: EventType.Meta,
|
||||
data: {
|
||||
href: 'http://localhost',
|
||||
width: 1200,
|
||||
height: 500,
|
||||
},
|
||||
timestamp: now + 100,
|
||||
},
|
||||
{
|
||||
type: EventType.FullSnapshot,
|
||||
data: {
|
||||
node: {
|
||||
id: 1,
|
||||
type: 0,
|
||||
childNodes: [
|
||||
{ id: 2, name: 'html', type: 1, publicId: '', systemId: '' },
|
||||
{
|
||||
id: 3,
|
||||
type: 2,
|
||||
tagName: 'html',
|
||||
attributes: { lang: 'en' },
|
||||
childNodes: [
|
||||
{
|
||||
id: 4,
|
||||
type: 2,
|
||||
tagName: 'head',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 2,
|
||||
tagName: 'body',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
initialOffset: { top: 0, left: 0 },
|
||||
},
|
||||
timestamp: now + 200,
|
||||
},
|
||||
// add shadow dom elements
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [],
|
||||
adds: [
|
||||
{
|
||||
parentId: 5,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'div',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
id: 6,
|
||||
isShadowHost: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: now + 500,
|
||||
},
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [],
|
||||
adds: [
|
||||
{
|
||||
parentId: 6,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'span',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
id: 7,
|
||||
isShadow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
parentId: 7,
|
||||
nextId: null,
|
||||
node: { type: 3, textContent: 'shadow dom one', id: 8 },
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: now + 500,
|
||||
},
|
||||
// add nested shadow dom elements
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [],
|
||||
adds: [
|
||||
{
|
||||
parentId: 6,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'div',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
id: 9,
|
||||
isShadow: true,
|
||||
isShadowHost: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: now + 1000,
|
||||
},
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [],
|
||||
adds: [
|
||||
{
|
||||
parentId: 9,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: 2,
|
||||
tagName: 'span',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
id: 10,
|
||||
isShadow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
parentId: 10,
|
||||
nextId: null,
|
||||
node: { type: 3, textContent: 'shadow dom two', id: 11 },
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: now + 1000,
|
||||
},
|
||||
];
|
||||
|
||||
export default events;
|
||||
@@ -105,22 +105,6 @@ const events: eventWithTime[] = [
|
||||
type: EventType.FullSnapshot,
|
||||
timestamp: now + 100,
|
||||
},
|
||||
// mutation that adds style rule to existing stylesheet
|
||||
{
|
||||
data: {
|
||||
id: 101,
|
||||
adds: [
|
||||
{
|
||||
rule:
|
||||
'.css-added-at-400-overwritten-at-3000 {border: 1px solid blue;}',
|
||||
index: 1,
|
||||
},
|
||||
],
|
||||
source: IncrementalSource.StyleSheetRule,
|
||||
},
|
||||
type: EventType.IncrementalSnapshot,
|
||||
timestamp: now + 400,
|
||||
},
|
||||
// mutation that adds stylesheet
|
||||
{
|
||||
data: {
|
||||
@@ -142,7 +126,7 @@ const events: eventWithTime[] = [
|
||||
type: 3,
|
||||
isStyle: true,
|
||||
textContent:
|
||||
'\n.css-added-at-500 {\n padding: 1.3125rem;\n flex: none;\n width: 100%;\n}\n',
|
||||
'\n.css-added-at-400 {\n padding: 1.3125rem;\n flex: none;\n width: 100%;\n}\n',
|
||||
},
|
||||
nextId: null,
|
||||
parentId: 255,
|
||||
@@ -154,6 +138,22 @@ const events: eventWithTime[] = [
|
||||
attributes: [],
|
||||
},
|
||||
type: EventType.IncrementalSnapshot,
|
||||
timestamp: now + 400,
|
||||
},
|
||||
// mutation that adds style rule to existing stylesheet
|
||||
{
|
||||
data: {
|
||||
id: 101,
|
||||
adds: [
|
||||
{
|
||||
rule:
|
||||
'.css-added-at-500-overwritten-at-3000 {border: 1px solid blue;}',
|
||||
index: 1,
|
||||
},
|
||||
],
|
||||
source: IncrementalSource.StyleSheetRule,
|
||||
},
|
||||
type: EventType.IncrementalSnapshot,
|
||||
timestamp: now + 500,
|
||||
},
|
||||
// adds StyleSheetRule
|
||||
|
||||
178
packages/rrweb/test/events/style-sheet-text-mutation.ts
Normal file
178
packages/rrweb/test/events/style-sheet-text-mutation.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { EventType, eventWithTime, IncrementalSource } from '../../src/types';
|
||||
|
||||
const now = Date.now();
|
||||
const events: eventWithTime[] = [
|
||||
{
|
||||
type: EventType.DomContentLoaded,
|
||||
data: {},
|
||||
timestamp: now,
|
||||
},
|
||||
{
|
||||
type: EventType.Load,
|
||||
data: {},
|
||||
timestamp: now + 100,
|
||||
},
|
||||
{
|
||||
type: EventType.Meta,
|
||||
data: {
|
||||
href: 'http://localhost',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
},
|
||||
timestamp: now + 100,
|
||||
},
|
||||
// full snapshot
|
||||
{
|
||||
data: {
|
||||
node: {
|
||||
id: 1,
|
||||
type: 0,
|
||||
childNodes: [
|
||||
{ id: 2, name: 'html', type: 1, publicId: '', systemId: '' },
|
||||
{
|
||||
id: 3,
|
||||
type: 2,
|
||||
tagName: 'html',
|
||||
attributes: { lang: 'en' },
|
||||
childNodes: [
|
||||
{
|
||||
id: 4,
|
||||
type: 2,
|
||||
tagName: 'head',
|
||||
attributes: {},
|
||||
childNodes: [
|
||||
{
|
||||
id: 101,
|
||||
type: 2,
|
||||
tagName: 'style',
|
||||
attributes: {},
|
||||
childNodes: [
|
||||
{
|
||||
id: 102,
|
||||
type: 3,
|
||||
isStyle: true,
|
||||
textContent: '\n.css-added-at-100 {color: yellow;}\n',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 107,
|
||||
type: 2,
|
||||
tagName: 'body',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
initialOffset: { top: 0, left: 0 },
|
||||
},
|
||||
type: EventType.FullSnapshot,
|
||||
timestamp: now + 100,
|
||||
},
|
||||
// mutation that adds an element
|
||||
{
|
||||
data: {
|
||||
adds: [
|
||||
{
|
||||
node: {
|
||||
id: 108,
|
||||
type: 2,
|
||||
tagName: 'div',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
},
|
||||
nextId: null,
|
||||
parentId: 107,
|
||||
},
|
||||
],
|
||||
texts: [],
|
||||
source: IncrementalSource.Mutation,
|
||||
removes: [],
|
||||
attributes: [],
|
||||
},
|
||||
type: EventType.IncrementalSnapshot,
|
||||
timestamp: now + 500,
|
||||
},
|
||||
// adds a StyleSheetRule by inserting
|
||||
{
|
||||
data: {
|
||||
id: 101,
|
||||
adds: [
|
||||
{
|
||||
rule: '.css-added-at-1000-overwritten-at-1500 {color:red;}',
|
||||
},
|
||||
],
|
||||
source: IncrementalSource.StyleSheetRule,
|
||||
},
|
||||
type: EventType.IncrementalSnapshot,
|
||||
timestamp: now + 1000,
|
||||
},
|
||||
// adds a StyleSheetRule by adding a text
|
||||
{
|
||||
data: {
|
||||
adds: [
|
||||
{
|
||||
node: {
|
||||
type: 3,
|
||||
textContent: '.css-added-at-1500-deleted-at-2500 {color: yellow;}',
|
||||
id: 109,
|
||||
},
|
||||
nextId: null,
|
||||
parentId: 101,
|
||||
},
|
||||
],
|
||||
texts: [],
|
||||
source: IncrementalSource.Mutation,
|
||||
removes: [],
|
||||
attributes: [],
|
||||
},
|
||||
type: EventType.IncrementalSnapshot,
|
||||
timestamp: now + 1500,
|
||||
},
|
||||
// adds a StyleSheetRule by inserting
|
||||
{
|
||||
data: {
|
||||
id: 101,
|
||||
adds: [
|
||||
{
|
||||
rule: '.css-added-at-2000-overwritten-at-2500 {color: blue;}',
|
||||
},
|
||||
],
|
||||
source: IncrementalSource.StyleSheetRule,
|
||||
},
|
||||
type: EventType.IncrementalSnapshot,
|
||||
timestamp: now + 2000,
|
||||
},
|
||||
// deletes a StyleSheetRule by removing the text
|
||||
{
|
||||
data: {
|
||||
texts: [],
|
||||
attributes: [],
|
||||
removes: [{ parentId: 101, id: 109 }],
|
||||
adds: [],
|
||||
source: IncrementalSource.Mutation,
|
||||
},
|
||||
type: EventType.IncrementalSnapshot,
|
||||
timestamp: now + 2500,
|
||||
},
|
||||
// adds a StyleSheetRule by inserting
|
||||
{
|
||||
data: {
|
||||
id: 101,
|
||||
adds: [
|
||||
{
|
||||
rule: '.css-added-at-3000 {color: red;}',
|
||||
},
|
||||
],
|
||||
source: IncrementalSource.StyleSheetRule,
|
||||
},
|
||||
type: EventType.IncrementalSnapshot,
|
||||
timestamp: now + 3000,
|
||||
},
|
||||
];
|
||||
|
||||
export default events;
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as http from 'http';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import type * as http from 'http';
|
||||
import type * as puppeteer from 'puppeteer';
|
||||
import {
|
||||
assertSnapshot,
|
||||
startServer,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import type * as puppeteer from 'puppeteer';
|
||||
import {
|
||||
recordOptions,
|
||||
listenerHandler,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import type * as puppeteer from 'puppeteer';
|
||||
import {
|
||||
recordOptions,
|
||||
listenerHandler,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
stripBase64,
|
||||
waitForRAF,
|
||||
} from '../utils';
|
||||
import { ICanvas } from 'rrweb-snapshot';
|
||||
import type { ICanvas } from 'rrweb-snapshot';
|
||||
|
||||
interface ISuite {
|
||||
code: string;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { polyfillWebGLGlobals } from '../utils';
|
||||
polyfillWebGLGlobals();
|
||||
|
||||
import { Replayer } from '../../src/replay';
|
||||
import {} from '../../src/types';
|
||||
import {
|
||||
CanvasContext,
|
||||
CanvasArg,
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import {
|
||||
applyVirtualStyleRulesToNode,
|
||||
StyleRuleType,
|
||||
VirtualStyleRules,
|
||||
} from '../../src/replay/virtual-styles';
|
||||
|
||||
describe('virtual styles', () => {
|
||||
describe('applyVirtualStyleRulesToNode', () => {
|
||||
it('should insert rule at index 0 in empty sheet', () => {
|
||||
const dom = new JSDOM(`
|
||||
<style></style>
|
||||
`);
|
||||
const styleEl = dom.window.document.getElementsByTagName('style')[0];
|
||||
|
||||
const cssText = '.added-rule {border: 1px solid yellow;}';
|
||||
|
||||
const virtualStyleRules: VirtualStyleRules = [
|
||||
{ cssText, index: 0, type: StyleRuleType.Insert },
|
||||
];
|
||||
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
|
||||
|
||||
expect(styleEl.sheet?.cssRules?.length).toEqual(1);
|
||||
expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText);
|
||||
});
|
||||
|
||||
it('should insert rule at index 0 and keep exsisting rules', () => {
|
||||
const dom = new JSDOM(`
|
||||
<style>
|
||||
a {color: blue}
|
||||
div {color: black}
|
||||
</style>
|
||||
`);
|
||||
const styleEl = dom.window.document.getElementsByTagName('style')[0];
|
||||
|
||||
const cssText = '.added-rule {border: 1px solid yellow;}';
|
||||
const virtualStyleRules: VirtualStyleRules = [
|
||||
{ cssText, index: 0, type: StyleRuleType.Insert },
|
||||
];
|
||||
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
|
||||
|
||||
expect(styleEl.sheet?.cssRules?.length).toEqual(3);
|
||||
expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText);
|
||||
});
|
||||
|
||||
it('should delete rule at index 0', () => {
|
||||
const dom = new JSDOM(`
|
||||
<style>
|
||||
a {color: blue;}
|
||||
div {color: black;}
|
||||
</style>
|
||||
`);
|
||||
const styleEl = dom.window.document.getElementsByTagName('style')[0];
|
||||
|
||||
const virtualStyleRules: VirtualStyleRules = [
|
||||
{ index: 0, type: StyleRuleType.Remove },
|
||||
];
|
||||
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
|
||||
|
||||
expect(styleEl.sheet?.cssRules?.length).toEqual(1);
|
||||
expect(styleEl.sheet?.cssRules[0].cssText).toEqual('div {color: black;}');
|
||||
});
|
||||
|
||||
it('should restore a snapshot by inserting missing rules', () => {
|
||||
const dom = new JSDOM(`
|
||||
<style>
|
||||
a {color: blue;}
|
||||
.deleted-rule {color: pink;}
|
||||
div {color: black;}
|
||||
</style>
|
||||
`);
|
||||
const styleEl = dom.window.document.getElementsByTagName('style')[0];
|
||||
|
||||
const virtualStyleRules: VirtualStyleRules = [
|
||||
{
|
||||
cssTexts: ['a {color: blue;}', 'div {color: black;}'],
|
||||
type: StyleRuleType.Snapshot,
|
||||
},
|
||||
];
|
||||
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
|
||||
|
||||
expect(styleEl.sheet?.cssRules?.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should restore a snapshot by fixing order of rules', () => {
|
||||
const dom = new JSDOM(`
|
||||
<style>
|
||||
div {color: black;}
|
||||
a {color: blue;}
|
||||
</style>
|
||||
`);
|
||||
const styleEl = dom.window.document.getElementsByTagName('style')[0];
|
||||
|
||||
const cssTexts = ['a {color: blue;}', 'div {color: black;}'];
|
||||
|
||||
const virtualStyleRules: VirtualStyleRules = [
|
||||
{
|
||||
cssTexts,
|
||||
type: StyleRuleType.Snapshot,
|
||||
},
|
||||
];
|
||||
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
|
||||
|
||||
expect(styleEl.sheet?.cssRules?.length).toEqual(2);
|
||||
expect(
|
||||
Array.from(styleEl.sheet?.cssRules || []).map((rule) => rule.cssText),
|
||||
).toEqual(cssTexts);
|
||||
});
|
||||
|
||||
// JSDOM/CSSOM is currently broken for this test
|
||||
// remove '.skip' once https://github.com/NV/CSSOM/pull/113#issue-712485075 is merged
|
||||
it.skip('should insert rule at index [0,0] and keep exsisting rules', () => {
|
||||
const dom = new JSDOM(`
|
||||
<style>
|
||||
@media {
|
||||
a {color: blue}
|
||||
div {color: black}
|
||||
}
|
||||
</style>
|
||||
`);
|
||||
const styleEl = dom.window.document.getElementsByTagName('style')[0];
|
||||
|
||||
const cssText = '.added-rule {border: 1px solid yellow;}';
|
||||
const virtualStyleRules: VirtualStyleRules = [
|
||||
{ cssText, index: [0, 0], type: StyleRuleType.Insert },
|
||||
];
|
||||
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
|
||||
|
||||
console.log(
|
||||
Array.from((styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules),
|
||||
);
|
||||
|
||||
expect(
|
||||
(styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length,
|
||||
).toEqual(3);
|
||||
expect(
|
||||
(styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText,
|
||||
).toEqual(cssText);
|
||||
});
|
||||
|
||||
it('should delete rule at index [0,1]', () => {
|
||||
const dom = new JSDOM(`
|
||||
<style>
|
||||
@media {
|
||||
a {color: blue;}
|
||||
div {color: black;}
|
||||
}
|
||||
</style>
|
||||
`);
|
||||
const styleEl = dom.window.document.getElementsByTagName('style')[0];
|
||||
|
||||
const virtualStyleRules: VirtualStyleRules = [
|
||||
{ index: [0, 1], type: StyleRuleType.Remove },
|
||||
];
|
||||
applyVirtualStyleRulesToNode(virtualStyleRules, styleEl);
|
||||
|
||||
expect(
|
||||
(styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length,
|
||||
).toEqual(1);
|
||||
expect(
|
||||
(styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText,
|
||||
).toEqual('a {color: blue;}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { assertDomSnapshot, launchPuppeteer } from '../utils';
|
||||
import { launchPuppeteer } from '../utils';
|
||||
import { toMatchImageSnapshot } from 'jest-image-snapshot';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import type * as puppeteer from 'puppeteer';
|
||||
import events from '../events/webgl';
|
||||
|
||||
interface ISuite {
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import type * as puppeteer from 'puppeteer';
|
||||
import {
|
||||
assertDomSnapshot,
|
||||
launchPuppeteer,
|
||||
sampleEvents as events,
|
||||
sampleStyleSheetRemoveEvents as stylesheetRemoveEvents,
|
||||
waitForRAF,
|
||||
} from './utils';
|
||||
import styleSheetRuleEvents from './events/style-sheet-rule-events';
|
||||
import orderingEvents from './events/ordering';
|
||||
import scrollEvents from './events/scroll';
|
||||
import inputEvents from './events/input';
|
||||
import iframeEvents from './events/iframe';
|
||||
import shadowDomEvents from './events/shadow-dom';
|
||||
import StyleSheetTextMutation from './events/style-sheet-text-mutation';
|
||||
|
||||
interface ISuite {
|
||||
code: string;
|
||||
@@ -247,12 +252,206 @@ describe('replayer', function () {
|
||||
const rules = [...replayer.iframe.contentDocument.styleSheets].map(
|
||||
(sheet) => [...sheet.rules],
|
||||
).flat();
|
||||
rules.some((x) => x.selectorText === '.css-added-at-3100');
|
||||
rules.some((x) => x.selectorText === '.css-added-at-3100') &&
|
||||
!rules.some(
|
||||
(x) => x.selectorText === '.css-added-at-500-overwritten-at-3000',
|
||||
);
|
||||
`);
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it('should overwrite all StyleSheetRules by appending a text node to stylesheet element while fast-forwarding', async () => {
|
||||
await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`);
|
||||
const result = await page.evaluate(`
|
||||
const { Replayer } = rrweb;
|
||||
const replayer = new Replayer(events);
|
||||
replayer.pause(1600);
|
||||
const rules = [...replayer.iframe.contentDocument.styleSheets].map(
|
||||
(sheet) => [...sheet.rules],
|
||||
).flat();
|
||||
rules.some((x) => x.selectorText === '.css-added-at-1000-overwritten-at-1500');
|
||||
`);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('should apply fast-forwarded StyleSheetRules that came after appending text node to stylesheet element', async () => {
|
||||
await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`);
|
||||
const result = await page.evaluate(`
|
||||
const { Replayer } = rrweb;
|
||||
const replayer = new Replayer(events);
|
||||
replayer.pause(2100);
|
||||
const rules = [...replayer.iframe.contentDocument.styleSheets].map(
|
||||
(sheet) => [...sheet.rules],
|
||||
).flat();
|
||||
rules.some((x) => x.selectorText === '.css-added-at-2000-overwritten-at-2500');
|
||||
`);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it('should overwrite all StyleSheetRules by removing text node from stylesheet element while fast-forwarding', async () => {
|
||||
await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`);
|
||||
const result = await page.evaluate(`
|
||||
const { Replayer } = rrweb;
|
||||
const replayer = new Replayer(events);
|
||||
replayer.pause(2600);
|
||||
const rules = [...replayer.iframe.contentDocument.styleSheets].map(
|
||||
(sheet) => [...sheet.rules],
|
||||
).flat();
|
||||
rules.some((x) => x.selectorText === '.css-added-at-2000-overwritten-at-2500');
|
||||
`);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('should apply fast-forwarded StyleSheetRules that came after removing text node from stylesheet element', async () => {
|
||||
await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`);
|
||||
const result = await page.evaluate(`
|
||||
const { Replayer } = rrweb;
|
||||
const replayer = new Replayer(events);
|
||||
replayer.pause(3100);
|
||||
const rules = [...replayer.iframe.contentDocument.styleSheets].map(
|
||||
(sheet) => [...sheet.rules],
|
||||
).flat();
|
||||
rules.some((x) => x.selectorText === '.css-added-at-3000');
|
||||
`);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it('can fast forward scroll events', async () => {
|
||||
await page.evaluate(`
|
||||
events = ${JSON.stringify(scrollEvents)};
|
||||
const { Replayer } = rrweb;
|
||||
var replayer = new Replayer(events,{showDebug:true});
|
||||
replayer.pause(550);
|
||||
`);
|
||||
// add the "#container" element at 500
|
||||
const iframe = await page.$('iframe');
|
||||
const contentDocument = await iframe!.contentFrame()!;
|
||||
expect(await contentDocument!.$('#container')).not.toBeNull();
|
||||
expect(await contentDocument!.$('#block')).not.toBeNull();
|
||||
expect(
|
||||
await contentDocument!.$eval(
|
||||
'#container',
|
||||
(element: Element) => element.scrollTop,
|
||||
),
|
||||
).toEqual(0);
|
||||
|
||||
// restart the replayer
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await waitForRAF(page);
|
||||
|
||||
await page.evaluate('replayer.pause(1050);');
|
||||
// scroll the "#container" div' at 1000
|
||||
expect(
|
||||
await contentDocument!.$eval(
|
||||
'#container',
|
||||
(element: Element) => element.scrollTop,
|
||||
),
|
||||
).toEqual(2500);
|
||||
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(1550);');
|
||||
// scroll the document at 1500
|
||||
expect(
|
||||
await page.$eval(
|
||||
'iframe',
|
||||
(element: Element) =>
|
||||
(element as HTMLIFrameElement)!.contentWindow!.scrollY,
|
||||
),
|
||||
).toEqual(250);
|
||||
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(2050);');
|
||||
// remove the "#container" element at 2000
|
||||
expect(await contentDocument!.$('#container')).toBeNull();
|
||||
expect(await contentDocument!.$('#block')).toBeNull();
|
||||
expect(
|
||||
await page.$eval(
|
||||
'iframe',
|
||||
(element: Element) =>
|
||||
(element as HTMLIFrameElement)!.contentWindow!.scrollY,
|
||||
),
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it('can fast forward input events', async () => {
|
||||
await page.evaluate(`
|
||||
events = ${JSON.stringify(inputEvents)};
|
||||
const { Replayer } = rrweb;
|
||||
var replayer = new Replayer(events,{showDebug:true});
|
||||
replayer.pause(1050);
|
||||
`);
|
||||
const iframe = await page.$('iframe');
|
||||
const contentDocument = await iframe!.contentFrame()!;
|
||||
expect(await contentDocument!.$('select')).not.toBeNull();
|
||||
expect(
|
||||
await contentDocument!.$eval(
|
||||
'select',
|
||||
(element: Element) => (element as HTMLSelectElement).value,
|
||||
),
|
||||
).toEqual('valueB'); // the default value
|
||||
|
||||
// restart the replayer
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await waitForRAF(page);
|
||||
|
||||
await page.evaluate('replayer.pause(1550);');
|
||||
// the value get changed to 'valueA' at 1500
|
||||
expect(
|
||||
await contentDocument!.$eval(
|
||||
'select',
|
||||
(element: Element) => (element as HTMLSelectElement).value,
|
||||
),
|
||||
).toEqual('valueA');
|
||||
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(2050);');
|
||||
// the value get changed to 'valueC' at 2000
|
||||
expect(
|
||||
await contentDocument!.$eval(
|
||||
'select',
|
||||
(element: Element) => (element as HTMLSelectElement).value,
|
||||
),
|
||||
).toEqual('valueC');
|
||||
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(2550);');
|
||||
// add a new input element at 2500
|
||||
expect(
|
||||
await contentDocument!.$eval(
|
||||
'input',
|
||||
(element: Element) => (element as HTMLSelectElement).value,
|
||||
),
|
||||
).toEqual('');
|
||||
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(3050);');
|
||||
// set the value 'test input' for the input element at 3000
|
||||
expect(
|
||||
await contentDocument!.$eval(
|
||||
'input',
|
||||
(element: Element) => (element as HTMLSelectElement).value,
|
||||
),
|
||||
).toEqual('test input');
|
||||
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(3550);');
|
||||
// remove the select element at 3500
|
||||
expect(await contentDocument!.$('select')).toBeNull();
|
||||
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(4050);');
|
||||
// remove the input element at 4000
|
||||
expect(await contentDocument!.$('input')).toBeNull();
|
||||
});
|
||||
|
||||
it('can fast-forward mutation events containing nested iframe elements', async () => {
|
||||
await page.evaluate(`
|
||||
events = ${JSON.stringify(iframeEvents)};
|
||||
@@ -264,13 +463,12 @@ describe('replayer', function () {
|
||||
const contentDocument = await iframe!.contentFrame()!;
|
||||
expect(await contentDocument!.$('iframe')).toBeNull();
|
||||
|
||||
const delay = 50;
|
||||
// restart the replayer
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await page.waitForTimeout(delay);
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(550);'); // add 'iframe one' at 500
|
||||
expect(await contentDocument!.$('iframe')).not.toBeNull();
|
||||
const iframeOneDocument = await (await contentDocument!.$(
|
||||
let iframeOneDocument = await (await contentDocument!.$(
|
||||
'iframe',
|
||||
))!.contentFrame();
|
||||
expect(iframeOneDocument).not.toBeNull();
|
||||
@@ -286,14 +484,21 @@ describe('replayer', function () {
|
||||
|
||||
// add 'iframe two' and 'iframe three' at 1000
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await page.waitForTimeout(delay);
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(1050);');
|
||||
// check the inserted style of iframe 'one' again
|
||||
iframeOneDocument = await (await contentDocument!.$(
|
||||
'iframe',
|
||||
))!.contentFrame();
|
||||
expect((await iframeOneDocument!.$$('style')).length).toBe(1);
|
||||
|
||||
expect((await contentDocument!.$$('iframe')).length).toEqual(2);
|
||||
let iframeTwoDocument = await (
|
||||
await contentDocument!.$$('iframe')
|
||||
)[1]!.contentFrame();
|
||||
expect(iframeTwoDocument).not.toBeNull();
|
||||
expect((await iframeTwoDocument!.$$('iframe')).length).toEqual(2);
|
||||
expect((await iframeTwoDocument!.$$('style')).length).toBe(1);
|
||||
let iframeThreeDocument = await (
|
||||
await iframeTwoDocument!.$$('iframe')
|
||||
)[0]!.contentFrame();
|
||||
@@ -301,25 +506,27 @@ describe('replayer', function () {
|
||||
await iframeTwoDocument!.$$('iframe')
|
||||
)[1]!.contentFrame();
|
||||
expect(iframeThreeDocument).not.toBeNull();
|
||||
expect((await iframeThreeDocument!.$$('style')).length).toBe(1);
|
||||
expect(iframeFourDocument).not.toBeNull();
|
||||
|
||||
// add 'iframe four' at 1500
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await page.waitForTimeout(delay);
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(1550);');
|
||||
iframeTwoDocument = await (
|
||||
await contentDocument!.$$('iframe')
|
||||
)[1]!.contentFrame();
|
||||
expect((await iframeTwoDocument!.$$('style')).length).toBe(1);
|
||||
iframeFourDocument = await (
|
||||
await iframeTwoDocument!.$$('iframe')
|
||||
)[1]!.contentFrame();
|
||||
expect(await iframeFourDocument!.$('iframe')).toBeNull();
|
||||
expect(await iframeFourDocument!.$('style')).not.toBeNull();
|
||||
expect((await iframeFourDocument!.$$('style')).length).toBe(1);
|
||||
expect(await iframeFourDocument!.title()).toEqual('iframe 4');
|
||||
|
||||
// add 'iframe five' at 2000
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await page.waitForTimeout(delay);
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(2050);');
|
||||
iframeTwoDocument = await (
|
||||
await contentDocument!.$$('iframe')
|
||||
@@ -327,6 +534,7 @@ describe('replayer', function () {
|
||||
iframeFourDocument = await (
|
||||
await iframeTwoDocument!.$$('iframe')
|
||||
)[1]!.contentFrame();
|
||||
expect((await iframeFourDocument!.$$('style')).length).toBe(1);
|
||||
expect(await iframeFourDocument!.$('iframe')).not.toBeNull();
|
||||
const iframeFiveDocument = await (await iframeFourDocument!.$(
|
||||
'iframe',
|
||||
@@ -343,7 +551,7 @@ describe('replayer', function () {
|
||||
|
||||
// remove the html element of 'iframe four' at 2500
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await page.waitForTimeout(delay);
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(2550);');
|
||||
iframeTwoDocument = await (
|
||||
await contentDocument!.$$('iframe')
|
||||
@@ -362,6 +570,51 @@ describe('replayer', function () {
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('can fast-forward mutation events containing nested shadow doms', async () => {
|
||||
await page.evaluate(`
|
||||
events = ${JSON.stringify(shadowDomEvents)};
|
||||
const { Replayer } = rrweb;
|
||||
var replayer = new Replayer(events,{showDebug:true});
|
||||
replayer.pause(550);
|
||||
`);
|
||||
// add shadow dom 'one' at 500
|
||||
const iframe = await page.$('iframe');
|
||||
const contentDocument = await iframe!.contentFrame()!;
|
||||
expect(
|
||||
await contentDocument!.$eval('div', (element) => element.shadowRoot),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
await contentDocument!.evaluate(
|
||||
() =>
|
||||
document
|
||||
.querySelector('body > div')!
|
||||
.shadowRoot!.querySelector('span')!.textContent,
|
||||
),
|
||||
).toEqual('shadow dom one');
|
||||
|
||||
// add shadow dom 'two' at 1000
|
||||
await page.evaluate('replayer.play(0);');
|
||||
await waitForRAF(page);
|
||||
await page.evaluate('replayer.pause(1050);');
|
||||
expect(
|
||||
await contentDocument!.evaluate(
|
||||
() =>
|
||||
document
|
||||
.querySelector('body > div')!
|
||||
.shadowRoot!.querySelector('div')!.shadowRoot,
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
await contentDocument!.evaluate(
|
||||
() =>
|
||||
document
|
||||
.querySelector('body > div')!
|
||||
.shadowRoot!.querySelector('div')!
|
||||
.shadowRoot!.querySelector('span')!.textContent,
|
||||
),
|
||||
).toEqual('shadow dom two');
|
||||
});
|
||||
|
||||
it('can stream events in live mode', async () => {
|
||||
const status = await page.evaluate(`
|
||||
const { Replayer } = rrweb;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"target": "ES5",
|
||||
"target": "ES6",
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"removeComments": true,
|
||||
@@ -10,7 +10,8 @@
|
||||
"rootDir": "src",
|
||||
"outDir": "build",
|
||||
"lib": ["es6", "dom"],
|
||||
"downlevelIteration": true
|
||||
"downlevelIteration": true,
|
||||
"importsNotUsedAsValues": "error"
|
||||
},
|
||||
"exclude": ["test"],
|
||||
"include": [
|
||||
|
||||
2
packages/rrweb/typings/packer/base.d.ts
vendored
2
packages/rrweb/typings/packer/base.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { eventWithTime } from '../types';
|
||||
import type { eventWithTime } from '../types';
|
||||
export declare type PackFn = (event: eventWithTime) => string;
|
||||
export declare type UnpackFn = (raw: string) => eventWithTime;
|
||||
export declare type eventWithTimeAndPacker = eventWithTime & {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RecordPlugin } from '../../../types';
|
||||
import type { RecordPlugin } from '../../../types';
|
||||
export declare type StringifyOptions = {
|
||||
stringLengthLimit?: number;
|
||||
numOfKeysLimit: number;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
import { StringifyOptions } from './index';
|
||||
import type { StringifyOptions } from './index';
|
||||
export declare function stringify(obj: any, stringifyOptions?: StringifyOptions): string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RecordPlugin } from '../../../types';
|
||||
import type { RecordPlugin } from '../../../types';
|
||||
export declare type SequentialIdOptions = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SequentialIdOptions } from '../record';
|
||||
import { ReplayPlugin } from '../../../types';
|
||||
import type { ReplayPlugin } from '../../../types';
|
||||
declare type Options = SequentialIdOptions & {
|
||||
warnOnMissingId: boolean;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
||||
import { mutationCallBack } from '../types';
|
||||
import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
||||
import type { mutationCallBack } from '../types';
|
||||
export declare class IframeManager {
|
||||
private iframes;
|
||||
private mutationCb;
|
||||
|
||||
2
packages/rrweb/typings/record/mutation.d.ts
vendored
2
packages/rrweb/typings/record/mutation.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { mutationRecord, MutationBufferParam } from '../types';
|
||||
import type { mutationRecord, MutationBufferParam } from '../types';
|
||||
export default class MutationBuffer {
|
||||
private frozen;
|
||||
private locked;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { Mirror } from 'rrweb-snapshot';
|
||||
import type { Mirror } from 'rrweb-snapshot';
|
||||
import { blockClass, canvasManagerMutationCallback, IWindow, listenerHandler } from '../../../types';
|
||||
export default function initCanvas2DMutationObserver(cb: canvasManagerMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Mirror } from 'rrweb-snapshot';
|
||||
import { blockClass, canvasMutationCallback, IWindow } from '../../../types';
|
||||
import type { Mirror } from 'rrweb-snapshot';
|
||||
import type { blockClass, canvasMutationCallback, IWindow } from '../../../types';
|
||||
export declare type RafStamps = {
|
||||
latestId: number;
|
||||
invokeId: number | null;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
import { blockClass, IWindow, listenerHandler } from '../../../types';
|
||||
import type { blockClass, IWindow, listenerHandler } from '../../../types';
|
||||
export default function initCanvasContextObserver(win: IWindow, blockClass: blockClass): listenerHandler;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IWindow, CanvasArg } from '../../../types';
|
||||
import type { IWindow, CanvasArg } from '../../../types';
|
||||
export declare function variableListFor(ctx: RenderingContext, ctor: string): any[];
|
||||
export declare const saveWebGLVar: (value: any, win: IWindow, ctx: RenderingContext) => number | void;
|
||||
export declare function serializeArg(value: any, win: IWindow, ctx: RenderingContext): CanvasArg;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { Mirror } from 'rrweb-snapshot';
|
||||
import type { Mirror } from 'rrweb-snapshot';
|
||||
import { blockClass, canvasManagerMutationCallback, IWindow, listenerHandler } from '../../../types';
|
||||
export default function initCanvasWebGLMutationObserver(cb: canvasManagerMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mutationCallBack, scrollCallback, MutationBufferParam, SamplingStrategy } from '../types';
|
||||
import { Mirror } from 'rrweb-snapshot';
|
||||
import type { mutationCallBack, scrollCallback, MutationBufferParam, SamplingStrategy } from '../types';
|
||||
import type { Mirror } from 'rrweb-snapshot';
|
||||
declare type BypassOptions = Omit<MutationBufferParam, 'doc' | 'mutationCb' | 'mirror' | 'shadowDomManager'> & {
|
||||
sampling: SamplingStrategy;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ImageBitmapDataURLWorkerParams, ImageBitmapDataURLWorkerResponse } from '../../types';
|
||||
import type { ImageBitmapDataURLWorkerParams, ImageBitmapDataURLWorkerResponse } from '../../types';
|
||||
export interface ImageBitmapDataURLRequestWorker {
|
||||
postMessage: (message: ImageBitmapDataURLWorkerParams, transfer?: [ImageBitmap]) => void;
|
||||
onmessage: (message: MessageEvent<ImageBitmapDataURLWorkerResponse>) => void;
|
||||
|
||||
4
packages/rrweb/typings/replay/canvas/2d.d.ts
vendored
4
packages/rrweb/typings/replay/canvas/2d.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { Replayer } from '../';
|
||||
import { canvasMutationCommand } from '../../types';
|
||||
import type { Replayer } from '../';
|
||||
import type { canvasMutationCommand } from '../../types';
|
||||
export default function canvasMutation({ event, mutation, target, imageMap, errorHandler, }: {
|
||||
event: Parameters<Replayer['applyIncremental']>[0];
|
||||
mutation: canvasMutationCommand;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Replayer } from '../';
|
||||
import { CanvasArg, SerializedCanvasArg } from '../../types';
|
||||
import type { CanvasArg, SerializedCanvasArg } from '../../types';
|
||||
export declare function variableListFor(ctx: CanvasRenderingContext2D | WebGLRenderingContext | WebGL2RenderingContext, ctor: string): any[];
|
||||
export declare function isSerializedArg(arg: unknown): arg is SerializedCanvasArg;
|
||||
export declare function deserializeArg(imageMap: Replayer['imageMap'], ctx: CanvasRenderingContext2D | WebGLRenderingContext | WebGL2RenderingContext | null, preload?: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Replayer } from '..';
|
||||
import type { Replayer } from '..';
|
||||
import { canvasMutationData } from '../../types';
|
||||
export default function canvasMutation({ event, mutation, target, imageMap, canvasEventMap, errorHandler, }: {
|
||||
event: Parameters<Replayer['applyIncremental']>[0];
|
||||
|
||||
12
packages/rrweb/typings/replay/index.d.ts
vendored
12
packages/rrweb/typings/replay/index.d.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { Mirror } from 'rrweb-snapshot';
|
||||
import { RRDocument } from 'rrdom/es/virtual-dom';
|
||||
import { Timer } from './timer';
|
||||
import { createPlayerService, createSpeedService } from './machine';
|
||||
import { eventWithTime, playerConfig, playerMetaData, Handler } from '../types';
|
||||
@@ -10,16 +11,14 @@ export declare class Replayer {
|
||||
speedService: ReturnType<typeof createSpeedService>;
|
||||
get timer(): Timer;
|
||||
config: playerConfig;
|
||||
usingVirtualDom: boolean;
|
||||
virtualDom: RRDocument;
|
||||
private mouse;
|
||||
private mouseTail;
|
||||
private tailPositions;
|
||||
private emitter;
|
||||
private nextUserInteractionEvent;
|
||||
private legacy_missingNodeRetryMap;
|
||||
private treeIndex;
|
||||
private fragmentParentMap;
|
||||
private elementStateMap;
|
||||
private virtualStyleRulesMap;
|
||||
private cache;
|
||||
private imageMap;
|
||||
private canvasEventMap;
|
||||
@@ -62,17 +61,12 @@ export declare class Replayer {
|
||||
private applyMutation;
|
||||
private applyScroll;
|
||||
private applyInput;
|
||||
private applyText;
|
||||
private legacy_resolveMissingNode;
|
||||
private moveAndHover;
|
||||
private drawMouseTail;
|
||||
private hoverElements;
|
||||
private isUserInteraction;
|
||||
private backToNormal;
|
||||
private restoreRealParent;
|
||||
private storeState;
|
||||
private restoreState;
|
||||
private restoreNodeSheet;
|
||||
private warnNodeNotFound;
|
||||
private warnCanvasMutationFailed;
|
||||
private debugNodeNotFound;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
export declare enum StyleRuleType {
|
||||
Insert = 0,
|
||||
Remove = 1,
|
||||
Snapshot = 2,
|
||||
SetProperty = 3,
|
||||
RemoveProperty = 4
|
||||
}
|
||||
declare type InsertRule = {
|
||||
cssText: string;
|
||||
type: StyleRuleType.Insert;
|
||||
index?: number | number[];
|
||||
};
|
||||
declare type RemoveRule = {
|
||||
type: StyleRuleType.Remove;
|
||||
index: number | number[];
|
||||
};
|
||||
declare type SnapshotRule = {
|
||||
type: StyleRuleType.Snapshot;
|
||||
cssTexts: string[];
|
||||
};
|
||||
declare type SetPropertyRule = {
|
||||
type: StyleRuleType.SetProperty;
|
||||
index: number[];
|
||||
property: string;
|
||||
value: string | null;
|
||||
priority: string | undefined;
|
||||
};
|
||||
declare type RemovePropertyRule = {
|
||||
type: StyleRuleType.RemoveProperty;
|
||||
index: number[];
|
||||
property: string;
|
||||
};
|
||||
export declare type VirtualStyleRules = Array<InsertRule | RemoveRule | SnapshotRule | SetPropertyRule | RemovePropertyRule>;
|
||||
export declare type VirtualStyleRulesMap = Map<Node, VirtualStyleRules>;
|
||||
export declare function getNestedRule(rules: CSSRuleList, position: number[]): CSSGroupingRule;
|
||||
export declare function getPositionsAndIndex(nestedIndex: number[]): {
|
||||
positions: number[];
|
||||
index: number | undefined;
|
||||
};
|
||||
export declare function applyVirtualStyleRulesToNode(storedRules: VirtualStyleRules, styleNode: HTMLStyleElement): void;
|
||||
export declare function storeCSSRules(parentElement: HTMLStyleElement, virtualStyleRulesMap: VirtualStyleRulesMap): void;
|
||||
export {};
|
||||
21
packages/rrweb/typings/types.d.ts
vendored
21
packages/rrweb/typings/types.d.ts
vendored
@@ -1,9 +1,10 @@
|
||||
import { serializedNodeWithId, Mirror, INode, MaskInputOptions, SlimDOMOptions, MaskInputFn, MaskTextFn } from 'rrweb-snapshot';
|
||||
import { PackFn, UnpackFn } from './packer/base';
|
||||
import { IframeManager } from './record/iframe-manager';
|
||||
import { ShadowDomManager } from './record/shadow-dom-manager';
|
||||
import type { serializedNodeWithId, Mirror, INode, MaskInputOptions, SlimDOMOptions, MaskInputFn, MaskTextFn } from 'rrweb-snapshot';
|
||||
import type { PackFn, UnpackFn } from './packer/base';
|
||||
import type { IframeManager } from './record/iframe-manager';
|
||||
import type { ShadowDomManager } from './record/shadow-dom-manager';
|
||||
import type { Replayer } from './replay';
|
||||
import { CanvasManager } from './record/observers/canvas/canvas-manager';
|
||||
import type { RRNode } from 'rrdom/es/virtual-dom';
|
||||
import type { CanvasManager } from './record/observers/canvas/canvas-manager';
|
||||
export declare enum EventType {
|
||||
DomContentLoaded = 0,
|
||||
Load = 1,
|
||||
@@ -115,6 +116,10 @@ export declare type eventWithTime = event & {
|
||||
timestamp: number;
|
||||
delay?: number;
|
||||
};
|
||||
export declare type canvasEventWithTime = eventWithTime & {
|
||||
type: EventType.IncrementalSnapshot;
|
||||
data: canvasMutationData;
|
||||
};
|
||||
export declare type blockClass = string | RegExp;
|
||||
export declare type maskTextClass = string | RegExp;
|
||||
export declare type SamplingStrategy = Partial<{
|
||||
@@ -464,6 +469,7 @@ export declare type playerConfig = {
|
||||
strokeStyle?: string;
|
||||
};
|
||||
unpackFn?: UnpackFn;
|
||||
useVirtualDom: boolean;
|
||||
plugins?: ReplayPlugin[];
|
||||
};
|
||||
export declare type playerMetaData = {
|
||||
@@ -472,7 +478,7 @@ export declare type playerMetaData = {
|
||||
totalTime: number;
|
||||
};
|
||||
export declare type missingNode = {
|
||||
node: Node;
|
||||
node: Node | RRNode;
|
||||
mutation: addedNodeMutation;
|
||||
};
|
||||
export declare type missingNodeMap = {
|
||||
@@ -507,9 +513,6 @@ export declare enum ReplayerEvents {
|
||||
StateChange = "state-change",
|
||||
PlayBack = "play-back"
|
||||
}
|
||||
export declare type ElementState = {
|
||||
scroll?: [number, number];
|
||||
};
|
||||
export declare type KeepIframeSrcFn = (src: string) => boolean;
|
||||
declare global {
|
||||
interface Window {
|
||||
|
||||
50
packages/rrweb/typings/utils.d.ts
vendored
50
packages/rrweb/typings/utils.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
import { throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, removedNodeMutation, textMutation, attributeMutation, mutationData, scrollData, inputData, DocumentDimension, IWindow, DeprecatedMirror } from './types';
|
||||
import { Mirror } from 'rrweb-snapshot';
|
||||
import type { throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, DocumentDimension, IWindow, DeprecatedMirror, textMutation } from './types';
|
||||
import type { IMirror, Mirror } from 'rrweb-snapshot';
|
||||
import type { RRNode, RRIFrameElement } from 'rrdom/es/virtual-dom';
|
||||
export declare function on(type: string, fn: EventListenerOrEventListenerObject, target?: Document | IWindow): listenerHandler;
|
||||
export declare let _mirror: DeprecatedMirror;
|
||||
export declare function throttle<T>(func: (arg: T) => void, wait: number, options?: throttleOptions): (arg: T) => void;
|
||||
@@ -15,38 +16,6 @@ export declare function isIgnored(n: Node, mirror: Mirror): boolean;
|
||||
export declare function isAncestorRemoved(target: Node, mirror: Mirror): boolean;
|
||||
export declare function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent;
|
||||
export declare function polyfill(win?: Window & typeof globalThis): void;
|
||||
export declare type TreeNode = {
|
||||
id: number;
|
||||
mutation: addedNodeMutation;
|
||||
parent?: TreeNode;
|
||||
children: Record<number, TreeNode>;
|
||||
texts: textMutation[];
|
||||
attributes: attributeMutation[];
|
||||
};
|
||||
export declare class TreeIndex {
|
||||
tree: Record<number, TreeNode>;
|
||||
private removeNodeMutations;
|
||||
private textMutations;
|
||||
private attributeMutations;
|
||||
private indexes;
|
||||
private removeIdSet;
|
||||
private scrollMap;
|
||||
private inputMap;
|
||||
constructor();
|
||||
add(mutation: addedNodeMutation): void;
|
||||
remove(mutation: removedNodeMutation, mirror: Mirror): void;
|
||||
text(mutation: textMutation): void;
|
||||
attribute(mutation: attributeMutation): void;
|
||||
scroll(d: scrollData): void;
|
||||
input(d: inputData): void;
|
||||
flush(): {
|
||||
mutationData: mutationData;
|
||||
scrollMap: TreeIndex['scrollMap'];
|
||||
inputMap: TreeIndex['inputMap'];
|
||||
};
|
||||
private reset;
|
||||
idRemoved(id: number): boolean;
|
||||
}
|
||||
declare type ResolveTree = {
|
||||
value: addedNodeMutation;
|
||||
children: ResolveTree[];
|
||||
@@ -56,12 +25,17 @@ export declare function queueToResolveTrees(queue: addedNodeMutation[]): Resolve
|
||||
export declare function iterateResolveTree(tree: ResolveTree, cb: (mutation: addedNodeMutation) => unknown): void;
|
||||
export declare type AppendedIframe = {
|
||||
mutationInQueue: addedNodeMutation;
|
||||
builtNode: HTMLIFrameElement;
|
||||
builtNode: HTMLIFrameElement | RRIFrameElement;
|
||||
};
|
||||
export declare function isSerializedIframe(n: Node, mirror: Mirror): n is HTMLIFrameElement;
|
||||
export declare function isSerializedIframe<TNode extends Node | RRNode>(n: TNode, mirror: IMirror<TNode>): boolean;
|
||||
export declare function getBaseDimension(node: Node, rootIframe: Node): DocumentDimension;
|
||||
export declare function hasShadowRoot<T extends Node>(n: T): n is T & {
|
||||
export declare function hasShadowRoot<T extends Node | RRNode>(n: T): n is T & {
|
||||
shadowRoot: ShadowRoot;
|
||||
};
|
||||
export declare function getUniqueTextMutations(mutations: textMutation[]): textMutation[];
|
||||
export declare function getNestedRule(rules: CSSRuleList, position: number[]): CSSGroupingRule;
|
||||
export declare function getPositionsAndIndex(nestedIndex: number[]): {
|
||||
positions: number[];
|
||||
index: number | undefined;
|
||||
};
|
||||
export declare function uniqueTextMutations(mutations: textMutation[]): textMutation[];
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user