* 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
|
dist: focal
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
- 12
|
- lts/*
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- yarn
|
- yarn
|
||||||
|
|||||||
3
.vscode/rrweb-monorepo.code-workspace
vendored
3
.vscode/rrweb-monorepo.code-workspace
vendored
@@ -24,8 +24,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"jest.disabledWorkspaceFolders": [
|
"jest.disabledWorkspaceFolders": [
|
||||||
" rrweb monorepo",
|
" rrweb monorepo",
|
||||||
"rrweb-player (package)",
|
"rrweb-player (package)"
|
||||||
"rrdom (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 |
|
| insertStyleRules | [] | accepts multiple CSS rule string, which will be injected into the replay iframe |
|
||||||
| triggerFocus | true | whether to trigger focus during replay |
|
| 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.** |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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
|
#### Use rrweb-player
|
||||||
|
|
||||||
|
|||||||
@@ -295,10 +295,11 @@ replayer.pause(5000);
|
|||||||
| insertStyleRules | [] | 可以传入多个 CSS rule string,用于自定义回放时 iframe 内的样式 |
|
| insertStyleRules | [] | 可以传入多个 CSS rule string,用于自定义回放时 iframe 内的样式 |
|
||||||
| triggerFocus | true | 回放时是否回放 focus 交互 |
|
| triggerFocus | true | 回放时是否回放 focus 交互 |
|
||||||
| UNSAFE_replayCanvas | false | 回放时是否回放 canvas 内容,**开启后将会关闭沙盒策略,导致一定风险** |
|
| UNSAFE_replayCanvas | false | 回放时是否回放 canvas 内容,**开启后将会关闭沙盒策略,导致一定风险** |
|
||||||
|
| pauseAnimation | true | 当播放器停止播放时,是否将 CSS 动画也停止播放 |
|
||||||
| mouseTail | true | 是否在回放时增加鼠标轨迹。传入 false 可关闭,传入对象可以定制轨迹持续时间、样式等,配置详见[类型](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L407) |
|
| mouseTail | true | 是否在回放时增加鼠标轨迹。传入 false 可关闭,传入对象可以定制轨迹持续时间、样式等,配置详见[类型](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L407) |
|
||||||
| unpackFn | - | 数据解压缩函数,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) |
|
| 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) |
|
| plugins | [] | 加载插件以获得额外的回放功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) |
|
||||||
|
| useVirtualDom | true | 在播放器跳转到一个新的时间点的过程中,是否使用 Virtual Dom 优化 |
|
||||||
|
|
||||||
#### 使用 rrweb-player
|
#### 使用 rrweb-player
|
||||||
|
|
||||||
|
|||||||
@@ -30,5 +30,8 @@
|
|||||||
"test:watch": "yarn lerna run test:watch --parallel",
|
"test:watch": "yarn lerna run test:watch --parallel",
|
||||||
"dev": "yarn lerna run dev --parallel",
|
"dev": "yarn lerna run dev --parallel",
|
||||||
"repl": "cd packages/rrweb && npm run repl"
|
"repl": "cd packages/rrweb && npm run repl"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"**/jsdom/cssom": "^0.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"dev": "rollup -c -w",
|
"dev": "rollup -c -w",
|
||||||
"bundle": "rollup --config",
|
"bundle": "rollup --config",
|
||||||
"bundle:es-only": "cross-env ES_ONLY=true rollup --config",
|
"bundle:es-only": "cross-env ES_ONLY=true rollup --config",
|
||||||
|
"check-types": "tsc -noEmit",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"prepublish": "npm run bundle"
|
"prepublish": "npm run bundle"
|
||||||
},
|
},
|
||||||
@@ -27,18 +28,24 @@
|
|||||||
"@rollup/plugin-commonjs": "^20.0.0",
|
"@rollup/plugin-commonjs": "^20.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^13.0.4",
|
"@rollup/plugin-node-resolve": "^13.0.4",
|
||||||
"@types/cssom": "^0.4.1",
|
"@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",
|
"@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": "^2.56.3",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"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",
|
"rrweb-snapshot": "^1.1.14",
|
||||||
"ts-jest": "^27.0.5",
|
"ts-jest": "^27.1.3",
|
||||||
"typescript": "^3.9.5"
|
"typescript": "^4.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssom": "^0.5.0",
|
"cssom": "^0.5.0",
|
||||||
|
"cssstyle": "^2.3.0",
|
||||||
"nwsapi": "^2.2.0"
|
"nwsapi": "^2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import resolve from '@rollup/plugin-node-resolve';
|
|||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
import { terser } from 'rollup-plugin-terser';
|
import { terser } from 'rollup-plugin-terser';
|
||||||
import typescript from 'rollup-plugin-typescript2';
|
import typescript from 'rollup-plugin-typescript2';
|
||||||
|
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
|
||||||
import pkg from './package.json';
|
import pkg from './package.json';
|
||||||
|
|
||||||
function toMinPath(path) {
|
function toMinPath(path) {
|
||||||
@@ -11,6 +12,10 @@ function toMinPath(path) {
|
|||||||
const basePlugins = [
|
const basePlugins = [
|
||||||
resolve({ browser: true }),
|
resolve({ browser: true }),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
|
|
||||||
|
// supports bundling `web-worker:..filename` from rrweb
|
||||||
|
webWorkerLoader(),
|
||||||
|
|
||||||
typescript({
|
typescript({
|
||||||
tsconfigOverride: { compilerOptions: { module: 'ESNext' } },
|
tsconfigOverride: { compilerOptions: { module: 'ESNext' } },
|
||||||
}),
|
}),
|
||||||
@@ -27,6 +32,11 @@ const baseConfigs = [
|
|||||||
name: 'RRDocument',
|
name: 'RRDocument',
|
||||||
path: 'document-nodejs',
|
path: 'document-nodejs',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: './src/virtual-dom.ts',
|
||||||
|
name: 'RRDocument',
|
||||||
|
path: 'virtual-dom',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let configs = [];
|
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 { NodeType as RRNodeType } from 'rrweb-snapshot';
|
||||||
import { NWSAPI } from 'nwsapi';
|
import type { NWSAPI } from 'nwsapi';
|
||||||
import { parseCSSText, camelize, toCSSText } from './style';
|
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 nwsapi = require('nwsapi');
|
||||||
const cssom = require('cssom');
|
const cssom = require('cssom');
|
||||||
|
const cssstyle = require('cssstyle');
|
||||||
|
|
||||||
export abstract class RRNode {
|
export class RRNode extends BaseRRNode {}
|
||||||
__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 RRWindow {
|
export class RRWindow {
|
||||||
scrollLeft = 0;
|
scrollLeft = 0;
|
||||||
@@ -74,8 +30,10 @@ export class RRWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RRDocument extends RRNode {
|
export class RRDocument
|
||||||
private mirror: Map<number, RRNode> = new Map();
|
extends BaseRRDocumentImpl(RRNode)
|
||||||
|
implements IRRDocument {
|
||||||
|
readonly nodeName: '#document' = '#document';
|
||||||
private _nwsapi: NWSAPI;
|
private _nwsapi: NWSAPI;
|
||||||
get nwsapi() {
|
get nwsapi() {
|
||||||
if (!this._nwsapi) {
|
if (!this._nwsapi) {
|
||||||
@@ -95,66 +53,32 @@ export class RRDocument extends RRNode {
|
|||||||
return this._nwsapi;
|
return this._nwsapi;
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentElement(): RRElement {
|
get documentElement(): RRElement | null {
|
||||||
return this.children.find(
|
return super.documentElement as RRElement | null;
|
||||||
(node) => node instanceof RRElement && node.tagName === 'HTML',
|
|
||||||
) as RRElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get body() {
|
get body(): RRElement | null {
|
||||||
return (
|
return super.body as RRElement | null;
|
||||||
this.documentElement?.children.find(
|
|
||||||
(node) => node instanceof RRElement && node.tagName === 'BODY',
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get head() {
|
get head() {
|
||||||
return (
|
return super.head as RRElement | null;
|
||||||
this.documentElement?.children.find(
|
|
||||||
(node) => node instanceof RRElement && node.tagName === 'HEAD',
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get implementation() {
|
get implementation(): RRDocument {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
get firstElementChild() {
|
get firstElementChild(): RRElement | null {
|
||||||
return this.documentElement;
|
return this.documentElement as RRElement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
appendChild(childNode: RRNode) {
|
appendChild(childNode: RRNode) {
|
||||||
const nodeType = childNode.nodeType;
|
return super.appendChild(childNode);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
insertBefore(newChild: RRNode, refChild: RRNode | null) {
|
insertBefore(newChild: RRNode, refChild: RRNode | null) {
|
||||||
if (refChild === null) return this.appendChild(newChild);
|
return super.insertBefore(newChild, refChild);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
querySelectorAll(selectors: string): RRNode[] {
|
querySelectorAll(selectors: string): RRNode[] {
|
||||||
@@ -216,16 +140,16 @@ export class RRDocument extends RRNode {
|
|||||||
element = new RRMediaElement(upperTagName);
|
element = new RRMediaElement(upperTagName);
|
||||||
break;
|
break;
|
||||||
case 'IFRAME':
|
case 'IFRAME':
|
||||||
element = new RRIframeElement(upperTagName);
|
element = new RRIFrameElement(upperTagName);
|
||||||
break;
|
break;
|
||||||
case 'IMG':
|
case 'IMG':
|
||||||
element = new RRImageElement('IMG');
|
element = new RRImageElement(upperTagName);
|
||||||
break;
|
break;
|
||||||
case 'CANVAS':
|
case 'CANVAS':
|
||||||
element = new RRCanvasElement('CANVAS');
|
element = new RRCanvasElement(upperTagName);
|
||||||
break;
|
break;
|
||||||
case 'STYLE':
|
case 'STYLE':
|
||||||
element = new RRStyleElement('STYLE');
|
element = new RRStyleElement(upperTagName);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
element = new RRElement(upperTagName);
|
element = new RRElement(upperTagName);
|
||||||
@@ -235,10 +159,7 @@ export class RRDocument extends RRNode {
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
createElementNS(
|
createElementNS(_namespaceURI: string, qualifiedName: string) {
|
||||||
_namespaceURI: 'http://www.w3.org/2000/svg',
|
|
||||||
qualifiedName: string,
|
|
||||||
) {
|
|
||||||
return this.createElement(qualifiedName as keyof HTMLElementTagNameMap);
|
return this.createElement(qualifiedName as keyof HTMLElementTagNameMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,266 +180,40 @@ export class RRDocument extends RRNode {
|
|||||||
textNode.ownerDocument = this;
|
textNode.ownerDocument = this;
|
||||||
return textNode;
|
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 {
|
export class RRDocumentType extends BaseRRDocumentTypeImpl(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 RRElement extends BaseRRElementImpl(RRNode) {
|
||||||
|
private _style: CSSStyleDeclarationType;
|
||||||
constructor(tagName: string) {
|
constructor(tagName: string) {
|
||||||
super();
|
super(tagName);
|
||||||
this.tagName = tagName;
|
this._style = new cssstyle.CSSStyleDeclaration();
|
||||||
}
|
const style = this._style;
|
||||||
|
Object.defineProperty(this.attributes, 'style', {
|
||||||
get classList() {
|
get() {
|
||||||
return new ClassList(
|
return style.cssText;
|
||||||
this.attributes.class as string | undefined,
|
|
||||||
(newClassName) => {
|
|
||||||
this.attributes.class = newClassName;
|
|
||||||
},
|
},
|
||||||
);
|
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() {
|
get style() {
|
||||||
const style = (this.attributes.style
|
return (this._style as unknown) as CSSStyleDeclaration;
|
||||||
? 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get firstElementChild(): RRElement | null {
|
attachShadow(_init: ShadowRootInit): RRElement {
|
||||||
for (let child of this.children)
|
return super.attachShadow(_init) as RRElement;
|
||||||
if (child instanceof RRElement) return child;
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get nextElementSibling(): RRElement | null {
|
appendChild(newChild: RRNode): RRNode {
|
||||||
let parentNode = this.parentNode;
|
return super.appendChild(newChild) as RRNode;
|
||||||
if (!parentNode) return null;
|
}
|
||||||
const siblings = parentNode.children;
|
|
||||||
let index = siblings.indexOf(this);
|
insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode {
|
||||||
for (let i = index + 1; i < siblings.length; i++)
|
return super.insertBefore(newChild, refChild) as RRNode;
|
||||||
if (siblings[i] instanceof RRElement) return siblings[i] as RRElement;
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttribute(name: string) {
|
getAttribute(name: string) {
|
||||||
@@ -531,57 +226,44 @@ export class RRElement extends RRNode {
|
|||||||
this.attributes[name.toLowerCase()] = attribute;
|
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) {
|
removeAttribute(name: string) {
|
||||||
delete this.attributes[name];
|
delete this.attributes[name.toLowerCase()];
|
||||||
}
|
}
|
||||||
|
|
||||||
appendChild(newChild: RRNode): RRNode {
|
get firstElementChild(): RRElement | null {
|
||||||
this.children.push(newChild);
|
for (let child of this.childNodes)
|
||||||
newChild.parentNode = this;
|
if (child.RRNodeType === RRNodeType.Element) return child as RRElement;
|
||||||
newChild.parentElement = this;
|
return null;
|
||||||
newChild.ownerDocument = this.ownerDocument;
|
|
||||||
return newChild;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode {
|
get nextElementSibling(): RRElement | null {
|
||||||
if (refChild === null) return this.appendChild(newChild);
|
let parentNode = this.parentNode;
|
||||||
const childIndex = this.children.indexOf(refChild);
|
if (!parentNode) return null;
|
||||||
if (childIndex == -1)
|
const siblings = parentNode.childNodes;
|
||||||
throw new Error(
|
let index = siblings.indexOf(this);
|
||||||
"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.",
|
for (let i = index + 1; i < siblings.length; i++)
|
||||||
);
|
if (siblings[i] instanceof RRElement) return siblings[i] as RRElement;
|
||||||
this.children.splice(childIndex, 0, newChild);
|
return null;
|
||||||
newChild.parentElement = null;
|
|
||||||
newChild.parentNode = this;
|
|
||||||
newChild.ownerDocument = this.ownerDocument;
|
|
||||||
return newChild;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
querySelectorAll(selectors: string): RRNode[] {
|
querySelectorAll(selectors: string): RRNode[] {
|
||||||
|
const result: RRElement[] = [];
|
||||||
if (this.ownerDocument !== null) {
|
if (this.ownerDocument !== null) {
|
||||||
return (this.ownerDocument.nwsapi.select(
|
((this.ownerDocument as RRDocument).nwsapi.select(
|
||||||
selectors,
|
selectors,
|
||||||
(this as unknown) as Element,
|
(this as unknown) as Element,
|
||||||
|
(element) => {
|
||||||
|
if (((element as unknown) as RRElement) !== this)
|
||||||
|
result.push((element as unknown) as RRElement);
|
||||||
|
},
|
||||||
) as unknown) as RRNode[];
|
) as unknown) as RRNode[];
|
||||||
}
|
}
|
||||||
return [];
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
getElementById(elementId: string): RRElement | null {
|
getElementById(elementId: string): RRElement | null {
|
||||||
if (this instanceof RRElement && this.id === elementId) return this;
|
if (this.id === elementId) return this;
|
||||||
for (const child of this.children) {
|
for (const child of this.childNodes) {
|
||||||
if (child instanceof RRElement) {
|
if (child instanceof RRElement) {
|
||||||
const result = child.getElementById(elementId);
|
const result = child.getElementById(elementId);
|
||||||
if (result !== null) return result;
|
if (result !== null) return result;
|
||||||
@@ -596,12 +278,12 @@ export class RRElement extends RRNode {
|
|||||||
// Make sure this element has all queried class names.
|
// Make sure this element has all queried class names.
|
||||||
if (
|
if (
|
||||||
this instanceof RRElement &&
|
this instanceof RRElement &&
|
||||||
queryClassList.filter((queriedClassName) =>
|
queryClassList.classes.filter((queriedClassName) =>
|
||||||
this.classList.some((name) => name === queriedClassName),
|
this.classList.classes.some((name) => name === queriedClassName),
|
||||||
).length == queryClassList.length
|
).length == queryClassList.classes.length
|
||||||
)
|
)
|
||||||
elements.push(this);
|
elements.push(this);
|
||||||
for (const child of this.children) {
|
for (const child of this.childNodes) {
|
||||||
if (child instanceof RRElement)
|
if (child instanceof RRElement)
|
||||||
elements = elements.concat(child.getElementsByClassName(className));
|
elements = elements.concat(child.getElementsByClassName(className));
|
||||||
}
|
}
|
||||||
@@ -613,32 +295,12 @@ export class RRElement extends RRNode {
|
|||||||
const normalizedTagName = tagName.toUpperCase();
|
const normalizedTagName = tagName.toUpperCase();
|
||||||
if (this instanceof RRElement && this.tagName === normalizedTagName)
|
if (this instanceof RRElement && this.tagName === normalizedTagName)
|
||||||
elements.push(this);
|
elements.push(this);
|
||||||
for (const child of this.children) {
|
for (const child of this.childNodes) {
|
||||||
if (child instanceof RRElement)
|
if (child instanceof RRElement)
|
||||||
elements = elements.concat(child.getElementsByTagName(tagName));
|
elements = elements.concat(child.getElementsByTagName(tagName));
|
||||||
}
|
}
|
||||||
return elements;
|
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 {
|
export class RRImageElement extends RRElement {
|
||||||
@@ -648,16 +310,7 @@ export class RRImageElement extends RRElement {
|
|||||||
onload: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
onload: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RRMediaElement extends RRElement {
|
export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {}
|
||||||
currentTime: number = 0;
|
|
||||||
paused: boolean = true;
|
|
||||||
async play() {
|
|
||||||
this.paused = false;
|
|
||||||
}
|
|
||||||
async pause() {
|
|
||||||
this.paused = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RRCanvasElement extends RRElement {
|
export class RRCanvasElement extends RRElement {
|
||||||
/**
|
/**
|
||||||
@@ -675,7 +328,7 @@ export class RRStyleElement extends RRElement {
|
|||||||
if (!this._sheet) {
|
if (!this._sheet) {
|
||||||
let result = '';
|
let result = '';
|
||||||
for (let child of this.childNodes)
|
for (let child of this.childNodes)
|
||||||
if (child.nodeType === NodeType.Text)
|
if (child.RRNodeType === RRNodeType.Text)
|
||||||
result += (child as RRText).textContent;
|
result += (child as RRText).textContent;
|
||||||
this._sheet = cssom.parse(result);
|
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 = '';
|
width: string = '';
|
||||||
height: string = '';
|
height: string = '';
|
||||||
src: string = '';
|
src: string = '';
|
||||||
@@ -699,89 +352,27 @@ export class RRIframeElement extends RRElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RRText extends RRNode {
|
export class RRText extends BaseRRTextImpl(RRNode) {
|
||||||
textContent: string;
|
readonly nodeName: '#text' = '#text';
|
||||||
|
|
||||||
constructor(data: string) {
|
|
||||||
super();
|
|
||||||
this.textContent = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `${super.toString('RRText')} text=${JSON.stringify(
|
|
||||||
this.textContent,
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RRComment extends RRNode {
|
export class RRComment extends BaseRRCommentImpl(RRNode) {
|
||||||
data: string;
|
readonly nodeName: '#comment' = '#comment';
|
||||||
|
|
||||||
constructor(data: string) {
|
|
||||||
super();
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `${super.toString('RRComment')} data=${JSON.stringify(this.data)}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export class RRCDATASection extends RRNode {
|
|
||||||
data: string;
|
|
||||||
|
|
||||||
constructor(data: string) {
|
export class RRCDATASection extends BaseRRCDATASectionImpl(RRNode) {
|
||||||
super();
|
readonly nodeName: '#cdata-section' = '#cdata-section';
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `${super.toString('RRCDATASection')} data=${JSON.stringify(
|
|
||||||
this.data,
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RRElementTagNameMap {
|
interface RRElementTagNameMap {
|
||||||
img: RRImageElement;
|
|
||||||
audio: RRMediaElement;
|
audio: RRMediaElement;
|
||||||
|
canvas: RRCanvasElement;
|
||||||
|
iframe: RRIFrameElement;
|
||||||
|
img: RRImageElement;
|
||||||
|
style: RRStyleElement;
|
||||||
video: RRMediaElement;
|
video: RRMediaElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RRElementType<
|
type RRElementType<
|
||||||
K extends keyof HTMLElementTagNameMap
|
K extends keyof HTMLElementTagNameMap
|
||||||
> = K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement;
|
> = 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.
|
* 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() {
|
export function polyfillPerformance() {
|
||||||
if (typeof window !== 'undefined' || 'performance' in global) return;
|
if (typeof window !== 'undefined' || 'performance' in global) return;
|
||||||
@@ -80,8 +82,8 @@ export function polyfillDocument() {
|
|||||||
const rrdom = new RRDocument();
|
const rrdom = new RRDocument();
|
||||||
(() => {
|
(() => {
|
||||||
rrdom.appendChild(rrdom.createElement('html'));
|
rrdom.appendChild(rrdom.createElement('html'));
|
||||||
rrdom.documentElement.appendChild(rrdom.createElement('head'));
|
rrdom.documentElement!.appendChild(rrdom.createElement('head'));
|
||||||
rrdom.documentElement.appendChild(rrdom.createElement('body'));
|
rrdom.documentElement!.appendChild(rrdom.createElement('body'));
|
||||||
})();
|
})();
|
||||||
global.document = (rrdom as unknown) as Document;
|
global.document = (rrdom as unknown) as Document;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
export function parseCSSText(cssText: string): Record<string, string> {
|
export function parseCSSText(cssText: string): Record<string, string> {
|
||||||
const res: Record<string, string> = {};
|
const res: Record<string, string> = {};
|
||||||
const listDelimiter = /;(?![^(]*\))/g;
|
const listDelimiter = /;(?![^(]*\))/g;
|
||||||
const propertyDelimiter = /:(.+)/;
|
const propertyDelimiter = /:(.+)/;
|
||||||
cssText.split(listDelimiter).forEach(function (item) {
|
const comment = /\/\*.*?\*\//g;
|
||||||
if (item) {
|
cssText
|
||||||
const tmp = item.split(propertyDelimiter);
|
.replace(comment, '')
|
||||||
tmp.length > 1 && (res[camelize(tmp[0].trim())] = tmp[1].trim());
|
.split(listDelimiter)
|
||||||
}
|
.forEach(function (item) {
|
||||||
});
|
if (item) {
|
||||||
return res;
|
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};`);
|
||||||
}
|
}
|
||||||
|
return properties.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
export function toCSSText(style: Record<string, string>): string {
|
/**
|
||||||
const properties = [];
|
* Camelize a hyphen-delimited string.
|
||||||
for (let name in style) {
|
*/
|
||||||
const value = style[name];
|
const camelizeRE = /-([a-z])/g;
|
||||||
if (typeof value !== 'string') continue;
|
const CUSTOM_PROPERTY_REGEX = /^--[a-zA-Z0-9-]+$/;
|
||||||
const normalizedName = hyphenate(name);
|
export const camelize = (str: string): string => {
|
||||||
properties.push(`${normalizedName}:${value};`);
|
if (CUSTOM_PROPERTY_REGEX.test(str)) return str;
|
||||||
}
|
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { RRDocument, RRElement, RRStyleElement } from '../src/document-nodejs';
|
import { NodeType as RRNodeType } from 'rrweb-snapshot';
|
||||||
import { printRRDom } from './util';
|
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('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', () => {
|
describe('RRDocument API', () => {
|
||||||
let rrdom: RRDocument;
|
let rrdom: RRDocument;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// initialize rrdom
|
// initialize rrdom
|
||||||
document.write(getHtml('main.html'));
|
document.write(getHtml('main.html'));
|
||||||
rrdom = new RRDocument();
|
rrdom = new RRDocument();
|
||||||
rrdom.buildFromDom(document);
|
buildFromDom(document, undefined, rrdom);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('get className', () => {
|
it('can create different type of RRNodes', () => {
|
||||||
expect(rrdom.getElementsByTagName('DIV')[0].className).toEqual(
|
const document = rrdom.createDocument('', '');
|
||||||
'blocks blocks1',
|
expect(document).toBeInstanceOf(RRDocument);
|
||||||
);
|
const audio = rrdom.createElement('audio');
|
||||||
expect(rrdom.getElementsByTagName('DIV')[1].className).toEqual(
|
expect(audio).toBeInstanceOf(RRMediaElement);
|
||||||
'blocks blocks1 :hover',
|
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', () => {
|
it('can get head element', () => {
|
||||||
expect(rrdom.getElementsByTagName('DIV')[0].id).toEqual('block1');
|
expect(rrdom.head).toBeDefined();
|
||||||
expect(rrdom.getElementsByTagName('DIV')[1].id).toEqual('block2');
|
expect(rrdom.head!.tagName).toBe('HEAD');
|
||||||
expect(rrdom.getElementsByTagName('DIV')[2].id).toEqual('block3');
|
expect(rrdom.head!.parentElement).toBe(rrdom.documentElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('get attribute name', () => {
|
it('can get body element', () => {
|
||||||
expect(
|
expect(rrdom.body).toBeDefined();
|
||||||
rrdom.getElementsByTagName('DIV')[0].getAttribute('class'),
|
expect(rrdom.body!.tagName).toBe('BODY');
|
||||||
).toEqual('blocks blocks1');
|
expect(rrdom.body!.parentElement).toBe(rrdom.documentElement);
|
||||||
expect(
|
});
|
||||||
rrdom.getElementsByTagName('dIv')[0].getAttribute('cLaSs'),
|
|
||||||
).toEqual('blocks blocks1');
|
it('can get implementation', () => {
|
||||||
expect(rrdom.getElementsByTagName('DIV')[0].getAttribute('id')).toEqual(
|
expect(rrdom.implementation).toBeDefined();
|
||||||
'block1',
|
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(
|
expect(() =>
|
||||||
'block1',
|
rrdom.insertBefore(rrdom.createElement('div'), null),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRElement on RRDocument allowed."`,
|
||||||
);
|
);
|
||||||
expect(
|
const node = new RRDocument();
|
||||||
rrdom.getElementsByTagName('p')[0].getAttribute('class'),
|
const doctype = rrdom.createDocumentType('', '', '');
|
||||||
).toBeNull();
|
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', () => {
|
it('get firstElementChild', () => {
|
||||||
expect(rrdom.firstElementChild).toBeDefined();
|
expect(rrdom.firstElementChild).toBeDefined();
|
||||||
expect(rrdom.firstElementChild.tagName).toEqual('HTML');
|
expect(rrdom.firstElementChild!.tagName).toEqual('HTML');
|
||||||
|
|
||||||
const div1 = rrdom.getElementById('block1');
|
const div1 = rrdom.getElementById('block1');
|
||||||
expect(div1).toBeDefined();
|
expect(div1).toBeDefined();
|
||||||
@@ -73,31 +111,6 @@ describe('RRDocument for nodejs environment', () => {
|
|||||||
expect(div2!.firstElementChild!.id).toEqual('block3');
|
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', () => {
|
it('getElementsByTagName', () => {
|
||||||
for (let tagname of [
|
for (let tagname of [
|
||||||
'HTML',
|
'HTML',
|
||||||
@@ -114,6 +127,8 @@ describe('RRDocument for nodejs environment', () => {
|
|||||||
'BUTTON',
|
'BUTTON',
|
||||||
'IMG',
|
'IMG',
|
||||||
'CANVAS',
|
'CANVAS',
|
||||||
|
'FORM',
|
||||||
|
'INPUT',
|
||||||
]) {
|
]) {
|
||||||
const expectedResult = document.getElementsByTagName(tagname).length;
|
const expectedResult = document.getElementsByTagName(tagname).length;
|
||||||
expect(rrdom.getElementsByTagName(tagname).length).toEqual(
|
expect(rrdom.getElementsByTagName(tagname).length).toEqual(
|
||||||
@@ -126,6 +141,8 @@ describe('RRDocument for nodejs environment', () => {
|
|||||||
expect(node.tagName).toEqual(tagname);
|
expect(node.tagName).toEqual(tagname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const node = new RRDocument();
|
||||||
|
expect(node.getElementsByTagName('h2').length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getElementsByClassName', () => {
|
it('getElementsByClassName', () => {
|
||||||
@@ -148,6 +165,8 @@ describe('RRDocument for nodejs environment', () => {
|
|||||||
result: document.getElementsByClassName(className).length,
|
result: document.getElementsByClassName(className).length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const node = new RRDocument();
|
||||||
|
expect(node.getElementsByClassName('block').length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getElementById', () => {
|
it('getElementById', () => {
|
||||||
@@ -157,6 +176,8 @@ describe('RRDocument for nodejs environment', () => {
|
|||||||
}
|
}
|
||||||
for (let elementId of ['block', 'blocks', 'blocks1'])
|
for (let elementId of ['block', 'blocks', 'blocks1'])
|
||||||
expect(rrdom.getElementById(elementId)).toBeNull();
|
expect(rrdom.getElementById(elementId)).toBeNull();
|
||||||
|
const node = new RRDocument();
|
||||||
|
expect(node.getElementById('id')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('querySelectorAll querying tag name', () => {
|
it('querySelectorAll querying tag name', () => {
|
||||||
@@ -193,7 +214,7 @@ describe('RRDocument for nodejs environment', () => {
|
|||||||
}
|
}
|
||||||
for (let element of rrdom.querySelectorAll('.\\:hover')) {
|
for (let element of rrdom.querySelectorAll('.\\:hover')) {
|
||||||
expect(element).toBeInstanceOf(RRElement);
|
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#block1').length).toEqual(1);
|
||||||
expect(rrdom.querySelectorAll('.blocks#block3').length).toEqual(0);
|
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', () => {
|
it('style element', () => {
|
||||||
expect(rrdom.getElementsByTagName('style').length).not.toEqual(0);
|
expect(rrdom.getElementsByTagName('style').length).not.toEqual(0);
|
||||||
@@ -250,6 +508,36 @@ describe('RRDocument for nodejs environment', () => {
|
|||||||
expect(rules[5]).toBeUndefined();
|
expect(rules[5]).toBeUndefined();
|
||||||
expect(rules[4].cssText).toEqual(`@import url(main.css);`);
|
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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Main</title>
|
<title>Main</title>
|
||||||
<link rel="stylesheet" href="somelink">
|
<link rel="stylesheet" href="somelink" />
|
||||||
<style>
|
<style>
|
||||||
h1 {
|
h1 {
|
||||||
color: 'black';
|
color: 'black';
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
width: 100px;
|
width: 100px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
@import url("main.css");
|
@import url('main.css');
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -35,6 +35,10 @@
|
|||||||
Text 2
|
Text 2
|
||||||
</div>
|
</div>
|
||||||
<img src="somelink" alt="This is an image" />
|
<img src="somelink" alt="This is an image" />
|
||||||
|
<!-- This is a line of comment -->
|
||||||
|
<form>
|
||||||
|
<input type="text" id="input1" />
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 { RRDocument, RRNode } from '../src/document-nodejs';
|
||||||
import {
|
import {
|
||||||
polyfillPerformance,
|
polyfillPerformance,
|
||||||
@@ -9,7 +10,8 @@ import {
|
|||||||
|
|
||||||
describe('polyfill for nodejs', () => {
|
describe('polyfill for nodejs', () => {
|
||||||
it('should polyfill performance api', () => {
|
it('should polyfill performance api', () => {
|
||||||
expect(global.performance).toBeUndefined();
|
if (compare(process.version, 'v16.0.0', '<'))
|
||||||
|
expect(global.performance).toBeUndefined();
|
||||||
polyfillPerformance();
|
polyfillPerformance();
|
||||||
expect(global.performance).toBeDefined();
|
expect(global.performance).toBeDefined();
|
||||||
expect(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', () => {
|
it('should polyfill requestAnimationFrame', () => {
|
||||||
expect(global.requestAnimationFrame).toBeUndefined();
|
expect(global.requestAnimationFrame).toBeUndefined();
|
||||||
expect(global.cancelAnimationFrame).toBeUndefined();
|
expect(global.cancelAnimationFrame).toBeUndefined();
|
||||||
@@ -59,12 +73,32 @@ describe('polyfill for nodejs', () => {
|
|||||||
jest.useRealTimers();
|
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', () => {
|
it('should polyfill Event type', () => {
|
||||||
|
// if the second version is greater
|
||||||
|
if (compare(process.version, 'v15.0.0', '<'))
|
||||||
|
expect(global.Event).toBeUndefined();
|
||||||
polyfillEvent();
|
polyfillEvent();
|
||||||
expect(global.Event).toBeDefined();
|
expect(global.Event).toBeDefined();
|
||||||
expect(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', () => {
|
it('should polyfill Node type', () => {
|
||||||
expect(global.Node).toBeUndefined();
|
expect(global.Node).toBeUndefined();
|
||||||
polyfillNode();
|
polyfillNode();
|
||||||
@@ -73,6 +107,13 @@ describe('polyfill for nodejs', () => {
|
|||||||
expect(Node).toEqual(RRNode);
|
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', () => {
|
it('should polyfill document object', () => {
|
||||||
expect(global.document).toBeUndefined();
|
expect(global.document).toBeUndefined();
|
||||||
polyfillDocument();
|
polyfillDocument();
|
||||||
@@ -80,4 +121,11 @@ describe('polyfill for nodejs', () => {
|
|||||||
expect(document).toBeDefined();
|
expect(document).toBeDefined();
|
||||||
expect(document).toBeInstanceOf(RRDocument);
|
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": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "ES6",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
@@ -11,9 +11,10 @@
|
|||||||
"outDir": "build",
|
"outDir": "build",
|
||||||
"lib": ["es6", "dom"],
|
"lib": ["es6", "dom"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"declaration": true
|
"declaration": true,
|
||||||
|
"importsNotUsedAsValues": "error"
|
||||||
},
|
},
|
||||||
"compileOnSave": true,
|
"compileOnSave": true,
|
||||||
"exclude": ["test"],
|
"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",
|
"name": "rrweb-player",
|
||||||
"version": "0.7.14",
|
"version": "0.7.14",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^21.0.2",
|
"@rollup/plugin-commonjs": "^22.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^7.0.0",
|
"@rollup/plugin-node-resolve": "^13.2.1",
|
||||||
"@rollup/plugin-typescript": "^4.0.0",
|
"@rollup/plugin-typescript": "^8.3.2",
|
||||||
|
"@types/offscreencanvas": "^2019.6.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.7.0",
|
"@typescript-eslint/eslint-plugin": "^3.7.0",
|
||||||
"@typescript-eslint/parser": "^3.7.0",
|
"@typescript-eslint/parser": "^3.7.0",
|
||||||
"eslint": "^7.5.0",
|
"eslint": "^7.5.0",
|
||||||
"eslint-config-google": "^0.11.0",
|
"eslint-config-google": "^0.11.0",
|
||||||
"eslint-plugin-svelte3": "^2.7.3",
|
"eslint-plugin-svelte3": "^2.7.3",
|
||||||
"postcss-easy-import": "^3.0.0",
|
"postcss-easy-import": "^3.0.0",
|
||||||
"rollup": "^2.45.2",
|
"rollup": "^2.71.1",
|
||||||
"rollup-plugin-css-only": "^3.1.0",
|
"rollup-plugin-css-only": "^3.1.0",
|
||||||
"rollup-plugin-livereload": "^2.0.0",
|
"rollup-plugin-livereload": "^2.0.0",
|
||||||
"rollup-plugin-svelte": "^7.1.0",
|
"rollup-plugin-svelte": "^7.1.0",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||||
"sirv-cli": "^0.4.4",
|
"sirv-cli": "^0.4.4",
|
||||||
"svelte": "^3.2.0",
|
"svelte": "^3.2.0",
|
||||||
"svelte-check": "^1.4.0",
|
"svelte-check": "^1.4.0",
|
||||||
"svelte-preprocess": "^4.0.0",
|
"svelte-preprocess": "^4.0.0",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "^3.9.7"
|
"typescript": "^4.6.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tsconfig/svelte": "^1.0.0",
|
"@tsconfig/svelte": "^1.0.0",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import commonjs from '@rollup/plugin-commonjs';
|
|||||||
import livereload from 'rollup-plugin-livereload';
|
import livereload from 'rollup-plugin-livereload';
|
||||||
import { terser } from 'rollup-plugin-terser';
|
import { terser } from 'rollup-plugin-terser';
|
||||||
import sveltePreprocess from 'svelte-preprocess';
|
import sveltePreprocess from 'svelte-preprocess';
|
||||||
|
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
|
||||||
import typescript from '@rollup/plugin-typescript';
|
import typescript from '@rollup/plugin-typescript';
|
||||||
import pkg from './package.json';
|
import pkg from './package.json';
|
||||||
import css from 'rollup-plugin-css-only';
|
import css from 'rollup-plugin-css-only';
|
||||||
@@ -64,8 +65,12 @@ export default entries.map((output) => ({
|
|||||||
browser: true,
|
browser: true,
|
||||||
dedupe: ['svelte'],
|
dedupe: ['svelte'],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
commonjs(),
|
commonjs(),
|
||||||
|
|
||||||
|
// supports bundling `web-worker:..filename` from rrweb
|
||||||
|
webWorkerLoader(),
|
||||||
|
|
||||||
typescript(),
|
typescript(),
|
||||||
|
|
||||||
css({
|
css({
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
"include": ["src/**/*"],
|
"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) {
|
if (n.rootId) {
|
||||||
console.assert(
|
console.assert(
|
||||||
(mirror.getNode(n.rootId) as Document) === doc,
|
(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
|
// use target document as root document
|
||||||
|
|||||||
@@ -76,6 +76,28 @@ export interface ICanvas extends HTMLCanvasElement {
|
|||||||
__context: string;
|
__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 idNodeMap = Map<number, Node>;
|
||||||
|
|
||||||
export type nodeMetaMap = WeakMap<Node, serializedNodeWithId>;
|
export type nodeMetaMap = WeakMap<Node, serializedNodeWithId>;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
MaskInputFn,
|
MaskInputFn,
|
||||||
MaskInputOptions,
|
MaskInputOptions,
|
||||||
nodeMetaMap,
|
nodeMetaMap,
|
||||||
|
IMirror,
|
||||||
serializedNodeWithId,
|
serializedNodeWithId,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ export function isShadowRoot(n: Node): n is ShadowRoot {
|
|||||||
return Boolean(host && host.shadowRoot && host.shadowRoot === n);
|
return Boolean(host && host.shadowRoot && host.shadowRoot === n);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Mirror {
|
export class Mirror implements IMirror<Node> {
|
||||||
private idNodeMap: idNodeMap = new Map();
|
private idNodeMap: idNodeMap = new Map();
|
||||||
private nodeMetaMap: nodeMetaMap = new WeakMap();
|
private nodeMetaMap: nodeMetaMap = new WeakMap();
|
||||||
|
|
||||||
@@ -47,7 +48,9 @@ export class Mirror {
|
|||||||
this.idNodeMap.delete(id);
|
this.idNodeMap.delete(id);
|
||||||
|
|
||||||
if (n.childNodes) {
|
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 {
|
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 {
|
export interface ICanvas extends HTMLCanvasElement {
|
||||||
__context: string;
|
__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 idNodeMap = Map<number, Node>;
|
||||||
export declare type nodeMetaMap = WeakMap<Node, serializedNodeWithId>;
|
export declare type nodeMetaMap = WeakMap<Node, serializedNodeWithId>;
|
||||||
export declare type MaskInputOptions = Partial<{
|
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 isElement(n: Node): n is Element;
|
||||||
export declare function isShadowRoot(n: Node): n is ShadowRoot;
|
export declare function isShadowRoot(n: Node): n is ShadowRoot;
|
||||||
export declare class Mirror {
|
export declare class Mirror implements IMirror<Node> {
|
||||||
private idNodeMap;
|
private idNodeMap;
|
||||||
private nodeMetaMap;
|
private nodeMetaMap;
|
||||||
getId(n: Node | undefined | null): number;
|
getId(n: Node | undefined | null): number;
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ module.exports = {
|
|||||||
testMatch: ['**/**.test.ts'],
|
testMatch: ['**/**.test.ts'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.css$': 'identity-obj-proxy',
|
'\\.css$': 'identity-obj-proxy',
|
||||||
|
'rrdom/es/(.*)': 'rrdom/lib/$1',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,12 +46,12 @@
|
|||||||
"@types/inquirer": "0.0.43",
|
"@types/inquirer": "0.0.43",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/jest-image-snapshot": "^4.3.1",
|
"@types/jest-image-snapshot": "^4.3.1",
|
||||||
"@types/jsdom": "^16.2.14",
|
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/offscreencanvas": "^2019.6.4",
|
"@types/offscreencanvas": "^2019.6.4",
|
||||||
"@types/prettier": "^2.3.2",
|
"@types/prettier": "^2.3.2",
|
||||||
"@types/puppeteer": "^5.4.4",
|
"@types/puppeteer": "^5.4.4",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
|
"esbuild": "^0.14.38",
|
||||||
"fast-mhtml": "^1.1.9",
|
"fast-mhtml": "^1.1.9",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"ignore-styles": "^5.0.1",
|
"ignore-styles": "^5.0.1",
|
||||||
@@ -59,14 +59,12 @@
|
|||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"jest-image-snapshot": "^4.5.1",
|
"jest-image-snapshot": "^4.5.1",
|
||||||
"jest-snapshot": "^23.6.0",
|
"jest-snapshot": "^23.6.0",
|
||||||
"jsdom": "^17.0.0",
|
|
||||||
"jsdom-global": "^3.0.2",
|
|
||||||
"prettier": "2.2.1",
|
"prettier": "2.2.1",
|
||||||
"puppeteer": "^9.1.1",
|
"puppeteer": "^9.1.1",
|
||||||
"rollup": "^2.68.0",
|
"rollup": "^2.68.0",
|
||||||
|
"rollup-plugin-esbuild": "^4.9.1",
|
||||||
"rollup-plugin-postcss": "^3.1.1",
|
"rollup-plugin-postcss": "^3.1.1",
|
||||||
"rollup-plugin-rename-node-modules": "^1.3.1",
|
"rollup-plugin-rename-node-modules": "^1.3.1",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
|
||||||
"rollup-plugin-typescript2": "^0.31.2",
|
"rollup-plugin-typescript2": "^0.31.2",
|
||||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||||
"ts-jest": "^27.1.3",
|
"ts-jest": "^27.1.3",
|
||||||
@@ -81,6 +79,7 @@
|
|||||||
"base64-arraybuffer": "^1.0.1",
|
"base64-arraybuffer": "^1.0.1",
|
||||||
"fflate": "^0.4.4",
|
"fflate": "^0.4.4",
|
||||||
"mitt": "^1.1.3",
|
"mitt": "^1.1.3",
|
||||||
|
"rrdom": "^0.1.2",
|
||||||
"rrweb-snapshot": "^1.1.14"
|
"rrweb-snapshot": "^1.1.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import typescript from 'rollup-plugin-typescript2';
|
import typescript from 'rollup-plugin-typescript2';
|
||||||
|
import esbuild from 'rollup-plugin-esbuild';
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
import { terser } from 'rollup-plugin-terser';
|
|
||||||
import postcss from 'rollup-plugin-postcss';
|
import postcss from 'rollup-plugin-postcss';
|
||||||
import renameNodeModules from 'rollup-plugin-rename-node-modules';
|
import renameNodeModules from 'rollup-plugin-rename-node-modules';
|
||||||
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
|
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
|
||||||
@@ -108,10 +108,34 @@ const baseConfigs = [
|
|||||||
|
|
||||||
let configs = [];
|
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) {
|
for (const c of baseConfigs) {
|
||||||
const basePlugins = [
|
const basePlugins = [
|
||||||
resolve({ browser: true }),
|
resolve({ browser: true }),
|
||||||
|
|
||||||
|
// supports bundling `web-worker:..filename`
|
||||||
webWorkerLoader(),
|
webWorkerLoader(),
|
||||||
|
|
||||||
typescript(),
|
typescript(),
|
||||||
];
|
];
|
||||||
const plugins = basePlugins.concat(
|
const plugins = basePlugins.concat(
|
||||||
@@ -123,7 +147,7 @@ for (const c of baseConfigs) {
|
|||||||
// browser
|
// browser
|
||||||
configs.push({
|
configs.push({
|
||||||
input: c.input,
|
input: c.input,
|
||||||
plugins,
|
plugins: getPlugins(),
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
name: c.name,
|
name: c.name,
|
||||||
@@ -135,14 +159,7 @@ for (const c of baseConfigs) {
|
|||||||
// browser + minify
|
// browser + minify
|
||||||
configs.push({
|
configs.push({
|
||||||
input: c.input,
|
input: c.input,
|
||||||
plugins: basePlugins.concat(
|
plugins: getPlugins({ minify: true, sourceMap: true }),
|
||||||
postcss({
|
|
||||||
extract: true,
|
|
||||||
minimize: true,
|
|
||||||
sourceMap: true,
|
|
||||||
}),
|
|
||||||
terser(),
|
|
||||||
),
|
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
name: c.name,
|
name: c.name,
|
||||||
@@ -197,23 +214,9 @@ if (process.env.BROWSER_ONLY) {
|
|||||||
configs = [];
|
configs = [];
|
||||||
|
|
||||||
for (const c of browserOnlyBaseConfigs) {
|
for (const c of browserOnlyBaseConfigs) {
|
||||||
const plugins = [
|
|
||||||
resolve({ browser: true }),
|
|
||||||
webWorkerLoader(),
|
|
||||||
typescript({
|
|
||||||
outDir: null,
|
|
||||||
}),
|
|
||||||
postcss({
|
|
||||||
extract: false,
|
|
||||||
inject: false,
|
|
||||||
sourceMap: true,
|
|
||||||
}),
|
|
||||||
terser(),
|
|
||||||
];
|
|
||||||
|
|
||||||
configs.push({
|
configs.push({
|
||||||
input: c.input,
|
input: c.input,
|
||||||
plugins,
|
plugins: getPlugins({ sourceMap: true, minify: true }),
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
name: c.name,
|
name: c.name,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { eventWithTime } from '../types';
|
import type { eventWithTime } from '../types';
|
||||||
|
|
||||||
export type PackFn = (event: eventWithTime) => string;
|
export type PackFn = (event: eventWithTime) => string;
|
||||||
export type UnpackFn = (raw: string) => eventWithTime;
|
export type UnpackFn = (raw: string) => eventWithTime;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { strFromU8, strToU8, unzlibSync } from 'fflate';
|
import { strFromU8, strToU8, unzlibSync } from 'fflate';
|
||||||
import { UnpackFn, eventWithTimeAndPacker, MARK } from './base';
|
import { UnpackFn, eventWithTimeAndPacker, MARK } from './base';
|
||||||
import { eventWithTime } from '../types';
|
import type { eventWithTime } from '../types';
|
||||||
|
|
||||||
export const unpack: UnpackFn = (raw: string) => {
|
export const unpack: UnpackFn = (raw: string) => {
|
||||||
if (typeof raw !== 'string') {
|
if (typeof raw !== 'string') {
|
||||||
@@ -16,7 +16,7 @@ export const unpack: UnpackFn = (raw: string) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const e: eventWithTimeAndPacker = JSON.parse(
|
const e: eventWithTimeAndPacker = JSON.parse(
|
||||||
strFromU8(unzlibSync(strToU8(raw, true)))
|
strFromU8(unzlibSync(strToU8(raw, true))),
|
||||||
);
|
);
|
||||||
if (e.v === MARK) {
|
if (e.v === MARK) {
|
||||||
return e;
|
return e;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { listenerHandler, RecordPlugin, IWindow } from '../../../types';
|
import type { listenerHandler, RecordPlugin, IWindow } from '../../../types';
|
||||||
import { patch } from '../../../utils';
|
import { patch } from '../../../utils';
|
||||||
import { ErrorStackParser, StackFrame } from './error-stack-parser';
|
import { ErrorStackParser, StackFrame } from './error-stack-parser';
|
||||||
import { stringify } from './stringify';
|
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
|
* transfer the node path in Event to string
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RecordPlugin } from '../../../types';
|
import type { RecordPlugin } from '../../../types';
|
||||||
|
|
||||||
export type SequentialIdOptions = {
|
export type SequentialIdOptions = {
|
||||||
key: string;
|
key: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { SequentialIdOptions } from '../record';
|
import type { SequentialIdOptions } from '../record';
|
||||||
import { ReplayPlugin, eventWithTime } from '../../../types';
|
import type { ReplayPlugin, eventWithTime } from '../../../types';
|
||||||
|
|
||||||
type Options = SequentialIdOptions & {
|
type Options = SequentialIdOptions & {
|
||||||
warnOnMissingId: boolean;
|
warnOnMissingId: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
||||||
import { mutationCallBack } from '../types';
|
import type { mutationCallBack } from '../types';
|
||||||
|
|
||||||
export class IframeManager {
|
export class IframeManager {
|
||||||
private iframes: WeakMap<HTMLIFrameElement, true> = new WeakMap();
|
private iframes: WeakMap<HTMLIFrameElement, true> = new WeakMap();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
maskInputValue,
|
maskInputValue,
|
||||||
Mirror,
|
Mirror,
|
||||||
} from 'rrweb-snapshot';
|
} from 'rrweb-snapshot';
|
||||||
import {
|
import type {
|
||||||
mutationRecord,
|
mutationRecord,
|
||||||
textCursor,
|
textCursor,
|
||||||
attributeCursor,
|
attributeCursor,
|
||||||
@@ -298,7 +298,7 @@ export default class MutationBuffer {
|
|||||||
inlineImages: this.inlineImages,
|
inlineImages: this.inlineImages,
|
||||||
onSerialize: (currentN) => {
|
onSerialize: (currentN) => {
|
||||||
if (isSerializedIframe(currentN, this.mirror)) {
|
if (isSerializedIframe(currentN, this.mirror)) {
|
||||||
this.iframeManager.addIframe(currentN);
|
this.iframeManager.addIframe(currentN as HTMLIFrameElement);
|
||||||
}
|
}
|
||||||
if (hasShadowRoot(n)) {
|
if (hasShadowRoot(n)) {
|
||||||
this.shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
this.shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
||||||
@@ -322,7 +322,7 @@ export default class MutationBuffer {
|
|||||||
this.mirror.removeNodeFromMap(this.mapRemoves.shift()!);
|
this.mirror.removeNodeFromMap(this.mapRemoves.shift()!);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const n of this.movedSet) {
|
for (const n of Array.from(this.movedSet.values())) {
|
||||||
if (
|
if (
|
||||||
isParentRemoved(this.removes, n, this.mirror) &&
|
isParentRemoved(this.removes, n, this.mirror) &&
|
||||||
!this.movedSet.has(n.parentNode!)
|
!this.movedSet.has(n.parentNode!)
|
||||||
@@ -332,7 +332,7 @@ export default class MutationBuffer {
|
|||||||
pushAdd(n);
|
pushAdd(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const n of this.addedSet) {
|
for (const n of Array.from(this.addedSet.values())) {
|
||||||
if (
|
if (
|
||||||
!isAncestorInSet(this.droppedSet, n) &&
|
!isAncestorInSet(this.droppedSet, n) &&
|
||||||
!isParentRemoved(this.removes, n, this.mirror)
|
!isParentRemoved(this.removes, n, this.mirror)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MaskInputOptions, maskInputValue } from 'rrweb-snapshot';
|
import { MaskInputOptions, maskInputValue } from 'rrweb-snapshot';
|
||||||
import { FontFaceSet } from 'css-font-loading-module';
|
import type { FontFaceSet } from 'css-font-loading-module';
|
||||||
import {
|
import {
|
||||||
throttle,
|
throttle,
|
||||||
on,
|
on,
|
||||||
@@ -108,9 +108,9 @@ export function initMutationObserver(
|
|||||||
typeof MutationObserver
|
typeof MutationObserver
|
||||||
>)[angularZoneSymbol];
|
>)[angularZoneSymbol];
|
||||||
}
|
}
|
||||||
const observer = new mutationObserverCtor(
|
const observer = new (mutationObserverCtor as new (
|
||||||
mutationBuffer.processMutations.bind(mutationBuffer),
|
callback: MutationCallback,
|
||||||
);
|
) => MutationObserver)(mutationBuffer.processMutations.bind(mutationBuffer));
|
||||||
observer.observe(rootEl, {
|
observer.observe(rootEl, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeOldValue: true,
|
attributeOldValue: true,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Mirror } from 'rrweb-snapshot';
|
import type { Mirror } from 'rrweb-snapshot';
|
||||||
import {
|
import {
|
||||||
blockClass,
|
blockClass,
|
||||||
CanvasContext,
|
CanvasContext,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ICanvas, Mirror } from 'rrweb-snapshot';
|
import type { ICanvas, Mirror } from 'rrweb-snapshot';
|
||||||
import {
|
import type {
|
||||||
blockClass,
|
blockClass,
|
||||||
CanvasContext,
|
|
||||||
canvasManagerMutationCallback,
|
canvasManagerMutationCallback,
|
||||||
canvasMutationCallback,
|
canvasMutationCallback,
|
||||||
canvasMutationCommand,
|
canvasMutationCommand,
|
||||||
@@ -10,11 +9,12 @@ import {
|
|||||||
listenerHandler,
|
listenerHandler,
|
||||||
CanvasArg,
|
CanvasArg,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
|
import { CanvasContext } from '../../../types';
|
||||||
import initCanvas2DMutationObserver from './2d';
|
import initCanvas2DMutationObserver from './2d';
|
||||||
import initCanvasContextObserver from './canvas';
|
import initCanvasContextObserver from './canvas';
|
||||||
import initCanvasWebGLMutationObserver from './webgl';
|
import initCanvasWebGLMutationObserver from './webgl';
|
||||||
import ImageBitmapDataURLWorker from 'web-worker:../../workers/image-bitmap-data-url-worker.ts';
|
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 };
|
export type RafStamps = { latestId: number; invokeId: number | null };
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ICanvas } from 'rrweb-snapshot';
|
import type { ICanvas } from 'rrweb-snapshot';
|
||||||
import { blockClass, IWindow, listenerHandler } from '../../../types';
|
import type { blockClass, IWindow, listenerHandler } from '../../../types';
|
||||||
import { isBlocked, patch } from '../../../utils';
|
import { isBlocked, patch } from '../../../utils';
|
||||||
|
|
||||||
export default function initCanvasContextObserver(
|
export default function initCanvasContextObserver(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { encode } from 'base64-arraybuffer';
|
import { encode } from 'base64-arraybuffer';
|
||||||
import { IWindow, CanvasArg } from '../../../types';
|
import type { IWindow, CanvasArg } from '../../../types';
|
||||||
|
|
||||||
// TODO: unify with `replay/webgl.ts`
|
// TODO: unify with `replay/webgl.ts`
|
||||||
type CanvasVarMap = Map<string, any[]>;
|
type CanvasVarMap = Map<string, any[]>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Mirror } from 'rrweb-snapshot';
|
import type { Mirror } from 'rrweb-snapshot';
|
||||||
import {
|
import {
|
||||||
blockClass,
|
blockClass,
|
||||||
CanvasContext,
|
CanvasContext,
|
||||||
@@ -31,8 +31,8 @@ function patchGLPrototype(
|
|||||||
return function (this: typeof prototype, ...args: Array<unknown>) {
|
return function (this: typeof prototype, ...args: Array<unknown>) {
|
||||||
const result = original.apply(this, args);
|
const result = original.apply(this, args);
|
||||||
saveWebGLVar(result, win, prototype);
|
saveWebGLVar(result, win, prototype);
|
||||||
if (!isBlocked(this.canvas, blockClass)) {
|
if (!isBlocked(this.canvas as HTMLCanvasElement, blockClass)) {
|
||||||
const id = mirror.getId(this.canvas);
|
const id = mirror.getId(this.canvas as HTMLCanvasElement);
|
||||||
|
|
||||||
const recordArgs = serializeArgs([...args], win, prototype);
|
const recordArgs = serializeArgs([...args], win, prototype);
|
||||||
const mutation: canvasMutationWithType = {
|
const mutation: canvasMutationWithType = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {
|
import type {
|
||||||
mutationCallBack,
|
mutationCallBack,
|
||||||
scrollCallback,
|
scrollCallback,
|
||||||
MutationBufferParam,
|
MutationBufferParam,
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
import { initMutationObserver, initScrollObserver } from './observer';
|
import { initMutationObserver, initScrollObserver } from './observer';
|
||||||
import { patch } from '../utils';
|
import { patch } from '../utils';
|
||||||
import { Mirror } from 'rrweb-snapshot';
|
import type { Mirror } from 'rrweb-snapshot';
|
||||||
|
|
||||||
type BypassOptions = Omit<
|
type BypassOptions = Omit<
|
||||||
MutationBufferParam,
|
MutationBufferParam,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { encode } from 'base64-arraybuffer';
|
import { encode } from 'base64-arraybuffer';
|
||||||
import {
|
import type {
|
||||||
ImageBitmapDataURLWorkerParams,
|
ImageBitmapDataURLWorkerParams,
|
||||||
ImageBitmapDataURLWorkerResponse,
|
ImageBitmapDataURLWorkerResponse,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Replayer } from '../';
|
import type { Replayer } from '../';
|
||||||
import { canvasMutationCommand } from '../../types';
|
import type { canvasMutationCommand } from '../../types';
|
||||||
import { deserializeArg } from './deserialize-args';
|
import { deserializeArg } from './deserialize-args';
|
||||||
|
|
||||||
export default async function canvasMutation({
|
export default async function canvasMutation({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { decode } from 'base64-arraybuffer';
|
import { decode } from 'base64-arraybuffer';
|
||||||
import type { Replayer } from '../';
|
import type { Replayer } from '../';
|
||||||
import { CanvasArg, SerializedCanvasArg } from '../../types';
|
import type { CanvasArg, SerializedCanvasArg } from '../../types';
|
||||||
|
|
||||||
// TODO: add ability to wipe this list
|
// TODO: add ability to wipe this list
|
||||||
type GLVarMap = Map<string, any[]>;
|
type GLVarMap = Map<string, any[]>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Replayer } from '..';
|
import type { Replayer } from '..';
|
||||||
import {
|
import {
|
||||||
CanvasContext,
|
CanvasContext,
|
||||||
canvasMutationCommand,
|
canvasMutationCommand,
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ function getContext(
|
|||||||
// you might have to do `ctx.flush()` before every webgl canvas event
|
// you might have to do `ctx.flush()` before every webgl canvas event
|
||||||
try {
|
try {
|
||||||
if (type === CanvasContext.WebGL) {
|
if (type === CanvasContext.WebGL) {
|
||||||
return (
|
return (target.getContext('webgl')! ||
|
||||||
target.getContext('webgl')! || target.getContext('experimental-webgl')
|
target.getContext('experimental-webgl')) as WebGLRenderingContext;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return target.getContext('webgl2')!;
|
return target.getContext('webgl2')!;
|
||||||
} catch (e) {
|
} 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,
|
serializedNodeWithId,
|
||||||
Mirror,
|
Mirror,
|
||||||
INode,
|
INode,
|
||||||
@@ -7,11 +7,12 @@ import {
|
|||||||
MaskInputFn,
|
MaskInputFn,
|
||||||
MaskTextFn,
|
MaskTextFn,
|
||||||
} from 'rrweb-snapshot';
|
} from 'rrweb-snapshot';
|
||||||
import { PackFn, UnpackFn } from './packer/base';
|
import type { PackFn, UnpackFn } from './packer/base';
|
||||||
import { IframeManager } from './record/iframe-manager';
|
import type { IframeManager } from './record/iframe-manager';
|
||||||
import { ShadowDomManager } from './record/shadow-dom-manager';
|
import type { ShadowDomManager } from './record/shadow-dom-manager';
|
||||||
import type { Replayer } from './replay';
|
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 {
|
export enum EventType {
|
||||||
DomContentLoaded,
|
DomContentLoaded,
|
||||||
@@ -169,6 +170,11 @@ export type eventWithTime = event & {
|
|||||||
delay?: number;
|
delay?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type canvasEventWithTime = eventWithTime & {
|
||||||
|
type: EventType.IncrementalSnapshot;
|
||||||
|
data: canvasMutationData;
|
||||||
|
};
|
||||||
|
|
||||||
export type blockClass = string | RegExp;
|
export type blockClass = string | RegExp;
|
||||||
|
|
||||||
export type maskTextClass = string | RegExp;
|
export type maskTextClass = string | RegExp;
|
||||||
@@ -653,6 +659,7 @@ export type playerConfig = {
|
|||||||
strokeStyle?: string;
|
strokeStyle?: string;
|
||||||
};
|
};
|
||||||
unpackFn?: UnpackFn;
|
unpackFn?: UnpackFn;
|
||||||
|
useVirtualDom: boolean;
|
||||||
plugins?: ReplayPlugin[];
|
plugins?: ReplayPlugin[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -663,7 +670,7 @@ export type playerMetaData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type missingNode = {
|
export type missingNode = {
|
||||||
node: Node;
|
node: Node | RRNode;
|
||||||
mutation: addedNodeMutation;
|
mutation: addedNodeMutation;
|
||||||
};
|
};
|
||||||
export type missingNodeMap = {
|
export type missingNodeMap = {
|
||||||
@@ -706,12 +713,6 @@ export enum ReplayerEvents {
|
|||||||
PlayBack = 'play-back',
|
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;
|
export type KeepIframeSrcFn = (src: string) => boolean;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import {
|
import type {
|
||||||
throttleOptions,
|
throttleOptions,
|
||||||
listenerHandler,
|
listenerHandler,
|
||||||
hookResetter,
|
hookResetter,
|
||||||
blockClass,
|
blockClass,
|
||||||
IncrementalSource,
|
|
||||||
addedNodeMutation,
|
addedNodeMutation,
|
||||||
removedNodeMutation,
|
|
||||||
textMutation,
|
|
||||||
attributeMutation,
|
|
||||||
mutationData,
|
|
||||||
scrollData,
|
|
||||||
inputData,
|
|
||||||
DocumentDimension,
|
DocumentDimension,
|
||||||
IWindow,
|
IWindow,
|
||||||
DeprecatedMirror,
|
DeprecatedMirror,
|
||||||
|
textMutation,
|
||||||
} from './types';
|
} 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(
|
export function on(
|
||||||
type: string,
|
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 = {
|
type ResolveTree = {
|
||||||
value: addedNodeMutation;
|
value: addedNodeMutation;
|
||||||
children: ResolveTree[];
|
children: ResolveTree[];
|
||||||
@@ -542,13 +343,13 @@ export function iterateResolveTree(
|
|||||||
|
|
||||||
export type AppendedIframe = {
|
export type AppendedIframe = {
|
||||||
mutationInQueue: addedNodeMutation;
|
mutationInQueue: addedNodeMutation;
|
||||||
builtNode: HTMLIFrameElement;
|
builtNode: HTMLIFrameElement | RRIFrameElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isSerializedIframe(
|
export function isSerializedIframe<TNode extends Node | RRNode>(
|
||||||
n: Node,
|
n: TNode,
|
||||||
mirror: Mirror,
|
mirror: IMirror<TNode>,
|
||||||
): n is HTMLIFrameElement {
|
): boolean {
|
||||||
return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n));
|
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: T,
|
||||||
): n is T & { shadowRoot: ShadowRoot } {
|
): n is T & { shadowRoot: ShadowRoot } {
|
||||||
return Boolean(((n as unknown) as Element)?.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.
|
* Returns the latest mutation in the queue for each node.
|
||||||
* @param {textMutation[]} mutations The text mutations to filter.
|
* @param {textMutation[]} mutations The text mutations to filter.
|
||||||
|
|||||||
@@ -8362,7 +8362,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"assert\\",
|
\\"level\\": \\"assert\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:2:37\\"
|
\\"__puppeteer_evaluation_script__:2:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"true\\",
|
\\"true\\",
|
||||||
@@ -8378,7 +8378,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"count\\",
|
\\"level\\": \\"count\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:3:37\\"
|
\\"__puppeteer_evaluation_script__:3:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"\\\\\\"count\\\\\\"\\"
|
\\"\\\\\\"count\\\\\\"\\"
|
||||||
@@ -8393,7 +8393,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"countReset\\",
|
\\"level\\": \\"countReset\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:4:37\\"
|
\\"__puppeteer_evaluation_script__:4:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"\\\\\\"count\\\\\\"\\"
|
\\"\\\\\\"count\\\\\\"\\"
|
||||||
@@ -8408,7 +8408,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"debug\\",
|
\\"level\\": \\"debug\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:5:37\\"
|
\\"__puppeteer_evaluation_script__:5:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"\\\\\\"debug\\\\\\"\\"
|
\\"\\\\\\"debug\\\\\\"\\"
|
||||||
@@ -8423,7 +8423,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"dir\\",
|
\\"level\\": \\"dir\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:6:37\\"
|
\\"__puppeteer_evaluation_script__:6:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"\\\\\\"dir\\\\\\"\\"
|
\\"\\\\\\"dir\\\\\\"\\"
|
||||||
@@ -8438,7 +8438,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"dirxml\\",
|
\\"level\\": \\"dirxml\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:7:37\\"
|
\\"__puppeteer_evaluation_script__:7:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"\\\\\\"dirxml\\\\\\"\\"
|
\\"\\\\\\"dirxml\\\\\\"\\"
|
||||||
@@ -8453,7 +8453,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"group\\",
|
\\"level\\": \\"group\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:8:37\\"
|
\\"__puppeteer_evaluation_script__:8:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": []
|
\\"payload\\": []
|
||||||
}
|
}
|
||||||
@@ -8466,7 +8466,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"groupCollapsed\\",
|
\\"level\\": \\"groupCollapsed\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:9:37\\"
|
\\"__puppeteer_evaluation_script__:9:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": []
|
\\"payload\\": []
|
||||||
}
|
}
|
||||||
@@ -8479,7 +8479,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"info\\",
|
\\"level\\": \\"info\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:10:37\\"
|
\\"__puppeteer_evaluation_script__:10:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"\\\\\\"info\\\\\\"\\"
|
\\"\\\\\\"info\\\\\\"\\"
|
||||||
@@ -8494,7 +8494,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"log\\",
|
\\"level\\": \\"log\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:11:37\\"
|
\\"__puppeteer_evaluation_script__:11:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"\\\\\\"log\\\\\\"\\"
|
\\"\\\\\\"log\\\\\\"\\"
|
||||||
@@ -8509,7 +8509,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"table\\",
|
\\"level\\": \\"table\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:12:37\\"
|
\\"__puppeteer_evaluation_script__:12:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"\\\\\\"table\\\\\\"\\"
|
\\"\\\\\\"table\\\\\\"\\"
|
||||||
@@ -8524,7 +8524,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"time\\",
|
\\"level\\": \\"time\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:13:37\\"
|
\\"__puppeteer_evaluation_script__:13:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": []
|
\\"payload\\": []
|
||||||
}
|
}
|
||||||
@@ -8537,7 +8537,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"timeEnd\\",
|
\\"level\\": \\"timeEnd\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:14:37\\"
|
\\"__puppeteer_evaluation_script__:14:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": []
|
\\"payload\\": []
|
||||||
}
|
}
|
||||||
@@ -8550,7 +8550,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"timeLog\\",
|
\\"level\\": \\"timeLog\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:15:37\\"
|
\\"__puppeteer_evaluation_script__:15:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": []
|
\\"payload\\": []
|
||||||
}
|
}
|
||||||
@@ -8563,7 +8563,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"trace\\",
|
\\"level\\": \\"trace\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:16:37\\"
|
\\"__puppeteer_evaluation_script__:16:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"\\\\\\"trace\\\\\\"\\"
|
\\"\\\\\\"trace\\\\\\"\\"
|
||||||
@@ -8578,7 +8578,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"warn\\",
|
\\"level\\": \\"warn\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:17:37\\"
|
\\"__puppeteer_evaluation_script__:17:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"payload\\": [
|
||||||
\\"\\\\\\"warn\\\\\\"\\"
|
\\"\\\\\\"warn\\\\\\"\\"
|
||||||
@@ -8593,7 +8593,7 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"clear\\",
|
\\"level\\": \\"clear\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:18:37\\"
|
\\"__puppeteer_evaluation_script__:18:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": []
|
\\"payload\\": []
|
||||||
}
|
}
|
||||||
@@ -8606,10 +8606,10 @@ exports[`record integration tests should record console messages 1`] = `
|
|||||||
\\"payload\\": {
|
\\"payload\\": {
|
||||||
\\"level\\": \\"log\\",
|
\\"level\\": \\"log\\",
|
||||||
\\"trace\\": [
|
\\"trace\\": [
|
||||||
\\"__puppeteer_evaluation_script__:19:37\\"
|
\\"__puppeteer_evaluation_script__:19:21\\"
|
||||||
],
|
],
|
||||||
\\"payload\\": [
|
\\"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
|
file-cid-1
|
||||||
@charset \\"utf-8\\";
|
@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
|
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-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
|
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 fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as puppeteer from 'puppeteer';
|
import type * as puppeteer from 'puppeteer';
|
||||||
import {
|
import {
|
||||||
startServer,
|
startServer,
|
||||||
launchPuppeteer,
|
launchPuppeteer,
|
||||||
@@ -9,12 +9,7 @@ import {
|
|||||||
replaceLast,
|
replaceLast,
|
||||||
waitForRAF,
|
waitForRAF,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import {
|
import type { recordOptions, eventWithTime } from '../../src/types';
|
||||||
recordOptions,
|
|
||||||
eventWithTime,
|
|
||||||
EventType,
|
|
||||||
IncrementalSource,
|
|
||||||
} from '../../src/types';
|
|
||||||
import { toMatchImageSnapshot } from 'jest-image-snapshot';
|
import { toMatchImageSnapshot } from 'jest-image-snapshot';
|
||||||
expect.extend({ toMatchImageSnapshot });
|
expect.extend({ toMatchImageSnapshot });
|
||||||
|
|
||||||
|
|||||||
@@ -486,6 +486,30 @@ const events: eventWithTime[] = [
|
|||||||
timestamp: now + 1500,
|
timestamp: now + 1500,
|
||||||
},
|
},
|
||||||
// add iframe five
|
// 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,
|
type: EventType.IncrementalSnapshot,
|
||||||
data: {
|
data: {
|
||||||
@@ -550,30 +574,6 @@ const events: eventWithTime[] = [
|
|||||||
},
|
},
|
||||||
timestamp: now + 2000,
|
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
|
// remove the html element of iframe four
|
||||||
{
|
{
|
||||||
type: EventType.IncrementalSnapshot,
|
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,
|
type: EventType.FullSnapshot,
|
||||||
timestamp: now + 100,
|
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
|
// mutation that adds stylesheet
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
@@ -142,7 +126,7 @@ const events: eventWithTime[] = [
|
|||||||
type: 3,
|
type: 3,
|
||||||
isStyle: true,
|
isStyle: true,
|
||||||
textContent:
|
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,
|
nextId: null,
|
||||||
parentId: 255,
|
parentId: 255,
|
||||||
@@ -154,6 +138,22 @@ const events: eventWithTime[] = [
|
|||||||
attributes: [],
|
attributes: [],
|
||||||
},
|
},
|
||||||
type: EventType.IncrementalSnapshot,
|
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,
|
timestamp: now + 500,
|
||||||
},
|
},
|
||||||
// adds StyleSheetRule
|
// 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 fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as http from 'http';
|
import type * as http from 'http';
|
||||||
import * as puppeteer from 'puppeteer';
|
import type * as puppeteer from 'puppeteer';
|
||||||
import {
|
import {
|
||||||
assertSnapshot,
|
assertSnapshot,
|
||||||
startServer,
|
startServer,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as puppeteer from 'puppeteer';
|
import type * as puppeteer from 'puppeteer';
|
||||||
import {
|
import {
|
||||||
recordOptions,
|
recordOptions,
|
||||||
listenerHandler,
|
listenerHandler,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as puppeteer from 'puppeteer';
|
import type * as puppeteer from 'puppeteer';
|
||||||
import {
|
import {
|
||||||
recordOptions,
|
recordOptions,
|
||||||
listenerHandler,
|
listenerHandler,
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
stripBase64,
|
stripBase64,
|
||||||
waitForRAF,
|
waitForRAF,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { ICanvas } from 'rrweb-snapshot';
|
import type { ICanvas } from 'rrweb-snapshot';
|
||||||
|
|
||||||
interface ISuite {
|
interface ISuite {
|
||||||
code: string;
|
code: string;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { polyfillWebGLGlobals } from '../utils';
|
|||||||
polyfillWebGLGlobals();
|
polyfillWebGLGlobals();
|
||||||
|
|
||||||
import { Replayer } from '../../src/replay';
|
import { Replayer } from '../../src/replay';
|
||||||
import {} from '../../src/types';
|
|
||||||
import {
|
import {
|
||||||
CanvasContext,
|
CanvasContext,
|
||||||
CanvasArg,
|
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 fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { assertDomSnapshot, launchPuppeteer } from '../utils';
|
import { launchPuppeteer } from '../utils';
|
||||||
import { toMatchImageSnapshot } from 'jest-image-snapshot';
|
import { toMatchImageSnapshot } from 'jest-image-snapshot';
|
||||||
import * as puppeteer from 'puppeteer';
|
import type * as puppeteer from 'puppeteer';
|
||||||
import events from '../events/webgl';
|
import events from '../events/webgl';
|
||||||
|
|
||||||
interface ISuite {
|
interface ISuite {
|
||||||
|
|||||||
@@ -2,16 +2,21 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as puppeteer from 'puppeteer';
|
import type * as puppeteer from 'puppeteer';
|
||||||
import {
|
import {
|
||||||
assertDomSnapshot,
|
assertDomSnapshot,
|
||||||
launchPuppeteer,
|
launchPuppeteer,
|
||||||
sampleEvents as events,
|
sampleEvents as events,
|
||||||
sampleStyleSheetRemoveEvents as stylesheetRemoveEvents,
|
sampleStyleSheetRemoveEvents as stylesheetRemoveEvents,
|
||||||
|
waitForRAF,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import styleSheetRuleEvents from './events/style-sheet-rule-events';
|
import styleSheetRuleEvents from './events/style-sheet-rule-events';
|
||||||
import orderingEvents from './events/ordering';
|
import orderingEvents from './events/ordering';
|
||||||
|
import scrollEvents from './events/scroll';
|
||||||
|
import inputEvents from './events/input';
|
||||||
import iframeEvents from './events/iframe';
|
import iframeEvents from './events/iframe';
|
||||||
|
import shadowDomEvents from './events/shadow-dom';
|
||||||
|
import StyleSheetTextMutation from './events/style-sheet-text-mutation';
|
||||||
|
|
||||||
interface ISuite {
|
interface ISuite {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -247,12 +252,206 @@ describe('replayer', function () {
|
|||||||
const rules = [...replayer.iframe.contentDocument.styleSheets].map(
|
const rules = [...replayer.iframe.contentDocument.styleSheets].map(
|
||||||
(sheet) => [...sheet.rules],
|
(sheet) => [...sheet.rules],
|
||||||
).flat();
|
).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);
|
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 () => {
|
it('can fast-forward mutation events containing nested iframe elements', async () => {
|
||||||
await page.evaluate(`
|
await page.evaluate(`
|
||||||
events = ${JSON.stringify(iframeEvents)};
|
events = ${JSON.stringify(iframeEvents)};
|
||||||
@@ -264,13 +463,12 @@ describe('replayer', function () {
|
|||||||
const contentDocument = await iframe!.contentFrame()!;
|
const contentDocument = await iframe!.contentFrame()!;
|
||||||
expect(await contentDocument!.$('iframe')).toBeNull();
|
expect(await contentDocument!.$('iframe')).toBeNull();
|
||||||
|
|
||||||
const delay = 50;
|
|
||||||
// restart the replayer
|
// restart the replayer
|
||||||
await page.evaluate('replayer.play(0);');
|
await page.evaluate('replayer.play(0);');
|
||||||
await page.waitForTimeout(delay);
|
await waitForRAF(page);
|
||||||
await page.evaluate('replayer.pause(550);'); // add 'iframe one' at 500
|
await page.evaluate('replayer.pause(550);'); // add 'iframe one' at 500
|
||||||
expect(await contentDocument!.$('iframe')).not.toBeNull();
|
expect(await contentDocument!.$('iframe')).not.toBeNull();
|
||||||
const iframeOneDocument = await (await contentDocument!.$(
|
let iframeOneDocument = await (await contentDocument!.$(
|
||||||
'iframe',
|
'iframe',
|
||||||
))!.contentFrame();
|
))!.contentFrame();
|
||||||
expect(iframeOneDocument).not.toBeNull();
|
expect(iframeOneDocument).not.toBeNull();
|
||||||
@@ -286,14 +484,21 @@ describe('replayer', function () {
|
|||||||
|
|
||||||
// add 'iframe two' and 'iframe three' at 1000
|
// add 'iframe two' and 'iframe three' at 1000
|
||||||
await page.evaluate('replayer.play(0);');
|
await page.evaluate('replayer.play(0);');
|
||||||
await page.waitForTimeout(delay);
|
await waitForRAF(page);
|
||||||
await page.evaluate('replayer.pause(1050);');
|
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);
|
expect((await contentDocument!.$$('iframe')).length).toEqual(2);
|
||||||
let iframeTwoDocument = await (
|
let iframeTwoDocument = await (
|
||||||
await contentDocument!.$$('iframe')
|
await contentDocument!.$$('iframe')
|
||||||
)[1]!.contentFrame();
|
)[1]!.contentFrame();
|
||||||
expect(iframeTwoDocument).not.toBeNull();
|
expect(iframeTwoDocument).not.toBeNull();
|
||||||
expect((await iframeTwoDocument!.$$('iframe')).length).toEqual(2);
|
expect((await iframeTwoDocument!.$$('iframe')).length).toEqual(2);
|
||||||
|
expect((await iframeTwoDocument!.$$('style')).length).toBe(1);
|
||||||
let iframeThreeDocument = await (
|
let iframeThreeDocument = await (
|
||||||
await iframeTwoDocument!.$$('iframe')
|
await iframeTwoDocument!.$$('iframe')
|
||||||
)[0]!.contentFrame();
|
)[0]!.contentFrame();
|
||||||
@@ -301,25 +506,27 @@ describe('replayer', function () {
|
|||||||
await iframeTwoDocument!.$$('iframe')
|
await iframeTwoDocument!.$$('iframe')
|
||||||
)[1]!.contentFrame();
|
)[1]!.contentFrame();
|
||||||
expect(iframeThreeDocument).not.toBeNull();
|
expect(iframeThreeDocument).not.toBeNull();
|
||||||
|
expect((await iframeThreeDocument!.$$('style')).length).toBe(1);
|
||||||
expect(iframeFourDocument).not.toBeNull();
|
expect(iframeFourDocument).not.toBeNull();
|
||||||
|
|
||||||
// add 'iframe four' at 1500
|
// add 'iframe four' at 1500
|
||||||
await page.evaluate('replayer.play(0);');
|
await page.evaluate('replayer.play(0);');
|
||||||
await page.waitForTimeout(delay);
|
await waitForRAF(page);
|
||||||
await page.evaluate('replayer.pause(1550);');
|
await page.evaluate('replayer.pause(1550);');
|
||||||
iframeTwoDocument = await (
|
iframeTwoDocument = await (
|
||||||
await contentDocument!.$$('iframe')
|
await contentDocument!.$$('iframe')
|
||||||
)[1]!.contentFrame();
|
)[1]!.contentFrame();
|
||||||
|
expect((await iframeTwoDocument!.$$('style')).length).toBe(1);
|
||||||
iframeFourDocument = await (
|
iframeFourDocument = await (
|
||||||
await iframeTwoDocument!.$$('iframe')
|
await iframeTwoDocument!.$$('iframe')
|
||||||
)[1]!.contentFrame();
|
)[1]!.contentFrame();
|
||||||
expect(await iframeFourDocument!.$('iframe')).toBeNull();
|
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');
|
expect(await iframeFourDocument!.title()).toEqual('iframe 4');
|
||||||
|
|
||||||
// add 'iframe five' at 2000
|
// add 'iframe five' at 2000
|
||||||
await page.evaluate('replayer.play(0);');
|
await page.evaluate('replayer.play(0);');
|
||||||
await page.waitForTimeout(delay);
|
await waitForRAF(page);
|
||||||
await page.evaluate('replayer.pause(2050);');
|
await page.evaluate('replayer.pause(2050);');
|
||||||
iframeTwoDocument = await (
|
iframeTwoDocument = await (
|
||||||
await contentDocument!.$$('iframe')
|
await contentDocument!.$$('iframe')
|
||||||
@@ -327,6 +534,7 @@ describe('replayer', function () {
|
|||||||
iframeFourDocument = await (
|
iframeFourDocument = await (
|
||||||
await iframeTwoDocument!.$$('iframe')
|
await iframeTwoDocument!.$$('iframe')
|
||||||
)[1]!.contentFrame();
|
)[1]!.contentFrame();
|
||||||
|
expect((await iframeFourDocument!.$$('style')).length).toBe(1);
|
||||||
expect(await iframeFourDocument!.$('iframe')).not.toBeNull();
|
expect(await iframeFourDocument!.$('iframe')).not.toBeNull();
|
||||||
const iframeFiveDocument = await (await iframeFourDocument!.$(
|
const iframeFiveDocument = await (await iframeFourDocument!.$(
|
||||||
'iframe',
|
'iframe',
|
||||||
@@ -343,7 +551,7 @@ describe('replayer', function () {
|
|||||||
|
|
||||||
// remove the html element of 'iframe four' at 2500
|
// remove the html element of 'iframe four' at 2500
|
||||||
await page.evaluate('replayer.play(0);');
|
await page.evaluate('replayer.play(0);');
|
||||||
await page.waitForTimeout(delay);
|
await waitForRAF(page);
|
||||||
await page.evaluate('replayer.pause(2550);');
|
await page.evaluate('replayer.pause(2550);');
|
||||||
iframeTwoDocument = await (
|
iframeTwoDocument = await (
|
||||||
await contentDocument!.$$('iframe')
|
await contentDocument!.$$('iframe')
|
||||||
@@ -362,6 +570,51 @@ describe('replayer', function () {
|
|||||||
).not.toBeNull();
|
).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 () => {
|
it('can stream events in live mode', async () => {
|
||||||
const status = await page.evaluate(`
|
const status = await page.evaluate(`
|
||||||
const { Replayer } = rrweb;
|
const { Replayer } = rrweb;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"target": "ES5",
|
"target": "ES6",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "build",
|
"outDir": "build",
|
||||||
"lib": ["es6", "dom"],
|
"lib": ["es6", "dom"],
|
||||||
"downlevelIteration": true
|
"downlevelIteration": true,
|
||||||
|
"importsNotUsedAsValues": "error"
|
||||||
},
|
},
|
||||||
"exclude": ["test"],
|
"exclude": ["test"],
|
||||||
"include": [
|
"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 PackFn = (event: eventWithTime) => string;
|
||||||
export declare type UnpackFn = (raw: string) => eventWithTime;
|
export declare type UnpackFn = (raw: string) => eventWithTime;
|
||||||
export declare type eventWithTimeAndPacker = eventWithTime & {
|
export declare type eventWithTimeAndPacker = eventWithTime & {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RecordPlugin } from '../../../types';
|
import type { RecordPlugin } from '../../../types';
|
||||||
export declare type StringifyOptions = {
|
export declare type StringifyOptions = {
|
||||||
stringLengthLimit?: number;
|
stringLengthLimit?: number;
|
||||||
numOfKeysLimit: 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;
|
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 = {
|
export declare type SequentialIdOptions = {
|
||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { SequentialIdOptions } from '../record';
|
import type { SequentialIdOptions } from '../record';
|
||||||
import { ReplayPlugin } from '../../../types';
|
import type { ReplayPlugin } from '../../../types';
|
||||||
declare type Options = SequentialIdOptions & {
|
declare type Options = SequentialIdOptions & {
|
||||||
warnOnMissingId: boolean;
|
warnOnMissingId: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
||||||
import { mutationCallBack } from '../types';
|
import type { mutationCallBack } from '../types';
|
||||||
export declare class IframeManager {
|
export declare class IframeManager {
|
||||||
private iframes;
|
private iframes;
|
||||||
private mutationCb;
|
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 {
|
export default class MutationBuffer {
|
||||||
private frozen;
|
private frozen;
|
||||||
private locked;
|
private locked;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { Mirror } from 'rrweb-snapshot';
|
import type { Mirror } from 'rrweb-snapshot';
|
||||||
import { blockClass, canvasManagerMutationCallback, IWindow, listenerHandler } from '../../../types';
|
import { blockClass, canvasManagerMutationCallback, IWindow, listenerHandler } from '../../../types';
|
||||||
export default function initCanvas2DMutationObserver(cb: canvasManagerMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
|
export default function initCanvas2DMutationObserver(cb: canvasManagerMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Mirror } from 'rrweb-snapshot';
|
import type { Mirror } from 'rrweb-snapshot';
|
||||||
import { blockClass, canvasMutationCallback, IWindow } from '../../../types';
|
import type { blockClass, canvasMutationCallback, IWindow } from '../../../types';
|
||||||
export declare type RafStamps = {
|
export declare type RafStamps = {
|
||||||
latestId: number;
|
latestId: number;
|
||||||
invokeId: number | null;
|
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;
|
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 function variableListFor(ctx: RenderingContext, ctor: string): any[];
|
||||||
export declare const saveWebGLVar: (value: any, win: IWindow, ctx: RenderingContext) => number | void;
|
export declare const saveWebGLVar: (value: any, win: IWindow, ctx: RenderingContext) => number | void;
|
||||||
export declare function serializeArg(value: any, win: IWindow, ctx: RenderingContext): CanvasArg;
|
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';
|
import { blockClass, canvasManagerMutationCallback, IWindow, listenerHandler } from '../../../types';
|
||||||
export default function initCanvasWebGLMutationObserver(cb: canvasManagerMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
|
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 type { mutationCallBack, scrollCallback, MutationBufferParam, SamplingStrategy } from '../types';
|
||||||
import { Mirror } from 'rrweb-snapshot';
|
import type { Mirror } from 'rrweb-snapshot';
|
||||||
declare type BypassOptions = Omit<MutationBufferParam, 'doc' | 'mutationCb' | 'mirror' | 'shadowDomManager'> & {
|
declare type BypassOptions = Omit<MutationBufferParam, 'doc' | 'mutationCb' | 'mirror' | 'shadowDomManager'> & {
|
||||||
sampling: SamplingStrategy;
|
sampling: SamplingStrategy;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ImageBitmapDataURLWorkerParams, ImageBitmapDataURLWorkerResponse } from '../../types';
|
import type { ImageBitmapDataURLWorkerParams, ImageBitmapDataURLWorkerResponse } from '../../types';
|
||||||
export interface ImageBitmapDataURLRequestWorker {
|
export interface ImageBitmapDataURLRequestWorker {
|
||||||
postMessage: (message: ImageBitmapDataURLWorkerParams, transfer?: [ImageBitmap]) => void;
|
postMessage: (message: ImageBitmapDataURLWorkerParams, transfer?: [ImageBitmap]) => void;
|
||||||
onmessage: (message: MessageEvent<ImageBitmapDataURLWorkerResponse>) => 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 type { Replayer } from '../';
|
||||||
import { canvasMutationCommand } from '../../types';
|
import type { canvasMutationCommand } from '../../types';
|
||||||
export default function canvasMutation({ event, mutation, target, imageMap, errorHandler, }: {
|
export default function canvasMutation({ event, mutation, target, imageMap, errorHandler, }: {
|
||||||
event: Parameters<Replayer['applyIncremental']>[0];
|
event: Parameters<Replayer['applyIncremental']>[0];
|
||||||
mutation: canvasMutationCommand;
|
mutation: canvasMutationCommand;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Replayer } from '../';
|
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 variableListFor(ctx: CanvasRenderingContext2D | WebGLRenderingContext | WebGL2RenderingContext, ctor: string): any[];
|
||||||
export declare function isSerializedArg(arg: unknown): arg is SerializedCanvasArg;
|
export declare function isSerializedArg(arg: unknown): arg is SerializedCanvasArg;
|
||||||
export declare function deserializeArg(imageMap: Replayer['imageMap'], ctx: CanvasRenderingContext2D | WebGLRenderingContext | WebGL2RenderingContext | null, preload?: {
|
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';
|
import { canvasMutationData } from '../../types';
|
||||||
export default function canvasMutation({ event, mutation, target, imageMap, canvasEventMap, errorHandler, }: {
|
export default function canvasMutation({ event, mutation, target, imageMap, canvasEventMap, errorHandler, }: {
|
||||||
event: Parameters<Replayer['applyIncremental']>[0];
|
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 { Mirror } from 'rrweb-snapshot';
|
||||||
|
import { RRDocument } from 'rrdom/es/virtual-dom';
|
||||||
import { Timer } from './timer';
|
import { Timer } from './timer';
|
||||||
import { createPlayerService, createSpeedService } from './machine';
|
import { createPlayerService, createSpeedService } from './machine';
|
||||||
import { eventWithTime, playerConfig, playerMetaData, Handler } from '../types';
|
import { eventWithTime, playerConfig, playerMetaData, Handler } from '../types';
|
||||||
@@ -10,16 +11,14 @@ export declare class Replayer {
|
|||||||
speedService: ReturnType<typeof createSpeedService>;
|
speedService: ReturnType<typeof createSpeedService>;
|
||||||
get timer(): Timer;
|
get timer(): Timer;
|
||||||
config: playerConfig;
|
config: playerConfig;
|
||||||
|
usingVirtualDom: boolean;
|
||||||
|
virtualDom: RRDocument;
|
||||||
private mouse;
|
private mouse;
|
||||||
private mouseTail;
|
private mouseTail;
|
||||||
private tailPositions;
|
private tailPositions;
|
||||||
private emitter;
|
private emitter;
|
||||||
private nextUserInteractionEvent;
|
private nextUserInteractionEvent;
|
||||||
private legacy_missingNodeRetryMap;
|
private legacy_missingNodeRetryMap;
|
||||||
private treeIndex;
|
|
||||||
private fragmentParentMap;
|
|
||||||
private elementStateMap;
|
|
||||||
private virtualStyleRulesMap;
|
|
||||||
private cache;
|
private cache;
|
||||||
private imageMap;
|
private imageMap;
|
||||||
private canvasEventMap;
|
private canvasEventMap;
|
||||||
@@ -62,17 +61,12 @@ export declare class Replayer {
|
|||||||
private applyMutation;
|
private applyMutation;
|
||||||
private applyScroll;
|
private applyScroll;
|
||||||
private applyInput;
|
private applyInput;
|
||||||
private applyText;
|
|
||||||
private legacy_resolveMissingNode;
|
private legacy_resolveMissingNode;
|
||||||
private moveAndHover;
|
private moveAndHover;
|
||||||
private drawMouseTail;
|
private drawMouseTail;
|
||||||
private hoverElements;
|
private hoverElements;
|
||||||
private isUserInteraction;
|
private isUserInteraction;
|
||||||
private backToNormal;
|
private backToNormal;
|
||||||
private restoreRealParent;
|
|
||||||
private storeState;
|
|
||||||
private restoreState;
|
|
||||||
private restoreNodeSheet;
|
|
||||||
private warnNodeNotFound;
|
private warnNodeNotFound;
|
||||||
private warnCanvasMutationFailed;
|
private warnCanvasMutationFailed;
|
||||||
private debugNodeNotFound;
|
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 type { serializedNodeWithId, Mirror, INode, MaskInputOptions, SlimDOMOptions, MaskInputFn, MaskTextFn } from 'rrweb-snapshot';
|
||||||
import { PackFn, UnpackFn } from './packer/base';
|
import type { PackFn, UnpackFn } from './packer/base';
|
||||||
import { IframeManager } from './record/iframe-manager';
|
import type { IframeManager } from './record/iframe-manager';
|
||||||
import { ShadowDomManager } from './record/shadow-dom-manager';
|
import type { ShadowDomManager } from './record/shadow-dom-manager';
|
||||||
import type { Replayer } from './replay';
|
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 {
|
export declare enum EventType {
|
||||||
DomContentLoaded = 0,
|
DomContentLoaded = 0,
|
||||||
Load = 1,
|
Load = 1,
|
||||||
@@ -115,6 +116,10 @@ export declare type eventWithTime = event & {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
};
|
};
|
||||||
|
export declare type canvasEventWithTime = eventWithTime & {
|
||||||
|
type: EventType.IncrementalSnapshot;
|
||||||
|
data: canvasMutationData;
|
||||||
|
};
|
||||||
export declare type blockClass = string | RegExp;
|
export declare type blockClass = string | RegExp;
|
||||||
export declare type maskTextClass = string | RegExp;
|
export declare type maskTextClass = string | RegExp;
|
||||||
export declare type SamplingStrategy = Partial<{
|
export declare type SamplingStrategy = Partial<{
|
||||||
@@ -464,6 +469,7 @@ export declare type playerConfig = {
|
|||||||
strokeStyle?: string;
|
strokeStyle?: string;
|
||||||
};
|
};
|
||||||
unpackFn?: UnpackFn;
|
unpackFn?: UnpackFn;
|
||||||
|
useVirtualDom: boolean;
|
||||||
plugins?: ReplayPlugin[];
|
plugins?: ReplayPlugin[];
|
||||||
};
|
};
|
||||||
export declare type playerMetaData = {
|
export declare type playerMetaData = {
|
||||||
@@ -472,7 +478,7 @@ export declare type playerMetaData = {
|
|||||||
totalTime: number;
|
totalTime: number;
|
||||||
};
|
};
|
||||||
export declare type missingNode = {
|
export declare type missingNode = {
|
||||||
node: Node;
|
node: Node | RRNode;
|
||||||
mutation: addedNodeMutation;
|
mutation: addedNodeMutation;
|
||||||
};
|
};
|
||||||
export declare type missingNodeMap = {
|
export declare type missingNodeMap = {
|
||||||
@@ -507,9 +513,6 @@ export declare enum ReplayerEvents {
|
|||||||
StateChange = "state-change",
|
StateChange = "state-change",
|
||||||
PlayBack = "play-back"
|
PlayBack = "play-back"
|
||||||
}
|
}
|
||||||
export declare type ElementState = {
|
|
||||||
scroll?: [number, number];
|
|
||||||
};
|
|
||||||
export declare type KeepIframeSrcFn = (src: string) => boolean;
|
export declare type KeepIframeSrcFn = (src: string) => boolean;
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
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 type { throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, DocumentDimension, IWindow, DeprecatedMirror, textMutation } from './types';
|
||||||
import { Mirror } from 'rrweb-snapshot';
|
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 function on(type: string, fn: EventListenerOrEventListenerObject, target?: Document | IWindow): listenerHandler;
|
||||||
export declare let _mirror: DeprecatedMirror;
|
export declare let _mirror: DeprecatedMirror;
|
||||||
export declare function throttle<T>(func: (arg: T) => void, wait: number, options?: throttleOptions): (arg: T) => void;
|
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 isAncestorRemoved(target: Node, mirror: Mirror): boolean;
|
||||||
export declare function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent;
|
export declare function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent;
|
||||||
export declare function polyfill(win?: Window & typeof globalThis): void;
|
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 = {
|
declare type ResolveTree = {
|
||||||
value: addedNodeMutation;
|
value: addedNodeMutation;
|
||||||
children: ResolveTree[];
|
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 function iterateResolveTree(tree: ResolveTree, cb: (mutation: addedNodeMutation) => unknown): void;
|
||||||
export declare type AppendedIframe = {
|
export declare type AppendedIframe = {
|
||||||
mutationInQueue: addedNodeMutation;
|
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 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;
|
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 {};
|
export {};
|
||||||
|
|||||||
Reference in New Issue
Block a user