Repository: rrweb-io/rrweb
Branch: master
Commit: 3ae040ddf605
Files: 679
Total size: 8.1 MB
Directory structure:
gitextract_di9qjh7g/
├── .cache/
│ └── .gitkeep
├── .changeset/
│ ├── README.md
│ ├── angry-turtles-provide.md
│ ├── attribute-text-reductions.md
│ ├── beige-olives-roll.md
│ ├── blank-cherries-laugh.md
│ ├── blank-dev-changset.md
│ ├── brave-numbers-joke.md
│ ├── brave-walls-shine.md
│ ├── breezy-cats-heal.md
│ ├── breezy-mice-breathe.md
│ ├── bright-socks-clap.md
│ ├── calm-bulldogs-speak.md
│ ├── calm-oranges-sin.md
│ ├── chatty-cherries-train.md
│ ├── chilled-penguins-sin.md
│ ├── clean-plants-play.md
│ ├── clean-shrimps-lay.md
│ ├── cold-eyes-hunt.md
│ ├── cold-hounds-teach.md
│ ├── config.json
│ ├── controller-finish-flag.md
│ ├── cool-grapes-hug.md
│ ├── cool-horses-bow.md
│ ├── cuddly-bikes-fail.md
│ ├── cuddly-dolphins-approve.md
│ ├── cuddly-readers-warn.md
│ ├── curvy-apples-lay.md
│ ├── curvy-balloons-brake.md
│ ├── date-now-guard.md
│ ├── dirty-pets-fly.md
│ ├── dirty-rules-dress.md
│ ├── docs-install-guidance.md
│ ├── efficiently-splitCssText-1603.md
│ ├── efficiently-splitCssText-1640.md
│ ├── eight-terms-hunt.md
│ ├── eight-years-hope.md
│ ├── eighty-teachers-smash.md
│ ├── eleven-bobcats-peel.md
│ ├── eleven-toys-vanish.md
│ ├── empty-bikes-cheer.md
│ ├── empty-devonly-template.md
│ ├── eslint-camelcase-devonly.md
│ ├── event-single-wrap.md
│ ├── fair-dragons-greet.md
│ ├── fair-ducks-clean.md
│ ├── famous-bobcats-push.md
│ ├── fast-chefs-smell.md
│ ├── fast-pets-exist.md
│ ├── few-rockets-travel.md
│ ├── few-turkeys-reflect.md
│ ├── five-peas-lay.md
│ ├── fix-adapt-css.md
│ ├── fluffy-planes-retire.md
│ ├── format-head-prettier.md
│ ├── forty-elephants-attack.md
│ ├── four-panthers-fly.md
│ ├── fresh-cars-impress.md
│ ├── fresh-spoons-drive.md
│ ├── friendly-numbers-leave.md
│ ├── fuzzy-mugs-march.md
│ ├── giant-rats-chew.md
│ ├── gold-apples-joke.md
│ ├── gold-experts-type.md
│ ├── gold-terms-look.md
│ ├── great-cows-camp.md
│ ├── grumpy-ways-own.md
│ ├── happy-carrots-hide.md
│ ├── healthy-glasses-shout.md
│ ├── hip-worms-relax.md
│ ├── hungry-dodos-taste.md
│ ├── inlineImage-maybeNot-crossOrigin.md
│ ├── itchy-dryers-double.md
│ ├── itchy-tables-compete.md
│ ├── khaki-dots-bathe.md
│ ├── kind-kids-design.md
│ ├── large-ants-prove.md
│ ├── last-jest-to-vitest.md
│ ├── lazy-squids-draw.md
│ ├── lazy-toes-confess.md
│ ├── lemon-lamps-switch.md
│ ├── light-beans-destroy.md
│ ├── light-fireants-exercise.md
│ ├── little-radios-thank.md
│ ├── little-suits-leave.md
│ ├── loud-seals-raise.md
│ ├── lovely-files-sparkle.md
│ ├── lovely-pears-cross.md
│ ├── lovely-students-boil.md
│ ├── lucky-donuts-hammer.md
│ ├── lucky-trainers-joke.md
│ ├── mean-tips-impress.md
│ ├── metal-mugs-mate.md
│ ├── mighty-ads-worry.md
│ ├── mighty-bulldogs-begin.md
│ ├── mighty-frogs-sparkle.md
│ ├── modern-doors-watch.md
│ ├── moody-dots-refuse.md
│ ├── moody-experts-build.md
│ ├── nasty-scissors-reply.md
│ ├── nervous-actors-jam.md
│ ├── nervous-buses-pump.md
│ ├── nervous-kiwis-nail.md
│ ├── nervous-mirrors-perform.md
│ ├── nervous-poets-grin.md
│ ├── nervous-tables-travel.md
│ ├── new-snakes-call.md
│ ├── nice-pugs-reply.md
│ ├── no-neg-lookbehind.md
│ ├── odd-onions-brush.md
│ ├── old-dryers-hide.md
│ ├── perfect-bulldogs-punch.md
│ ├── perfect-dolls-grab.md
│ ├── polite-olives-wave.md
│ ├── pre.json
│ ├── pretty-meals-flash.md
│ ├── pretty-plums-rescue.md
│ ├── pretty-schools-remember.md
│ ├── proud-clocks-hope.md
│ ├── proud-experts-jam.md
│ ├── purple-carrots-film.md
│ ├── quiet-actors-mate.md
│ ├── rare-adults-sneeze.md
│ ├── real-masks-explode.md
│ ├── real-trains-switch.md
│ ├── red-peaches-explode.md
│ ├── rich-crews-protect.md
│ ├── rich-dots-lay.md
│ ├── rich-jars-remember.md
│ ├── rich-scissors-hide.md
│ ├── rotten-spies-enjoy.md
│ ├── serious-ants-juggle.md
│ ├── serious-eggs-greet.md
│ ├── shadow-dom-unbusify.md
│ ├── short-hounds-confess.md
│ ├── shy-countries-rhyme.md
│ ├── silent-plants-perform.md
│ ├── silly-knives-chew.md
│ ├── silver-pots-sit.md
│ ├── silver-windows-float.md
│ ├── simplifify-hover-replacement.md
│ ├── single-style-capture.md
│ ├── six-llamas-brush.md
│ ├── sixty-impalas-laugh.md
│ ├── skip-mask-check-on-leaf-elements.md
│ ├── slimdom-defaults-refactor.md
│ ├── slimy-eagles-grow.md
│ ├── small-hats-kneel.md
│ ├── small-olives-arrive.md
│ ├── smart-ears-refuse.md
│ ├── smart-geckos-cover.md
│ ├── smooth-papayas-boil.md
│ ├── smooth-poems-bake.md
│ ├── soft-worms-tan.md
│ ├── spotty-bees-destroy.md
│ ├── spotty-emus-listen.md
│ ├── stupid-ghosts-help.md
│ ├── swift-dancers-rest.md
│ ├── swift-peas-film.md
│ ├── swift-pots-search.md
│ ├── tender-foxes-rest.md
│ ├── textarea-inner-html.md
│ ├── thin-vans-applaud.md
│ ├── thirty-baboons-punch.md
│ ├── thirty-shirts-grow.md
│ ├── three-baboons-bow.md
│ ├── tidy-swans-repair.md
│ ├── tidy-yaks-joke.md
│ ├── tiny-buckets-love.md
│ ├── tiny-candles-whisper.md
│ ├── tiny-chairs-build.md
│ ├── title-deanimate-option.md
│ ├── tricky-panthers-guess.md
│ ├── twenty-flies-attend.md
│ ├── twenty-goats-kneel.md
│ ├── twenty-lies-switch.md
│ ├── twenty-planets-repeat.md
│ ├── two-boats-boil.md
│ ├── unlucky-mirrors-invite.md
│ ├── violet-melons-itch.md
│ ├── violet-zebras-cry.md
│ ├── wet-bottles-flash.md
│ ├── wicked-dolphins-tie.md
│ ├── wicked-lions-return.md
│ ├── wise-spiders-jog.md
│ ├── witty-kids-talk.md
│ ├── yellow-mails-cheat.md
│ └── young-timers-grow.md
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── feature_request.yml
│ ├── config.yml
│ ├── scripts/
│ │ ├── measure-bundle-sizes.js
│ │ └── render-bundle-size-comment.js
│ └── workflows/
│ ├── ci-cd.yml
│ ├── eslint-check.yml
│ ├── pr-checks-privileged.yml
│ ├── release.yml
│ └── style-check.yml
├── .gitignore
├── .markdownlint.yml
├── .npmignore
├── .prettierignore
├── .prettierrc
├── .puppeteerrc.cjs
├── .release-it.json
├── .vscode/
│ └── rrweb-monorepo.code-workspace
├── .yarn/
│ └── releases/
│ └── yarn-1.23.0-20220130.1630.cjs
├── .yarnrc.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── README.zh_CN.md
├── SECURITY.md
├── docs/
│ ├── development/
│ │ └── coding-style.md
│ ├── observer.md
│ ├── observer.zh_CN.md
│ ├── recipes/
│ │ ├── canvas.md
│ │ ├── canvas.zh_CN.md
│ │ ├── console.md
│ │ ├── console.zh_CN.md
│ │ ├── cross-origin-iframes.md
│ │ ├── cross-origin-iframes.zh_CN.md
│ │ ├── custom-event.md
│ │ ├── custom-event.zh_CN.md
│ │ ├── customize-replayer.md
│ │ ├── customize-replayer.zh_CN.md
│ │ ├── dive-into-event.md
│ │ ├── dive-into-event.zh_CN.md
│ │ ├── export-to-video.md
│ │ ├── export-to-video.zh_CN.md
│ │ ├── index.md
│ │ ├── index.zh_CN.md
│ │ ├── interaction.md
│ │ ├── interaction.zh_CN.md
│ │ ├── live-mode.md
│ │ ├── live-mode.zh_CN.md
│ │ ├── optimize-storage.md
│ │ ├── optimize-storage.zh_CN.md
│ │ ├── pagination.md
│ │ ├── pagination.zh_CN.md
│ │ ├── plugin.md
│ │ ├── plugin.zh_CN.md
│ │ ├── record-and-replay.md
│ │ └── record-and-replay.zh_CN.md
│ ├── replay.md
│ ├── replay.zh_CN.md
│ ├── sandbox.md
│ ├── sandbox.zh_CN.md
│ ├── serialization.md
│ ├── serialization.zh_CN.md
│ └── styleguide.md
├── guide.md
├── guide.zh_CN.md
├── package.json
├── packages/
│ ├── all/
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── test/
│ │ │ ├── __snapshots__/
│ │ │ │ └── cross-origin-iframe-packer.test.ts.snap
│ │ │ ├── cross-origin-iframe-packer.test.ts
│ │ │ ├── html/
│ │ │ │ └── blank.html
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ └── vitest.config.ts
│ ├── packer/
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── pack/
│ │ │ └── package.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── base.ts
│ │ │ ├── index.ts
│ │ │ ├── pack.ts
│ │ │ └── unpack.ts
│ │ ├── test/
│ │ │ ├── __snapshots__/
│ │ │ │ └── packer.test.ts.snap
│ │ │ └── packer.test.ts
│ │ ├── tsconfig.json
│ │ ├── unpack/
│ │ │ └── package.json
│ │ └── vite.config.ts
│ ├── plugins/
│ │ ├── rrweb-plugin-canvas-webrtc-record/
│ │ │ ├── CHANGELOG.md
│ │ │ ├── Readme.md
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── index.ts
│ │ │ │ ├── simple-peer-light.d.ts
│ │ │ │ └── types.ts
│ │ │ ├── tsconfig.json
│ │ │ └── vite.config.ts
│ │ ├── rrweb-plugin-canvas-webrtc-replay/
│ │ │ ├── CHANGELOG.md
│ │ │ ├── Readme.md
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── index.ts
│ │ │ │ ├── simple-peer-light.d.ts
│ │ │ │ └── types.ts
│ │ │ ├── tsconfig.json
│ │ │ └── vite.config.ts
│ │ ├── rrweb-plugin-console-record/
│ │ │ ├── CHANGELOG.md
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── error-stack-parser.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── stringify.ts
│ │ │ ├── test/
│ │ │ │ ├── __snapshots__/
│ │ │ │ │ └── index.test.ts.snap
│ │ │ │ ├── html/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── log.html
│ │ │ │ ├── index.test.ts
│ │ │ │ └── stringify.test.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── vite.config.ts
│ │ │ └── vitest.config.ts
│ │ ├── rrweb-plugin-console-replay/
│ │ │ ├── CHANGELOG.md
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ └── index.ts
│ │ │ ├── tsconfig.json
│ │ │ └── vite.config.ts
│ │ ├── rrweb-plugin-sequential-id-record/
│ │ │ ├── CHANGELOG.md
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ └── index.ts
│ │ │ ├── tsconfig.json
│ │ │ └── vite.config.ts
│ │ └── rrweb-plugin-sequential-id-replay/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ ├── record/
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── test/
│ │ │ └── record.test.ts
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ └── vitest.config.ts
│ ├── replay/
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── test/
│ │ │ └── replay.test.ts
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ └── vitest.config.ts
│ ├── rrdom/
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── diff.ts
│ │ │ ├── document.ts
│ │ │ ├── index.ts
│ │ │ └── style.ts
│ │ ├── test/
│ │ │ ├── __snapshots__/
│ │ │ │ └── virtual-dom.test.ts.snap
│ │ │ ├── diff/
│ │ │ │ └── dialog.test.ts
│ │ │ ├── diff.test.ts
│ │ │ ├── document.test.ts
│ │ │ ├── html/
│ │ │ │ ├── iframe.html
│ │ │ │ ├── main.html
│ │ │ │ └── shadow-dom.html
│ │ │ └── virtual-dom.test.ts
│ │ ├── tsconfig.json
│ │ ├── vite.config.js
│ │ └── vitest.config.ts
│ ├── rrdom-nodejs/
│ │ ├── .gitignore
│ │ ├── .vscode/
│ │ │ ├── extensions.json
│ │ │ └── launch.json
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── document-nodejs.ts
│ │ │ ├── index.ts
│ │ │ └── polyfill.ts
│ │ ├── test/
│ │ │ ├── document-nodejs.test.ts
│ │ │ └── polyfill.test.ts
│ │ ├── tsconfig.json
│ │ ├── vite.config.js
│ │ └── vitest.config.ts
│ ├── rrvideo/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── README.zh_CN.md
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── rrvideo.config.example.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ └── index.ts
│ │ ├── test/
│ │ │ ├── cli.test.ts
│ │ │ ├── events/
│ │ │ │ └── example.ts
│ │ │ └── tsconfig.json
│ │ └── tsconfig.json
│ ├── rrweb/
│ │ ├── .gitignore
│ │ ├── .release-it.json
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── rrweb-record/
│ │ │ └── package.json
│ │ ├── rrweb-replay/
│ │ │ └── package.json
│ │ ├── scripts/
│ │ │ ├── repl.js
│ │ │ ├── stream.js
│ │ │ └── utils.js
│ │ ├── src/
│ │ │ ├── entries/
│ │ │ │ ├── record.ts
│ │ │ │ └── replay.ts
│ │ │ ├── index.ts
│ │ │ ├── record/
│ │ │ │ ├── cross-origin-iframe-mirror.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── iframe-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mutation.ts
│ │ │ │ ├── observer.ts
│ │ │ │ ├── observers/
│ │ │ │ │ └── canvas/
│ │ │ │ │ ├── 2d.ts
│ │ │ │ │ ├── canvas-manager.ts
│ │ │ │ │ ├── canvas.ts
│ │ │ │ │ ├── serialize-args.ts
│ │ │ │ │ └── webgl.ts
│ │ │ │ ├── processed-node-manager.ts
│ │ │ │ ├── shadow-dom-manager.ts
│ │ │ │ ├── stylesheet-manager.ts
│ │ │ │ └── workers/
│ │ │ │ ├── image-bitmap-data-url-worker.ts
│ │ │ │ └── tsconfig.json
│ │ │ ├── replay/
│ │ │ │ ├── canvas/
│ │ │ │ │ ├── 2d.ts
│ │ │ │ │ ├── deserialize-args.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── webgl.ts
│ │ │ │ ├── dialog/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── machine.ts
│ │ │ │ ├── media/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── smoothscroll.ts
│ │ │ │ ├── styles/
│ │ │ │ │ ├── inject-style.ts
│ │ │ │ │ └── style.css
│ │ │ │ └── timer.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── test/
│ │ │ ├── __snapshots__/
│ │ │ │ ├── integration.test.ts.snap
│ │ │ │ ├── record.test.ts.snap
│ │ │ │ └── replayer.test.ts.snap
│ │ │ ├── benchmark/
│ │ │ │ ├── dom-mutation.test.ts
│ │ │ │ └── replay-fast-forward.test.ts
│ │ │ ├── e2e/
│ │ │ │ └── webgl.test.ts
│ │ │ ├── events/
│ │ │ │ ├── adopted-style-sheet-empty-replace.ts
│ │ │ │ ├── adopted-style-sheet-modification.ts
│ │ │ │ ├── adopted-style-sheet.ts
│ │ │ │ ├── bad-style.ts
│ │ │ │ ├── bad-textarea.ts
│ │ │ │ ├── canvas-in-iframe.ts
│ │ │ │ ├── custom-element-define-class.ts
│ │ │ │ ├── dialog-playback.ts
│ │ │ │ ├── document-replacement.ts
│ │ │ │ ├── hover.ts
│ │ │ │ ├── iframe-shadowdom-hover.ts
│ │ │ │ ├── iframe.ts
│ │ │ │ ├── input.ts
│ │ │ │ ├── nested-style-declaration.ts
│ │ │ │ ├── ordering.ts
│ │ │ │ ├── scroll-with-parent-styles.ts
│ │ │ │ ├── scroll.ts
│ │ │ │ ├── selection.ts
│ │ │ │ ├── shadow-dom.ts
│ │ │ │ ├── style-declaration-missing-rule.ts
│ │ │ │ ├── style-sheet-rule-events.ts
│ │ │ │ ├── style-sheet-text-mutation.ts
│ │ │ │ ├── video-playback-on-full-snapshot.ts
│ │ │ │ ├── video-playback.ts
│ │ │ │ └── webgl.ts
│ │ │ ├── html/
│ │ │ │ ├── assets/
│ │ │ │ │ ├── bunny-video.webm
│ │ │ │ │ ├── style.css
│ │ │ │ │ └── webgl-utils.js
│ │ │ │ ├── audio.html
│ │ │ │ ├── benchmark-dom-mutation-add-and-move.html
│ │ │ │ ├── benchmark-dom-mutation-add-and-remove.html
│ │ │ │ ├── benchmark-dom-mutation-attributes.html
│ │ │ │ ├── benchmark-dom-mutation-deep-nested.html
│ │ │ │ ├── benchmark-dom-mutation-multiple-descendant-add.html
│ │ │ │ ├── benchmark-dom-mutation.html
│ │ │ │ ├── blank.html
│ │ │ │ ├── block.html
│ │ │ │ ├── blocked-unblocked.html
│ │ │ │ ├── canvas-webgl-image.html
│ │ │ │ ├── canvas-webgl-shader.html
│ │ │ │ ├── canvas-webgl-square.html
│ │ │ │ ├── canvas-webgl.html
│ │ │ │ ├── canvas.html
│ │ │ │ ├── dialog.html
│ │ │ │ ├── empty.html
│ │ │ │ ├── form.html
│ │ │ │ ├── frame-image-blob-url.html
│ │ │ │ ├── frame1.html
│ │ │ │ ├── frame2.html
│ │ │ │ ├── hello-world.html
│ │ │ │ ├── ignore.html
│ │ │ │ ├── image-blob-url.html
│ │ │ │ ├── link.html
│ │ │ │ ├── main.html
│ │ │ │ ├── mask-text.html
│ │ │ │ ├── move-node.html
│ │ │ │ ├── mutation-observer.html
│ │ │ │ ├── password.html
│ │ │ │ ├── polyfilled-shadowdom-mutation.html
│ │ │ │ ├── react-styled-components.html
│ │ │ │ ├── select2.html
│ │ │ │ ├── shadow-dom.html
│ │ │ │ ├── shuffle.html
│ │ │ │ ├── style.html
│ │ │ │ └── video.html
│ │ │ ├── integration.test.ts
│ │ │ ├── machine.test.ts
│ │ │ ├── record/
│ │ │ │ ├── __snapshots__/
│ │ │ │ │ ├── cross-origin-iframes.test.ts.snap
│ │ │ │ │ ├── cross-origin-iframes.test.ts.snap.extra
│ │ │ │ │ ├── dialog.test.ts.snap
│ │ │ │ │ └── webgl.test.ts.snap
│ │ │ │ ├── cross-origin-iframes.test.ts
│ │ │ │ ├── dialog.test.ts
│ │ │ │ ├── error-handler.test.ts
│ │ │ │ ├── serialize-args.test.ts
│ │ │ │ └── webgl.test.ts
│ │ │ ├── record.test.ts
│ │ │ ├── replay/
│ │ │ │ ├── 2d-mutation.test.ts
│ │ │ │ ├── deserialize-args.test.ts
│ │ │ │ ├── dialog.test.ts
│ │ │ │ ├── hover.test.ts
│ │ │ │ ├── preload-all-images.test.ts
│ │ │ │ ├── video.test.ts
│ │ │ │ ├── webgl-mutation.test.ts
│ │ │ │ └── webgl.test.ts
│ │ │ ├── replayer.test.ts
│ │ │ ├── rrdom.test.ts
│ │ │ ├── util.test.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ ├── vite.config.entries.js
│ │ ├── vite.config.js
│ │ └── vitest.config.ts
│ ├── rrweb-player/
│ │ ├── .eslintignore
│ │ ├── .eslintrc.cjs
│ │ ├── .gitignore
│ │ ├── .prettierignore
│ │ ├── .release-it.json
│ │ ├── .svelte-kit/
│ │ │ ├── ambient.d.ts
│ │ │ ├── generated/
│ │ │ │ └── client/
│ │ │ │ ├── app.js
│ │ │ │ ├── matchers.js
│ │ │ │ └── nodes/
│ │ │ │ ├── 0.js
│ │ │ │ └── 1.js
│ │ │ ├── non-ambient.d.ts
│ │ │ └── tsconfig.json
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── public/
│ │ │ ├── events.js
│ │ │ └── global.css
│ │ ├── src/
│ │ │ ├── Controller.svelte
│ │ │ ├── Player.svelte
│ │ │ ├── components/
│ │ │ │ └── Switch.svelte
│ │ │ ├── main.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── svelte.config.js
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ ├── vite-env.d.ts
│ │ └── vite.config.ts
│ ├── rrweb-snapshot/
│ │ ├── .gitignore
│ │ ├── .release-it.json
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── jsr.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── css.ts
│ │ │ ├── index.ts
│ │ │ ├── rebuild.ts
│ │ │ ├── snapshot.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── test/
│ │ │ ├── __snapshots__/
│ │ │ │ └── integration.test.ts.snap
│ │ │ ├── alt-css/
│ │ │ │ └── alt-style.css
│ │ │ ├── css/
│ │ │ │ ├── benchmark.css
│ │ │ │ ├── style-with-import.css
│ │ │ │ └── style.css
│ │ │ ├── css.test.ts
│ │ │ ├── html/
│ │ │ │ ├── about-mozilla.html
│ │ │ │ ├── background-clip-text.html
│ │ │ │ ├── basic.html
│ │ │ │ ├── block-element.html
│ │ │ │ ├── compat-mode.html
│ │ │ │ ├── cors-style-sheet.html
│ │ │ │ ├── dialog.html
│ │ │ │ ├── dynamic-stylesheet.html
│ │ │ │ ├── form-fields.html
│ │ │ │ ├── hover.html
│ │ │ │ ├── iframe-inner.html
│ │ │ │ ├── iframe.html
│ │ │ │ ├── invalid-attribute.html
│ │ │ │ ├── invalid-doctype.html
│ │ │ │ ├── invalid-tagname.html
│ │ │ │ ├── mask-text.html
│ │ │ │ ├── monkey-patched-elements.html
│ │ │ │ ├── picture-blob-in-frame.html
│ │ │ │ ├── picture-blob.html
│ │ │ │ ├── picture-in-frame.html
│ │ │ │ ├── picture-with-inline-onload.html
│ │ │ │ ├── picture.html
│ │ │ │ ├── preload.html
│ │ │ │ ├── shadow-dom.html
│ │ │ │ ├── svg.html
│ │ │ │ ├── video.html
│ │ │ │ ├── with-relative-res.html
│ │ │ │ ├── with-script.html
│ │ │ │ ├── with-style-sheet-with-import.html
│ │ │ │ └── with-style-sheet.html
│ │ │ ├── iframe-html/
│ │ │ │ ├── frame1.html
│ │ │ │ ├── frame2.html
│ │ │ │ └── main.html
│ │ │ ├── integration.test.ts
│ │ │ ├── js/
│ │ │ │ └── a.js
│ │ │ ├── rebuild.test.ts
│ │ │ ├── snapshot.test.ts
│ │ │ ├── stringify-stylesheet.bench.ts
│ │ │ ├── utils.test.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ ├── vite.config.js
│ │ └── vitest.config.ts
│ ├── types/
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── tsconfig.json
│ │ └── vite.config.js
│ ├── utils/
│ │ ├── CHANGELOG.md
│ │ ├── Readme.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── tsconfig.json
│ │ └── vite.config.js
│ └── web-extension/
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── background/
│ │ │ └── index.ts
│ │ ├── components/
│ │ │ ├── CircleButton.tsx
│ │ │ └── SidebarWithHeader.tsx
│ │ ├── content/
│ │ │ ├── index.ts
│ │ │ └── inject.ts
│ │ ├── manifest.json
│ │ ├── options/
│ │ │ ├── App.tsx
│ │ │ ├── index.html
│ │ │ └── index.tsx
│ │ ├── pages/
│ │ │ ├── App.tsx
│ │ │ ├── Player.tsx
│ │ │ ├── SessionList.tsx
│ │ │ ├── index.html
│ │ │ └── index.tsx
│ │ ├── popup/
│ │ │ ├── App.tsx
│ │ │ ├── Timer.tsx
│ │ │ ├── index.tsx
│ │ │ └── popup.html
│ │ ├── types.ts
│ │ └── utils/
│ │ ├── channel.ts
│ │ ├── index.ts
│ │ └── storage.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── scripts/
│ └── lint-packages.sh
├── tsconfig.base.json
├── tsconfig.eslint.json
├── tsconfig.json
├── turbo.json
├── vite.config.default.ts
├── vitest.config.ts
└── vitest.workspace.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .cache/.gitkeep
================================================
================================================
FILE: .changeset/README.md
================================================
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
================================================
FILE: .changeset/angry-turtles-provide.md
================================================
---
"rrweb-snapshot": patch
---
Handle exceptions thrown from postcss when calling adaptCssForReplay
================================================
FILE: .changeset/attribute-text-reductions.md
================================================
---
'rrweb': patch
---
Don't include redundant data from text/attribute mutations on just-added nodes
================================================
FILE: .changeset/beige-olives-roll.md
================================================
---
"rrweb-snapshot": patch
"rrweb": patch
---
Fix that the optional `maskInputFn` was being accidentally ignored during the creation of the full snapshot
================================================
FILE: .changeset/blank-cherries-laugh.md
================================================
---
---
================================================
FILE: .changeset/blank-dev-changset.md
================================================
---
---
================================================
FILE: .changeset/brave-numbers-joke.md
================================================
---
---
================================================
FILE: .changeset/brave-walls-shine.md
================================================
---
"@rrweb/record": major
"@rrweb/replay": major
---
BREAKING CHANGE: Rename UMD global names from `rrweb` to `rrwebRecord` for the recorder and `rrwebReplay` for the replayer. This avoids conflicts when both are loaded on the same page.
================================================
FILE: .changeset/breezy-cats-heal.md
================================================
---
'rrweb': patch
---
fix: createImageBitmap throws DOMException if source is 0 width or height
================================================
FILE: .changeset/breezy-mice-breathe.md
================================================
---
'rrweb': patch
---
safely capture BigInt values with the console log plugin"
================================================
FILE: .changeset/bright-socks-clap.md
================================================
---
---
================================================
FILE: .changeset/calm-bulldogs-speak.md
================================================
---
---
================================================
FILE: .changeset/calm-oranges-sin.md
================================================
---
'rrweb': patch
---
fix: Fix checking for `patchTarget` in `initAdoptedStyleSheetObserver`
================================================
FILE: .changeset/chatty-cherries-train.md
================================================
---
'rrweb': patch
---
Fix the statement which is getting changed by Microbundle
================================================
FILE: .changeset/chilled-penguins-sin.md
================================================
---
"rrdom": patch
---
Ignore invalid DOM attributes when diffing
================================================
FILE: .changeset/clean-plants-play.md
================================================
---
'rrweb': patch
'@rrweb/types': patch
---
Compact style mutation fixes and improvements
- fixes when style updates contain a 'var()' on a shorthand property #1246
- further ensures that style mutations are compact by reverting to string method if it is shorter
================================================
FILE: .changeset/clean-shrimps-lay.md
================================================
---
'rrweb': patch
---
feat: Add `ignoreSelector` option
Similar to ignoreClass, but accepts a CSS selector so that you can use any CSS selector.
================================================
FILE: .changeset/cold-eyes-hunt.md
================================================
---
'rrdom': patch
---
Fix: rrdom bugs
1. Fix a bug in the diff algorithm.
2. Omit the 'srcdoc' attribute of iframes to avoid overwriting content.
================================================
FILE: .changeset/cold-hounds-teach.md
================================================
---
---
================================================
FILE: .changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "rrweb-io/rrweb" }],
"commit": false,
"fixed": [
[
"rrweb",
"rrweb-snapshot",
"rrdom",
"rrdom-nodejs",
"rrweb-player",
"@rrweb/all",
"@rrweb/replay",
"@rrweb/record",
"@rrweb/types",
"@rrweb/packer",
"@rrweb/utils",
"@rrweb/web-extension",
"rrvideo",
"@rrweb/rrweb-plugin-console-record",
"@rrweb/rrweb-plugin-console-replay",
"@rrweb/rrweb-plugin-sequential-id-record",
"@rrweb/rrweb-plugin-sequential-id-replay",
"@rrweb/rrweb-plugin-canvas-webrtc-record",
"@rrweb/rrweb-plugin-canvas-webrtc-replay"
]
],
"linked": [],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": []
}
================================================
FILE: .changeset/controller-finish-flag.md
================================================
---
'rrweb-player': patch
'rrweb': patch
---
Reset the finished flag in Controller `goto` instead of `handleProgressClick` so that it is properly handled if `goto` is called directly.
================================================
FILE: .changeset/cool-grapes-hug.md
================================================
---
'rrdom': patch
---
Support `loop` in `RRMediaElement`
================================================
FILE: .changeset/cool-horses-bow.md
================================================
---
"@rrweb/rrweb-plugin-canvas-webrtc-record": patch
"@rrweb/rrweb-plugin-canvas-webrtc-replay": patch
"@rrweb/rrweb-plugin-sequential-id-record": patch
"@rrweb/rrweb-plugin-sequential-id-replay": patch
"@rrweb/rrweb-plugin-console-record": patch
"@rrweb/rrweb-plugin-console-replay": patch
"@rrweb/packer": patch
"@rrweb/record": patch
"@rrweb/replay": patch
"@rrweb/all": patch
---
Keep package version in sync with other packages
================================================
FILE: .changeset/cuddly-bikes-fail.md
================================================
---
"rrweb-snapshot": patch
"rrweb": patch
---
fix: duplicate textContent for style elements cause incremental style mutations to be invalid
================================================
FILE: .changeset/cuddly-dolphins-approve.md
================================================
---
"@rrweb/all": patch
"@rrweb/packer": patch
"@rrweb/record": patch
"rrweb-snapshot": patch
"rrweb": patch
"@rrweb/web-extension": patch
---
Drop base64 inlined worker source from all bundles
================================================
FILE: .changeset/cuddly-readers-warn.md
================================================
---
---
================================================
FILE: .changeset/curvy-apples-lay.md
================================================
---
'rrweb-snapshot': patch
'rrweb': patch
---
Extend to run fixBrowserCompatibilityIssuesInCSS over inline stylesheets
================================================
FILE: .changeset/curvy-balloons-brake.md
================================================
---
---
================================================
FILE: .changeset/date-now-guard.md
================================================
---
'rrweb': patch
---
Guard against presence of older 3rd party javascript libraries which redefine Date.now()
================================================
FILE: .changeset/dirty-pets-fly.md
================================================
---
---
================================================
FILE: .changeset/dirty-rules-dress.md
================================================
---
'rrweb-snapshot': minor
---
Video and Audio elements now also capture `playbackRate`, `muted`, `loop`, `volume`.
================================================
FILE: .changeset/docs-install-guidance.md
================================================
---
---
Docs-only update: clarify package recommendation order (`@rrweb/record` + `@rrweb/replay` first, `@rrweb/all` as convenience), and fix example typos.
================================================
FILE: .changeset/efficiently-splitCssText-1603.md
================================================
---
"rrweb-snapshot": patch
"rrweb": patch
---
Improve performance of splitCssText for '); // old document with elements that need removing
const rrDocument = new RRDocument();
const docType = rrDocument.createDocumentType('html', '', '');
rrDocument.mirror.add(docType, getDefaultSN(docType, 1));
rrDocument.appendChild(docType);
const htmlEl = rrDocument.createElement('html');
rrDocument.mirror.add(htmlEl, getDefaultSN(htmlEl, 2));
rrDocument.appendChild(htmlEl);
diff(document, rrDocument, replayer);
expect(document.childNodes.length).toBe(2);
const element = document.childNodes[0] as HTMLElement;
expect(element.nodeType).toBe(element.DOCUMENT_TYPE_NODE);
expect(mirror.getId(element)).toEqual(1);
});
it('should remove children from document before adding new nodes 2', () => {
document.write('');
const iframe = document.querySelector('iframe')!;
// Remove everthing from the iframe but the root html element
// `buildNodeWithSn` injects docType elements to trigger compatMode in iframes
iframe.contentDocument!.write(
'',
);
replayer.mirror.add(iframe.contentDocument!, {
id: 1,
type: 0,
childNodes: [
{
id: 2,
rootId: 1,
type: 2,
tagName: 'html',
childNodes: [],
attributes: {},
},
],
} as serializedNodeWithId);
replayer.mirror.add(iframe.contentDocument!.childNodes[0], {
id: 2,
rootId: 1,
type: 2,
tagName: 'html',
childNodes: [],
attributes: {},
} as serializedNodeWithId);
const rrDocument = new RRDocument();
rrDocument.mirror.add(rrDocument, getDefaultSN(rrDocument, 1));
const docType = rrDocument.createDocumentType('html', '', '');
rrDocument.mirror.add(docType, getDefaultSN(docType, 2));
rrDocument.appendChild(docType);
const htmlEl = rrDocument.createElement('html');
rrDocument.mirror.add(htmlEl, getDefaultSN(htmlEl, 3));
rrDocument.appendChild(htmlEl);
const styleEl = rrDocument.createElement('style');
rrDocument.mirror.add(styleEl, getDefaultSN(styleEl, 4));
htmlEl.appendChild(styleEl);
const headEl = rrDocument.createElement('head');
rrDocument.mirror.add(headEl, getDefaultSN(headEl, 5));
htmlEl.appendChild(headEl);
const bodyEl = rrDocument.createElement('body');
rrDocument.mirror.add(bodyEl, getDefaultSN(bodyEl, 6));
htmlEl.appendChild(bodyEl);
diff(iframe.contentDocument!, rrDocument, replayer);
expect(iframe.contentDocument!.childNodes.length).toBe(2);
const element = iframe.contentDocument!.childNodes[0] as HTMLElement;
expect(element.nodeType).toBe(element.DOCUMENT_TYPE_NODE);
expect(mirror.getId(element)).toEqual(2);
});
it('should remove children from document before adding new nodes 3', () => {
document.write('
');
const iframeInDom = document.querySelector('iframe')!;
replayer.mirror.add(iframeInDom, {
id: 3,
type: 2,
rootId: 1,
tagName: 'iframe',
childNodes: [],
attributes: {},
} as serializedNodeWithId);
replayer.mirror.add(iframeInDom.contentDocument!, {
id: 4,
type: 0,
childNodes: [],
} as serializedNodeWithId);
const rrDocument = new RRDocument();
const rrIframeEl = rrDocument.createElement('iframe');
rrDocument.mirror.add(rrIframeEl, getDefaultSN(rrIframeEl, 3));
rrDocument.appendChild(rrIframeEl);
rrDocument.mirror.add(
rrIframeEl.contentDocument!,
getDefaultSN(rrIframeEl.contentDocument!, 4),
);
const rrDocType = rrDocument.createDocumentType('html', '', '');
rrIframeEl.contentDocument.appendChild(rrDocType);
const rrHtmlEl = rrDocument.createElement('html');
rrDocument.mirror.add(rrHtmlEl, getDefaultSN(rrHtmlEl, 6));
rrIframeEl.contentDocument.appendChild(rrHtmlEl);
const rrHeadEl = rrDocument.createElement('head');
rrDocument.mirror.add(rrHeadEl, getDefaultSN(rrHeadEl, 8));
rrHtmlEl.appendChild(rrHeadEl);
const bodyEl = rrDocument.createElement('body');
rrDocument.mirror.add(bodyEl, getDefaultSN(bodyEl, 9));
rrHtmlEl.appendChild(bodyEl);
diff(iframeInDom, rrIframeEl, replayer);
expect(iframeInDom.contentDocument!.childNodes.length).toBe(2);
const element = iframeInDom.contentDocument!.childNodes[0] as HTMLElement;
expect(element.nodeType).toBe(element.DOCUMENT_TYPE_NODE);
expect(mirror.getId(element)).toEqual(-1);
});
it('should remove children from document before adding new nodes 4', () => {
/**
* This case aims to test whether the diff function can remove all the old doctype and html element from the document before adding new doctype and html element.
* If not, the diff function will throw errors or warnings.
*/
// Mock the original console.warn function to make the test fail once console.warn is called.
const warn = vi.spyOn(global.console, 'warn');
document.write('');
const rrdom = new RRDocument();
/**
* Make the structure of document and RRDom look like this:
* -2 Document
* -3 DocumentType
* -4 HTML
* -5 HEAD
* -6 BODY
*/
buildFromDom(document, mirror, rrdom);
expect(mirror.getId(document)).toBe(-2);
expect(mirror.getId(document.body)).toBe(-6);
expect(rrdom.mirror.getId(rrdom)).toBe(-2);
expect(rrdom.mirror.getId(rrdom.body)).toBe(-6);
while (rrdom.firstChild) rrdom.removeChild(rrdom.firstChild);
/**
* Rebuild the rrdom and make it looks like this:
* -7 RRDocument
* -8 RRDocumentType
* -9 HTML
* -10 HEAD
* -11 BODY
*/
buildFromDom(document, undefined, rrdom);
// Keep the ids of real document unchanged.
expect(mirror.getId(document)).toBe(-2);
expect(mirror.getId(document.body)).toBe(-6);
expect(rrdom.mirror.getId(rrdom)).toBe(-7);
expect(rrdom.mirror.getId(rrdom.body)).toBe(-11);
// Diff the document with the new rrdom.
diff(document, rrdom, replayer);
// Check that warn was not called (fail on warning)
expect(warn).not.toHaveBeenCalled();
// Check that the old nodes are removed from the NodeMirror.
[-2, -3, -4, -5, -6].forEach((id) =>
expect(mirror.getNode(id)).toBeNull(),
);
expect(mirror.getId(document)).toBe(-7);
expect(mirror.getId(document.doctype)).toBe(-8);
expect(mirror.getId(document.documentElement)).toBe(-9);
expect(mirror.getId(document.head)).toBe(-10);
expect(mirror.getId(document.body)).toBe(-11);
warn.mockRestore();
});
it('selectors should be case-sensitive for matching in iframe dom', async () => {
/**
* If the selector match is case insensitive, it will cause some CSS style problems in the replayer.
* This test result executed in JSDom is different from that in real browser so we use puppeteer as test environment.
*/
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.goto('about:blank');
try {
const code = fs.readFileSync(
path.resolve(__dirname, '../dist/rrdom.umd.cjs'),
'utf8',
);
await page.evaluate(code);
const className = 'case-sensitive';
// To show the selector match pattern (case sensitive) in normal dom.
const caseInsensitiveInNormalDom = await page.evaluate((className) => {
document.write(
'',
);
const htmlEl = document.documentElement;
htmlEl.className = className.toLowerCase();
return htmlEl.matches(`.${className.toUpperCase()}`);
}, className);
expect(caseInsensitiveInNormalDom).toBeFalsy();
// To show the selector match pattern (case insensitive) in auto mounted iframe dom.
const caseInsensitiveInDefaultIFrameDom = await page.evaluate(
(className) => {
const iframeEl = document.querySelector('iframe');
const htmlEl = iframeEl?.contentDocument?.documentElement;
if (htmlEl) {
htmlEl.className = className.toLowerCase();
return htmlEl.matches(`.${className.toUpperCase()}`);
}
},
className,
);
expect(caseInsensitiveInDefaultIFrameDom).toBeTruthy();
const iframeElId = 3,
iframeDomId = 4,
htmlElId = 5;
const result = await page.evaluate(`
const iframeEl = document.querySelector('iframe');
// Construct a virtual dom tree.
const rrDocument = new rrdom.RRDocument();
const rrIframeEl = rrDocument.createElement('iframe');
rrDocument.mirror.add(rrIframeEl, rrdom.getDefaultSN(rrIframeEl, ${iframeElId}));
rrDocument.appendChild(rrIframeEl);
rrDocument.mirror.add(
rrIframeEl.contentDocument,
rrdom.getDefaultSN(rrIframeEl.contentDocument, ${iframeDomId}),
);
const rrDocType = rrDocument.createDocumentType('html', '', '');
rrIframeEl.contentDocument.appendChild(rrDocType);
const rrHtmlEl = rrDocument.createElement('html');
rrDocument.mirror.add(rrHtmlEl, rrdom.getDefaultSN(rrHtmlEl, ${htmlElId}));
rrIframeEl.contentDocument.appendChild(rrHtmlEl);
const replayer = {
mirror: rrdom.createMirror(),
applyCanvas: () => {},
applyInput: () => {},
applyScroll: () => {},
applyStyleSheetMutation: () => {},
};
rrdom.diff(iframeEl, rrIframeEl, replayer);
iframeEl.contentDocument.documentElement.className =
'${className.toLowerCase()}';
iframeEl.contentDocument.childNodes.length === 2 &&
replayer.mirror.getId(iframeEl.contentDocument.documentElement) === ${htmlElId} &&
// To test whether the selector match of the updated iframe document is case sensitive or not.
!iframeEl.contentDocument.documentElement.matches(
'.${className.toUpperCase()}',
);
`);
// IFrame document has two children, mirror id of documentElement is ${htmlElId}, and selectors should be case-sensitive for matching in iframe dom (consistent with the normal dom).
expect(result).toBeTruthy();
} finally {
await page.close();
await browser.close();
}
});
});
describe('afterAppend callback', () => {
it('should call afterAppend callback', () => {
const afterAppendFn = vi.spyOn(replayer, 'afterAppend');
const node = createTree(
{
tagName: 'div',
id: 1,
},
undefined,
mirror,
) as Node;
const rrdom = new RRDocument();
const rrNode = createTree(
{
tagName: 'div',
id: 1,
children: [
{
tagName: 'span',
id: 2,
},
],
},
rrdom,
) as RRNode;
diff(node, rrNode, replayer);
expect(afterAppendFn).toHaveBeenCalledTimes(1);
expect(afterAppendFn).toHaveBeenCalledWith(node.childNodes[0], 2);
afterAppendFn.mockRestore();
});
it('should diff without afterAppend callback', () => {
replayer.afterAppend = undefined;
const rrdom = buildFromDom(document);
document.open();
diff(document, rrdom, replayer);
replayer.afterAppend = () => {};
});
it('should call afterAppend callback in the post traversal order', () => {
const afterAppendFn = vi.spyOn(replayer, 'afterAppend');
document.open();
const rrdom = new RRDocument();
rrdom.mirror.add(rrdom, getDefaultSN(rrdom, 1));
const rrNode = createTree(
{
tagName: 'html',
id: 1,
children: [
{
tagName: 'head',
id: 2,
},
{
tagName: 'body',
id: 3,
children: [
{
tagName: 'span',
id: 4,
children: [
{
tagName: 'li',
id: 5,
},
{
tagName: 'li',
id: 6,
},
],
},
{
tagName: 'p',
id: 7,
},
{
tagName: 'p',
id: 8,
children: [
{
tagName: 'li',
id: 9,
},
{
tagName: 'li',
id: 10,
},
],
},
],
},
],
},
rrdom,
) as RRNode;
diff(document, rrNode, replayer);
expect(afterAppendFn).toHaveBeenCalledTimes(10);
// the correct traversal order
[2, 5, 6, 4, 7, 9, 10, 8, 3, 1].forEach((id, index) => {
expect((mirror.getNode(id) as HTMLElement).tagName).toEqual(
(rrdom.mirror.getNode(id) as IRRElement).tagName,
);
expect(afterAppendFn).toHaveBeenNthCalledWith(
index + 1,
mirror.getNode(id),
id,
);
});
});
it('should only call afterAppend for newly created nodes', () => {
const afterAppendFn = vi.spyOn(replayer, 'afterAppend');
const rrdom = buildFromDom(document, replayer.mirror) as RRDocument;
// Append 3 nodes to rrdom.
const rrNode = createTree(
{
tagName: 'span',
id: 1,
children: [
{
tagName: 'li',
id: 2,
},
{
tagName: 'li',
id: 3,
},
],
},
rrdom,
) as RRNode;
rrdom.body?.appendChild(rrNode);
diff(document, rrdom, replayer);
expect(afterAppendFn).toHaveBeenCalledTimes(3);
// Should only call afterAppend for 3 newly appended nodes.
[2, 3, 1].forEach((id, index) => {
expect((mirror.getNode(id) as HTMLElement).tagName).toEqual(
(rrdom.mirror.getNode(id) as IRRElement).tagName,
);
expect(afterAppendFn).toHaveBeenNthCalledWith(
index + 1,
mirror.getNode(id),
id,
);
});
afterAppendFn.mockClear();
});
});
describe('create or get a Node', () => {
it('create a real HTML element from RRElement', () => {
const rrDocument = new RRDocument();
const rrNode = rrDocument.createElement('DIV');
const sn2 = Object.assign({}, elementSn, { id: 0 });
rrDocument.mirror.add(rrNode, sn2);
let result = createOrGetNode(rrNode, mirror, rrDocument.mirror);
expect(result).toBeInstanceOf(HTMLElement);
expect(mirror.getId(result)).toBe(0);
expect((result as Node as HTMLElement).tagName).toBe('DIV');
});
it('create a node from RRNode', () => {
const rrDocument = new RRDocument();
rrDocument.mirror.add(rrDocument, getDefaultSN(rrDocument, 0));
let result = createOrGetNode(rrDocument, mirror, rrDocument.mirror);
expect(result).toBeInstanceOf(Document);
expect(mirror.getId(result)).toBe(0);
const textContent = 'Text Content';
let rrNode: RRNode = rrDocument.createTextNode(textContent);
rrDocument.mirror.add(rrNode, getDefaultSN(rrNode, 1));
result = createOrGetNode(rrNode, mirror, rrDocument.mirror);
expect(result).toBeInstanceOf(Text);
expect(mirror.getId(result)).toBe(1);
expect((result as Node as Text).textContent).toBe(textContent);
rrNode = rrDocument.createComment(textContent);
rrDocument.mirror.add(rrNode, getDefaultSN(rrNode, 2));
result = createOrGetNode(rrNode, mirror, rrDocument.mirror);
expect(result).toBeInstanceOf(Comment);
expect(mirror.getId(result)).toBe(2);
expect((result as Node as Comment).textContent).toBe(textContent);
rrNode = rrDocument.createCDATASection('');
rrDocument.mirror.add(rrNode, getDefaultSN(rrNode, 3));
expect(() =>
createOrGetNode(rrNode, mirror, rrDocument.mirror),
).toThrowErrorMatchingInlineSnapshot(`DOMException {}`);
});
it('create a DocumentType from RRDocumentType', () => {
const rrDocument = new RRDocument();
const publicId = '-//W3C//DTD XHTML 1.0 Transitional//EN';
let rrNode: RRNode = rrDocument.createDocumentType('html', publicId, '');
rrDocument.mirror.add(rrNode, getDefaultSN(rrNode, 0));
let result = createOrGetNode(rrNode, mirror, rrDocument.mirror);
expect(result).toBeInstanceOf(DocumentType);
expect(mirror.getId(result)).toBe(0);
expect((result as Node as DocumentType).name).toEqual('html');
expect((result as Node as DocumentType).publicId).toEqual(publicId);
expect((result as Node as DocumentType).systemId).toEqual('');
});
it('can get a node if it already exists', () => {
const rrDocument = new RRDocument();
const textContent = 'Text Content';
const text = document.createTextNode(textContent);
const sn: serializedNodeWithId = {
id: 0,
type: RRNodeType.Text,
textContent: 'text of the existed node',
};
// Add the text node to the mirror to make it look like already existing.
mirror.add(text, sn);
const rrNode: RRNode = rrDocument.createTextNode(textContent);
rrDocument.mirror.add(rrNode, getDefaultSN(rrNode, 0));
let result = createOrGetNode(rrNode, mirror, rrDocument.mirror);
expect(result).toBeInstanceOf(Text);
expect(mirror.getId(result)).toBe(0);
expect((result as Node as Text).textContent).toBe(textContent);
expect(result).toEqual(text);
// To make sure the existed text node is used.
expect(mirror.getMeta(result)).toEqual(mirror.getMeta(text));
});
});
describe('test sameNodeType function', () => {
const rrdom = new RRDocument();
it('should return true when two elements have same tagNames', () => {
const div1 = document.createElement('div');
const div2 = rrdom.createElement('div');
expect(sameNodeType(div1, div2)).toBeTruthy();
});
it('should return false when two elements have different tagNames', () => {
const div1 = document.createElement('div');
const div2 = rrdom.createElement('span');
expect(sameNodeType(div1, div2)).toBeFalsy();
});
it('should return false when two nodes have the same node type', () => {
let node1: Node = new Document();
let node2: IRRNode = new RRDocument();
expect(sameNodeType(node1, node2)).toBeTruthy();
node1 = document.implementation.createDocumentType('html', '', '');
node2 = rrdom.createDocumentType('', '', '');
expect(sameNodeType(node1, node2)).toBeTruthy();
node1 = document.createTextNode('node1');
node2 = rrdom.createTextNode('node2');
expect(sameNodeType(node1, node2)).toBeTruthy();
node1 = document.createComment('node1');
node2 = rrdom.createComment('node2');
expect(sameNodeType(node1, node2)).toBeTruthy();
});
it('should return false when two nodes have different node types', () => {
let node1: Node = new Document();
let node2: IRRNode = rrdom.createDocumentType('', '', '');
expect(sameNodeType(node1, node2)).toBeFalsy();
node1 = document.implementation.createDocumentType('html', '', '');
node2 = new RRDocument();
expect(sameNodeType(node1, node2)).toBeFalsy();
node1 = document.createTextNode('node1');
node2 = rrdom.createComment('node2');
expect(sameNodeType(node1, node2)).toBeFalsy();
node1 = document.createComment('node1');
node2 = rrdom.createTextNode('node2');
expect(sameNodeType(node1, node2)).toBeFalsy();
});
});
describe('test nodeMatching function', () => {
const rrdom = new RRDocument();
const NodeMirror = createMirror();
const rrdomMirror = new RRNodeMirror();
beforeEach(() => {
NodeMirror.reset();
rrdomMirror.reset();
});
it('should return false when two nodes have different Ids', () => {
const node1 = document.createElement('div');
const node2 = rrdom.createElement('div');
NodeMirror.add(node1, getDefaultSN(node2, 1));
rrdomMirror.add(node2, getDefaultSN(node2, 2));
expect(nodeMatching(node1, node2, NodeMirror, rrdomMirror)).toBeFalsy();
});
it('should return false when two nodes have same Ids but different node types', () => {
// Compare an element with a comment node
let node1: Node = document.createElement('div');
NodeMirror.add(node1, getDefaultSN(rrdom.createElement('div'), 1));
let node2: IRRNode = rrdom.createComment('test');
rrdomMirror.add(node2, getDefaultSN(node2, 1));
expect(nodeMatching(node1, node2, NodeMirror, rrdomMirror)).toBeFalsy();
// Compare an element node with a text node
node2 = rrdom.createTextNode('');
rrdomMirror.add(node2, getDefaultSN(node2, 1));
expect(nodeMatching(node1, node2, NodeMirror, rrdomMirror)).toBeFalsy();
// Compare a document with a text node
node1 = new Document();
NodeMirror.add(node1, getDefaultSN(rrdom, 1));
expect(nodeMatching(node1, node2, NodeMirror, rrdomMirror)).toBeFalsy();
// Compare a document with a document type node
node2 = rrdom.createDocumentType('', '', '');
rrdomMirror.add(node2, getDefaultSN(node2, 1));
expect(nodeMatching(node1, node2, NodeMirror, rrdomMirror)).toBeFalsy();
});
it('should compare two elements', () => {
// Compare two elements with different tagNames
let node1 = document.createElement('div');
let node2 = rrdom.createElement('span');
NodeMirror.add(node1, getDefaultSN(rrdom.createElement('div'), 1));
rrdomMirror.add(node2, getDefaultSN(node2, 1));
expect(nodeMatching(node1, node2, NodeMirror, rrdomMirror)).toBeFalsy();
// Compare two elements with same tagNames but different attributes
node2 = rrdom.createElement('div');
node2.setAttribute('class', 'test');
rrdomMirror.add(node2, getDefaultSN(node2, 1));
expect(nodeMatching(node1, node2, NodeMirror, rrdomMirror)).toBeTruthy();
// Should return false when two elements have same tagNames and attributes but different children
rrdomMirror.add(node2, getDefaultSN(node2, 2));
expect(nodeMatching(node1, node2, NodeMirror, rrdomMirror)).toBeFalsy();
});
});
});
================================================
FILE: packages/rrdom/test/document.test.ts
================================================
/**
* @jest-environment jsdom
*/
import { NodeType as RRNodeType } from '@rrweb/types';
import {
BaseRRDocument,
BaseRRDocumentType,
BaseRRMediaElement,
BaseRRNode,
IRRDocumentType,
IRRNode,
} from '../src/document';
describe('Basic RRDocument implementation', () => {
const RRNode = class extends BaseRRNode {
public textContent: string | null;
};
const RRDocument = BaseRRDocument;
const RRDocumentType = BaseRRDocumentType;
class RRMediaElement extends BaseRRMediaElement {}
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.previousSibling).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 and set first child node', () => {
const parentNode = new RRNode();
const childNode1 = new RRNode();
expect(parentNode.firstChild).toBeNull();
parentNode.firstChild = childNode1;
expect(parentNode.firstChild).toBe(childNode1);
parentNode.firstChild = null;
expect(parentNode.firstChild).toBeNull();
});
it('can get and set last child node', () => {
const parentNode = new RRNode();
const childNode1 = new RRNode();
expect(parentNode.lastChild).toBeNull();
parentNode.lastChild = childNode1;
expect(parentNode.lastChild).toBe(childNode1);
parentNode.lastChild = null;
expect(parentNode.lastChild).toBeNull();
});
it('can get and set preSibling', () => {
const node1 = new RRNode();
const node2 = new RRNode();
expect(node1.previousSibling).toBeNull();
node1.previousSibling = node2;
expect(node1.previousSibling).toBe(node2);
node1.previousSibling = null;
expect(node1.previousSibling).toBeNull();
});
it('can get and set nextSibling', () => {
const node1 = new RRNode();
const node2 = new RRNode();
expect(node1.nextSibling).toBeNull();
node1.nextSibling = node2;
expect(node1.nextSibling).toBe(node2);
node1.nextSibling = null;
expect(node1.nextSibling).toBeNull();
});
it('can get childNodes', () => {
const parentNode = new RRNode();
expect(parentNode.childNodes).toBeInstanceOf(Array);
expect(parentNode.childNodes.length).toBe(0);
const childNode1 = new RRNode();
parentNode.firstChild = childNode1;
parentNode.lastChild = childNode1;
expect(parentNode.childNodes).toEqual([childNode1]);
const childNode2 = new RRNode();
parentNode.lastChild = childNode2;
childNode1.nextSibling = childNode2;
childNode2.previousSibling = childNode1;
expect(parentNode.childNodes).toEqual([childNode1, childNode2]);
const childNode3 = new RRNode();
parentNode.lastChild = childNode3;
childNode2.nextSibling = childNode3;
childNode3.previousSibling = childNode2;
expect(parentNode.childNodes).toEqual([
childNode1,
childNode2,
childNode3,
]);
});
it('should return whether the node contains another node', () => {
const parentNode = new RRNode();
expect(parentNode.contains(parentNode)).toBeTruthy();
expect(parentNode.contains(null as unknown as IRRNode)).toBeFalsy();
expect(parentNode.contains(undefined as unknown as IRRNode)).toBeFalsy();
expect(parentNode.contains({} as unknown as IRRNode)).toBeFalsy();
expect(
parentNode.contains(new RRDocument().createElement('div')),
).toBeFalsy();
const childNode1 = new RRNode();
const childNode2 = new RRNode();
parentNode.firstChild = childNode1;
parentNode.lastChild = childNode1;
childNode1.parentNode = parentNode;
expect(parentNode.contains(childNode1)).toBeTruthy();
expect(parentNode.contains(childNode2)).toBeFalsy();
parentNode.lastChild = childNode2;
childNode1.nextSibling = childNode2;
childNode2.previousSibling = childNode1;
childNode2.parentNode = childNode1;
expect(parentNode.contains(childNode1)).toBeTruthy();
expect(parentNode.contains(childNode2)).toBeTruthy();
const childNode3 = new RRNode();
expect(parentNode.contains(childNode3)).toBeFalsy();
childNode2.firstChild = childNode3;
childNode2.lastChild = childNode3;
childNode3.parentNode = childNode2;
expect(parentNode.contains(childNode3)).toBeTruthy();
});
it('should not implement appendChild', () => {
const parentNode = new RRNode();
const childNode = new RRNode();
expect(() => parentNode.appendChild(childNode)).toThrowError(
`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)).toThrowError(
`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)).toThrowError(
`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).toBe(node);
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.previousSibling).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)).toThrowError(
`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)).toThrowError(
`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)).toThrowError(
`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)).toThrowError(
`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)).toThrowError(
`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)).toThrowError(
`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(
`[Error: Failed to execute 'removeChild' on 'RRNode': 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(
'',
);
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(
'',
);
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.previousSibling).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.previousSibling).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.createElement('span');
expect(node.appendChild(child1)).toBe(child1);
expect(node.childNodes[0]).toBe(child1);
expect(node.firstChild).toBe(child1);
expect(node.lastChild).toBe(child1);
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBeNull();
expect(child1.parentElement).toBe(node);
expect(child1.parentNode).toBe(node);
expect(child1.ownerDocument).toBe(document);
expect(node.contains(child1)).toBeTruthy();
const child2 = document.createElement('p');
expect(node.appendChild(child2)).toBe(child2);
expect(node.childNodes[1]).toBe(child2);
expect(node.firstChild).toBe(child1);
expect(node.lastChild).toBe(child2);
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBe(child2);
expect(child2.previousSibling).toBe(child1);
expect(child2.nextSibling).toBeNull();
expect(child2.parentElement).toBe(node);
expect(child2.parentNode).toBe(node);
expect(child2.ownerDocument).toBe(document);
expect(node.contains(child1)).toBeTruthy();
expect(node.contains(child2)).toBeTruthy();
});
it('can append a child with parent node', () => {
const node = document.createElement('div');
const child = document.createElement('span');
expect(node.appendChild(child)).toBe(child);
expect(node.childNodes).toEqual([child]);
expect(node.appendChild(child)).toBe(child);
expect(node.childNodes).toEqual([child]);
expect(child.parentNode).toBe(node);
const node1 = document.createElement('div');
expect(node1.appendChild(child)).toBe(child);
expect(node1.childNodes).toEqual([child]);
expect(child.parentNode).toBe(node1);
expect(node.childNodes).toEqual([]);
});
it('can insert new child before an existing child', () => {
const node = document.createElement('div');
const child1 = document.createElement('h1');
const child2 = document.createElement('h2');
const child3 = document.createElement('h3');
expect(() =>
node.insertBefore(node, child1),
).toThrowErrorMatchingInlineSnapshot(
`[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.]`,
);
expect(node.insertBefore(child1, null)).toBe(child1);
expect(node.childNodes[0]).toBe(child1);
expect(node.childNodes.length).toBe(1);
expect(node.firstChild).toBe(child1);
expect(node.lastChild).toBe(child1);
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBeNull();
expect(child1.parentNode).toBe(node);
expect(child1.parentElement).toBe(node);
expect(child1.ownerDocument).toBe(document);
expect(node.contains(child1)).toBeTruthy();
expect(node.insertBefore(child2, child1)).toBe(child2);
expect(node.childNodes).toEqual([child2, child1]);
expect(node.firstChild).toBe(child2);
expect(node.lastChild).toBe(child1);
expect(child1.previousSibling).toBe(child2);
expect(child1.nextSibling).toBeNull();
expect(child2.previousSibling).toBeNull();
expect(child2.nextSibling).toBe(child1);
expect(child2.parentNode).toBe(node);
expect(child2.parentElement).toBe(node);
expect(child2.ownerDocument).toBe(document);
expect(node.contains(child2)).toBeTruthy();
expect(node.contains(child1)).toBeTruthy();
expect(node.insertBefore(child3, child1)).toBe(child3);
expect(node.childNodes).toEqual([child2, child3, child1]);
expect(node.firstChild).toBe(child2);
expect(node.lastChild).toBe(child1);
expect(child1.previousSibling).toBe(child3);
expect(child1.nextSibling).toBeNull();
expect(child3.previousSibling).toBe(child2);
expect(child3.nextSibling).toBe(child1);
expect(child2.previousSibling).toBeNull();
expect(child2.nextSibling).toBe(child3);
expect(child3.parentNode).toBe(node);
expect(child3.parentElement).toBe(node);
expect(child3.ownerDocument).toBe(document);
expect(node.contains(child2)).toBeTruthy();
expect(node.contains(child3)).toBeTruthy();
expect(node.contains(child1)).toBeTruthy();
});
it('can insert a child with parent node', () => {
const node = document.createElement('div');
const child1 = document.createElement('h1');
expect(node.insertBefore(child1, null)).toBe(child1);
expect(node.childNodes).toEqual([child1]);
expect(node.insertBefore(child1, child1)).toBe(child1);
expect(node.childNodes).toEqual([child1]);
expect(child1.parentNode).toEqual(node);
const node2 = document.createElement('div');
const child2 = document.createElement('h2');
expect(node2.insertBefore(child2, null)).toBe(child2);
expect(node2.childNodes).toEqual([child2]);
expect(node2.insertBefore(child1, child2)).toBe(child1);
expect(node2.childNodes).toEqual([child1, child2]);
expect(child1.parentNode).toEqual(node2);
expect(node.childNodes).toEqual([]);
});
it('can remove an existing child', () => {
const node = document.createElement('div');
const child1 = document.createElement('h1');
const child2 = document.createElement('h2');
const child3 = document.createElement('h3');
node.appendChild(child1);
node.appendChild(child2);
node.appendChild(child3);
expect(node.childNodes).toEqual([child1, child2, child3]);
expect(() =>
node.removeChild(document.createElement('div')),
).toThrowErrorMatchingInlineSnapshot(
`[Error: Failed to execute 'removeChild' on 'RRNode': The RRNode to be removed is not a child of this RRNode.]`,
);
// Remove the middle child.
expect(node.removeChild(child2)).toBe(child2);
expect(node.childNodes).toEqual([child1, child3]);
expect(node.contains(child2)).toBeFalsy();
expect(node.firstChild).toBe(child1);
expect(node.lastChild).toBe(child3);
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBe(child3);
expect(child3.previousSibling).toBe(child1);
expect(child3.nextSibling).toBeNull();
expect(child2.previousSibling).toBeNull();
expect(child2.nextSibling).toBeNull();
expect(child2.parentNode).toBeNull();
expect(child2.parentElement).toBeNull();
// Remove the previous child.
expect(node.removeChild(child1)).toBe(child1);
expect(node.childNodes).toEqual([child3]);
expect(node.contains(child1)).toBeFalsy();
expect(node.firstChild).toBe(child3);
expect(node.lastChild).toBe(child3);
expect(child3.previousSibling).toBeNull();
expect(child3.nextSibling).toBeNull();
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBeNull();
expect(child1.parentNode).toBeNull();
expect(child1.parentElement).toBeNull();
node.insertBefore(child1, child3);
expect(node.childNodes).toEqual([child1, child3]);
// Remove the next child.
expect(node.removeChild(child3)).toBe(child3);
expect(node.childNodes).toEqual([child1]);
expect(node.contains(child3)).toBeFalsy();
expect(node.contains(child1)).toBeTruthy();
expect(node.firstChild).toBe(child1);
expect(node.lastChild).toBe(child1);
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBeNull();
expect(child3.previousSibling).toBeNull();
expect(child3.nextSibling).toBeNull();
expect(child3.parentNode).toBeNull();
expect(child3.parentElement).toBeNull();
// Remove all children.
expect(node.removeChild(child1)).toBe(child1);
expect(node.childNodes).toEqual([]);
expect(node.contains(child1)).toBeFalsy();
expect(node.contains(child2)).toBeFalsy();
expect(node.contains(child3)).toBeFalsy();
expect(node.firstChild).toBeNull();
expect(node.lastChild).toBeNull();
expect(child1.previousSibling).toBeNull();
expect(child1.nextSibling).toBeNull();
expect(child1.parentNode).toBeNull();
expect(child1.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.previousSibling).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.previousSibling).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.previousSibling).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.previousSibling).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.playbackRate).toBeUndefined();
expect(node.loop).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' })).toThrowError(
`RRDomException: Failed to execute 'attachShadow' on 'RRElement': This RRElement does not support attachShadow`,
);
});
});
});
================================================
FILE: packages/rrdom/test/html/iframe.html
================================================
Iframe
================================================
FILE: packages/rrdom/test/html/main.html
================================================
Main
This is a h1 heading
This is a h1 heading with styles
Text 1
This is a paragraph
button1
Text 2
================================================
FILE: packages/rrdom/test/html/shadow-dom.html
================================================
shadow dom
shadow dom one
shadow dom two
================================================
FILE: packages/rrdom/test/virtual-dom.test.ts
================================================
/**
* @jest-environment jsdom
*/
import * as fs from 'fs';
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import { vi } from 'vitest';
import { JSDOM } from 'jsdom';
import { buildNodeWithSN, Mirror } from 'rrweb-snapshot';
import {
cdataNode,
commentNode,
documentNode,
documentTypeNode,
elementNode,
NodeType,
NodeType as RRNodeType,
textNode,
} from '@rrweb/types';
import {
buildFromDom,
buildFromNode,
createMirror,
getDefaultSN,
RRCanvasElement,
RRDocument,
RRElement,
BaseRRNode as RRNode,
} from '../src';
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', () => {
vi.setConfig({ testTimeout: 60_000 });
let mirror: Mirror;
beforeEach(() => {
mirror = new Mirror();
});
describe('create a RRNode from a real Node', () => {
it('should support quicksmode documents', () => {
// separate 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(-2);
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(-2);
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(-3);
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(-3);
// 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(-4);
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(-4);
// 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(-5);
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(-5);
// 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(-6);
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(-6);
// build from CDATASection
const xmlDoc = new DOMParser().parseFromString(
' ',
'application/xml',
);
const cdata = 'Some 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(-7);
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(-7);
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(-2);
expect(mirror.getId(iframe.contentDocument)).toEqual(-2);
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(-2);
expect(mirror.getId(div.shadowRoot)).toEqual(-2);
expect(rrNode.RRNodeType).toEqual(RRNodeType.Element);
expect((rrNode as RRElement).tagName).toEqual('SHADOWROOT');
expect(rrNode).toBe(parentRRNode.shadowRoot);
});
it('can rebuild blocked element with correct dimensions', () => {
// @ts-expect-error Testing buildNodeWithSN with rr elements
const node = buildNodeWithSN(
{
id: 1,
tagName: 'svg',
type: NodeType.Element,
isSVG: true,
attributes: {
rr_width: '50px',
rr_height: '50px',
},
childNodes: [],
},
{
// @ts-expect-error
doc: new RRDocument(),
mirror,
blockSelector: '*',
slimDOMOptions: {},
},
) as RRElement;
expect(node.style.width).toBe('50px');
expect(node.style.height).toBe('50px');
});
});
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({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
code = fs.readFileSync(
path.resolve(__dirname, '../dist/rrdom.umd.cjs'),
'utf8',
);
});
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(' ', 'application/xml');
var cdata = docu.createCDATASection('Some data & then some');
docu.getElementsByTagName('xml')[0].appendChild(cdata);
// Displays: data & then some]]>
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 = 2; 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(-2);
expect(dom.unserializedId).toBe(-3);
expect(dom.close());
expect(dom.open());
expect(dom.childNodes.length).toEqual(0);
expect(dom.unserializedId).toBe(-2);
});
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');
}
================================================
FILE: packages/rrdom/tsconfig.json
================================================
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
"rootDir": "src",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"references": [
{
"path": "../rrweb-snapshot"
},
{
"path": "../types"
}
]
}
================================================
FILE: packages/rrdom/vite.config.js
================================================
import path from 'path';
import config from '../../vite.config.default';
export default config(path.resolve(__dirname, 'src/index.ts'), 'rrdom');
================================================
FILE: packages/rrdom/vitest.config.ts
================================================
///
import { defineProject, mergeConfig } from 'vitest/config';
import configShared from '../../vitest.config';
export default mergeConfig(
configShared,
defineProject({
test: {
globals: true,
},
}),
);
================================================
FILE: packages/rrdom-nodejs/.gitignore
================================================
dist
es
lib
typings
================================================
FILE: packages/rrdom-nodejs/.vscode/extensions.json
================================================
{
"recommendations": ["vitest.explorer"]
}
================================================
FILE: packages/rrdom-nodejs/.vscode/launch.json
================================================
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"autoAttachChildProcesses": true,
"skipFiles": ["/**", "**/node_modules/**"],
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": ["run", "${relativeFile}"],
"smartStep": true,
"console": "integratedTerminal"
}
]
}
================================================
FILE: packages/rrdom-nodejs/CHANGELOG.md
================================================
# rrdom-nodejs
## 2.0.0-alpha.20
### Patch Changes
- Updated dependencies []:
- rrdom@2.0.0-alpha.20
- @rrweb/types@2.0.0-alpha.20
## 2.0.0-alpha.19
### Patch Changes
- Updated dependencies []:
- rrdom@2.0.0-alpha.19
- @rrweb/types@2.0.0-alpha.19
## 2.0.0-alpha.18
### Patch Changes
- [#1593](https://github.com/rrweb-io/rrweb/pull/1593) [`5a78938`](https://github.com/rrweb-io/rrweb/commit/5a789385a341311ba327a768fe0e2f0f2f5002ee) Thanks [@daibhin](https://github.com/daibhin)! - `NodeType` enum was moved from rrweb-snapshot to @rrweb/types
The following types where moved from rrweb-snapshot to @rrweb/types: `documentNode`, `documentTypeNode`, `legacyAttributes`, `textNode`, `cdataNode`, `commentNode`, `elementNode`, `serializedNode`, `serializedNodeWithId`, `serializedElementNodeWithId`, `serializedTextNodeWithId`, `IMirror`, `INode`, `mediaAttributes`, `attributes` and `DataURLOptions`
- Updated dependencies [[`8e55c45`](https://github.com/rrweb-io/rrweb/commit/8e55c455ff2987a3b5f367f23f48c1f2de74ce45), [`5a78938`](https://github.com/rrweb-io/rrweb/commit/5a789385a341311ba327a768fe0e2f0f2f5002ee)]:
- rrdom@2.0.0-alpha.18
- @rrweb/types@2.0.0-alpha.18
## 2.0.0-alpha.17
### Patch Changes
- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`d350da8`](https://github.com/rrweb-io/rrweb/commit/d350da8552d8616dd118ee550bdfbce082986562), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]:
- rrweb-snapshot@2.0.0-alpha.17
- rrdom@2.0.0-alpha.17
## 2.0.0-alpha.16
### Patch Changes
- Updated dependencies [[`a2c8a1a`](https://github.com/rrweb-io/rrweb/commit/a2c8a1a37bfcf8389b280af792262c8263a979a3), [`d08624c`](https://github.com/rrweb-io/rrweb/commit/d08624cb28add386c3618a0e6607424c3f1884d8)]:
- rrweb-snapshot@2.0.0-alpha.16
- rrdom@2.0.0-alpha.16
## 2.0.0-alpha.15
### Major Changes
- [#1497](https://github.com/rrweb-io/rrweb/pull/1497) [`2606a2a`](https://github.com/rrweb-io/rrweb/commit/2606a2a28f2a6d897b8ae4ea3ec40ef0eeacbfaf) Thanks [@Juice10](https://github.com/Juice10)! - Distributed files have new filenames, paths and extensions. **Important: If you reference distributed files or types directly, you might have to update your paths/filenames. E.g. you import from `rrweb/typings/...` or `rrdom/es`. However you run `import rrweb from 'rrweb'` you won't notice a difference with this change.** If you include rrweb files directly in a script tag, you might have to update that path to include a the `.umd.cjs` files instead. All `.js` files now use ES modules which can be used in modern browsers, node.js and bundlers that support ES modules. All npm packages now also ship `.cjs` and `.umd.cjs` files. The `.umd.cjs` files are CommonJS modules that bundle all files together to make it easy to ship one file to browser environments (similar to the previous `.js` files). The `.cjs` files are CommonJS modules that can be used in older Node.js environments. Types should be better defined in `package.json` and if you need specific types they might be exported from new packages (for example `PlayerMachineState` and `SpeedMachineState` are now exported from `@rrweb/replay`). Check the `package.json`'s `main` and `exports` field for the available files.
### Patch Changes
- Updated dependencies [[`4014305`](https://github.com/rrweb-io/rrweb/commit/40143059446cee5c042c007b1c2e976f36e172f5), [`82f6fec`](https://github.com/rrweb-io/rrweb/commit/82f6fecf36413ecbc994a510144487f1de20d1d5), [`2606a2a`](https://github.com/rrweb-io/rrweb/commit/2606a2a28f2a6d897b8ae4ea3ec40ef0eeacbfaf), [`f3cf092`](https://github.com/rrweb-io/rrweb/commit/f3cf0928df30d5ed5c0d573c524be6e744c0f8d3), [`e08706a`](https://github.com/rrweb-io/rrweb/commit/e08706ae60268b6eb05c6292ef948c71bd423ce3)]:
- rrweb-snapshot@2.0.0-alpha.15
- rrdom@2.0.0-alpha.15
## 2.0.0-alpha.14
### Patch Changes
- Updated dependencies [[`03b5216`](https://github.com/rrweb-io/rrweb/commit/03b5216a9403f1509b4f69d1d71ef9874277fe91), [`46f1b25`](https://github.com/rrweb-io/rrweb/commit/46f1b252a5919c68c68e825bd6089cc2e7d34e7c), [`cbbd1e5`](https://github.com/rrweb-io/rrweb/commit/cbbd1e55f1f7fa2eed9fa11e4152b509bdfd88f7), [`5e7943d`](https://github.com/rrweb-io/rrweb/commit/5e7943dbae6e2cde76c484bdd26bc0b96f1b6dce), [`c0f83af`](https://github.com/rrweb-io/rrweb/commit/c0f83afab8f1565633de0e986b7e96fa56f2d25c), [`e96f668`](https://github.com/rrweb-io/rrweb/commit/e96f668c86bd0ab5dc190bb2957a170271bb2ebc)]:
- rrweb-snapshot@2.0.0-alpha.14
- rrdom@2.0.0-alpha.14
## 2.0.0-alpha.13
### Patch Changes
- Updated dependencies [[`123a81e`](https://github.com/rrweb-io/rrweb/commit/123a81e12d072cd95d701231176d7eb2d03b3961), [`123a81e`](https://github.com/rrweb-io/rrweb/commit/123a81e12d072cd95d701231176d7eb2d03b3961), [`f7c6973`](https://github.com/rrweb-io/rrweb/commit/f7c6973ae9c21b9ea014bdef7101f976f04d9356)]:
- rrdom@2.0.0-alpha.13
- rrweb-snapshot@2.0.0-alpha.13
## 2.0.0-alpha.12
### Patch Changes
- Updated dependencies [[`58c9104`](https://github.com/rrweb-io/rrweb/commit/58c9104eddc8b7994a067a97daae5684e42f892f), [`a2be77b`](https://github.com/rrweb-io/rrweb/commit/a2be77b82826c4be0e7f3c7c9f7ee50476d5f6f8), [`a7c33f2`](https://github.com/rrweb-io/rrweb/commit/a7c33f2093c4d92faf7ae25e8bb0e088d122c13b), [`8aea5b0`](https://github.com/rrweb-io/rrweb/commit/8aea5b00a4dfe5a6f59bd2ae72bb624f45e51e81), [`314a8dd`](https://github.com/rrweb-io/rrweb/commit/314a8dde5a13095873b89d07bac7c949918bf817), [`e607e83`](https://github.com/rrweb-io/rrweb/commit/e607e83b21d45131a56c1ff606e9519a5b475fc1), [`7c0dc9d`](https://github.com/rrweb-io/rrweb/commit/7c0dc9dfe1564c9d6624557c5b394e7844955882), [`07ac5c9`](https://github.com/rrweb-io/rrweb/commit/07ac5c9e1371824ec3ffb705f9250bbe10f4b73e)]:
- rrweb-snapshot@2.0.0-alpha.12
- rrdom@2.0.0-alpha.12
## 2.0.0-alpha.11
### Patch Changes
- Updated dependencies [[`11f6567`](https://github.com/rrweb-io/rrweb/commit/11f6567fd81ef9ed0f954a7b6d5e39653f56004f), [`efdc167`](https://github.com/rrweb-io/rrweb/commit/efdc167ca6c039d04af83612e3d92498bb9b41a7), [`efdc167`](https://github.com/rrweb-io/rrweb/commit/efdc167ca6c039d04af83612e3d92498bb9b41a7)]:
- rrweb-snapshot@2.0.0-alpha.11
- rrdom@2.0.0-alpha.11
## 2.0.0-alpha.10
### Patch Changes
- Updated dependencies [[`c6600e7`](https://github.com/rrweb-io/rrweb/commit/c6600e742b8ec0b6295816bb5de9edcd624d975e)]:
- rrweb-snapshot@2.0.0-alpha.10
- rrdom@2.0.0-alpha.10
## 2.0.0-alpha.9
### Patch Changes
- Updated dependencies [[`b798f2d`](https://github.com/rrweb-io/rrweb/commit/b798f2dbc07b5a24dcaf40d164159200b6c0679d), [`d7c72bf`](https://github.com/rrweb-io/rrweb/commit/d7c72bff0724b46a6fa94af455220626a27104fe)]:
- rrdom@2.0.0-alpha.9
- rrweb-snapshot@2.0.0-alpha.9
## 2.0.0-alpha.8
### Patch Changes
- Updated dependencies [[`bc84246`](https://github.com/rrweb-io/rrweb/commit/bc84246f78849a80dbb8fe9b4e76117afcc5c3f7), [`d0fdc0f`](https://github.com/rrweb-io/rrweb/commit/d0fdc0f273bb156a1faab4782b40fbec8dccf915)]:
- rrweb-snapshot@2.0.0-alpha.8
- rrdom@2.0.0-alpha.8
## 2.0.0-alpha.7
### Patch Changes
- Updated dependencies [[`d2582e9`](https://github.com/rrweb-io/rrweb/commit/d2582e9a81197130cd93bc1dd778e16fddfb0be3), [`e7f0c80`](https://github.com/rrweb-io/rrweb/commit/e7f0c808c3f348fb27d1acd5fa300a5d92b14d00)]:
- rrweb-snapshot@2.0.0-alpha.7
- rrdom@2.0.0-alpha.7
## 2.0.0-alpha.6
### Patch Changes
- Updated dependencies [[`c28ef5f`](https://github.com/rrweb-io/rrweb/commit/c28ef5f658abb93086504581409cf7a376db48dc), [`f6f07e9`](https://github.com/rrweb-io/rrweb/commit/f6f07e953376634a4caf28ff8cbfed5a017c4347), [`eac9b18`](https://github.com/rrweb-io/rrweb/commit/eac9b18bbfa3c350797b99b583dd93a5fc32b828), [`f27e545`](https://github.com/rrweb-io/rrweb/commit/f27e545e1871ed2c1753d37543f556e8ddc406b4), [`8e47ca1`](https://github.com/rrweb-io/rrweb/commit/8e47ca1021ebb4fc036b37623ef10abf7976d6dd)]:
- rrweb-snapshot@2.0.0-alpha.6
- rrdom@2.0.0-alpha.6
## 2.0.0-alpha.5
### Major Changes
- [#1127](https://github.com/rrweb-io/rrweb/pull/1127) [`3cc4323`](https://github.com/rrweb-io/rrweb/commit/3cc4323094065a12f8b65afecd45061d604e245f) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - Refactor: Improve performance by 80% in a super large benchmark case.
1. Refactor: change the data structure of childNodes from array to linked list
2. Improve the performance of the "contains" function. New algorithm will reduce the complexity from O(n) to O(logn)
### Patch Changes
- [#1126](https://github.com/rrweb-io/rrweb/pull/1126) [`227d43a`](https://github.com/rrweb-io/rrweb/commit/227d43abb93d57cadc70c760b28c46911bf7d8ff) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - Refactor all suffix of bundled scripts with commonjs module from 'js' to cjs [#1087](https://github.com/rrweb-io/rrweb/pull/1087).
- Updated dependencies [[`1385f7a`](https://github.com/rrweb-io/rrweb/commit/1385f7acc0052f83be1458a7b00e18c026ee393f), [`227d43a`](https://github.com/rrweb-io/rrweb/commit/227d43abb93d57cadc70c760b28c46911bf7d8ff), [`227d43a`](https://github.com/rrweb-io/rrweb/commit/227d43abb93d57cadc70c760b28c46911bf7d8ff), [`3cc4323`](https://github.com/rrweb-io/rrweb/commit/3cc4323094065a12f8b65afecd45061d604e245f)]:
- rrweb-snapshot@2.0.0-alpha.5
- rrdom@2.0.0-alpha.5
================================================
FILE: packages/rrdom-nodejs/README.md
================================================
# rrdom-nodejs
`rrdom-nodejs` is a Node.js implementation of the [`rrdom`](../rrdom/) library. It allows you to replay and inspect recorded user interactions with `rrweb` in a Node.js environment.
See the [guide](../../guide.md) for more info on rrweb.
## Sponsors
[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.
### Gold Sponsors 🥇
### Silver Sponsors 🥈
### Bronze Sponsors 🥉
### Backers
## Core Team Members
## Who's using rrweb?
================================================
FILE: packages/rrdom-nodejs/package.json
================================================
{
"name": "rrdom-nodejs",
"version": "2.0.0-alpha.20",
"scripts": {
"dev": "vite build --watch",
"build": "yarn turbo run prepublish",
"check-types": "tsc -noEmit",
"test": "vitest run",
"test:watch": "vitest watch",
"prepublish": "tsc -noEmit && vite build",
"lint": "yarn eslint src/**/*.ts"
},
"keywords": [
"rrweb",
"rrdom-nodejs"
],
"license": "MIT",
"type": "module",
"main": "./dist/rrdom-nodejs.umd.cjs",
"module": "./dist/rrdom-nodejs.js",
"unpkg": "./dist/rrdom-nodejs.umd.cjs",
"typings": "dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/rrdom-nodejs.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/rrdom-nodejs.umd.cjs"
}
}
},
"files": [
"umd",
"dist",
"package.json"
],
"devDependencies": {
"@types/cssom": "^0.4.1",
"@types/cssstyle": "^2.2.1",
"@types/nwsapi": "^2.2.2",
"@types/puppeteer": "^5.4.4",
"@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.23.0",
"compare-versions": "^4.1.3",
"eslint": "^8.15.0",
"puppeteer": "^9.1.1",
"vite": "^6.0.1",
"vite-plugin-dts": "^3.9.1",
"vitest": "^1.4.0",
"typescript": "^5.4.5"
},
"dependencies": {
"cssom": "^0.5.0",
"cssstyle": "^2.3.0",
"nwsapi": "2.2.0",
"rrdom": "^2.0.0-alpha.20",
"@rrweb/types": "^2.0.0-alpha.20"
}
}
================================================
FILE: packages/rrdom-nodejs/src/document-nodejs.ts
================================================
import { NodeType as RRNodeType } from '@rrweb/types';
import type { NWSAPI } from 'nwsapi';
import type { CSSStyleDeclaration as CSSStyleDeclarationType } from 'cssstyle';
import {
BaseRRCDATASection,
BaseRRComment,
BaseRRDocument,
BaseRRDocumentType,
BaseRRElement,
BaseRRMediaElement,
BaseRRNode,
BaseRRText,
ClassList,
type IRRDocument,
type CSSStyleDeclaration,
} from 'rrdom';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
const nwsapi = require('nwsapi');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
const cssom = require('cssom');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
const cssstyle = require('cssstyle');
export class RRWindow {
scrollLeft = 0;
scrollTop = 0;
scrollTo(options?: ScrollToOptions) {
if (!options) return;
if (typeof options.left === 'number') this.scrollLeft = options.left;
if (typeof options.top === 'number') this.scrollTop = options.top;
}
}
export class RRDocument extends BaseRRDocument implements IRRDocument {
readonly nodeName = '#document' as const;
private _nwsapi: NWSAPI | undefined;
get nwsapi(): NWSAPI {
if (!this._nwsapi) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
this._nwsapi = nwsapi({
document: this as unknown as Document,
DOMException: null as unknown as new (
message?: string,
name?: string,
) => DOMException,
}) as NWSAPI;
this._nwsapi.configure({
LOGERRORS: false,
IDS_DUPES: true,
MIXEDCASE: true,
});
}
return this._nwsapi;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
get documentElement(): RRElement | null {
return super.documentElement as RRElement | null;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
get body(): RRElement | null {
return super.body as RRElement | null;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
get head() {
return super.head as RRElement | null;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
get implementation(): RRDocument {
return this;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
get firstElementChild(): RRElement | null {
return this.documentElement;
}
appendChild(childNode: BaseRRNode) {
return super.appendChild(childNode);
}
insertBefore(newChild: BaseRRNode, refChild: BaseRRNode | null) {
return super.insertBefore(newChild, refChild);
}
querySelectorAll(selectors: string): BaseRRNode[] {
return this.nwsapi.select(selectors) as unknown as BaseRRNode[];
}
getElementsByTagName(tagName: string): RRElement[] {
if (this.documentElement)
return this.documentElement.getElementsByTagName(tagName);
return [];
}
getElementsByClassName(className: string): RRElement[] {
if (this.documentElement)
return this.documentElement.getElementsByClassName(className);
return [];
}
getElementById(elementId: string): RRElement | null {
if (this.documentElement)
return this.documentElement.getElementById(elementId);
return null;
}
createDocument(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_namespace: string | null,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_qualifiedName: string | null,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_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(
tagName: K,
): RRElementType;
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);
break;
case 'IMG':
element = new RRImageElement(upperTagName);
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;
}
createElementNS(_namespaceURI: string, qualifiedName: string) {
return this.createElement(qualifiedName as keyof HTMLElementTagNameMap);
}
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;
}
}
export class RRDocumentType extends BaseRRDocumentType {}
export class RRElement extends BaseRRElement {
private _style: CSSStyleDeclarationType;
constructor(tagName: string) {
super(tagName);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
this._style = new cssstyle.CSSStyleDeclaration();
const style = this._style;
Object.defineProperty(this.attributes, 'style', {
get() {
return style.cssText;
},
set(cssText: string) {
style.cssText = cssText;
},
});
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
get style() {
return this._style as unknown as CSSStyleDeclaration;
}
attachShadow(_init: ShadowRootInit): RRElement {
return super.attachShadow(_init) as RRElement;
}
appendChild(newChild: BaseRRNode): BaseRRNode {
return super.appendChild(newChild) as BaseRRNode;
}
insertBefore(newChild: BaseRRNode, refChild: BaseRRNode | null): BaseRRNode {
return super.insertBefore(newChild, refChild) as BaseRRNode;
}
getAttribute(name: string) {
const upperName = name && name.toLowerCase();
if (upperName in this.attributes) return this.attributes[upperName];
return null;
}
setAttribute(name: string, attribute: string) {
this.attributes[name.toLowerCase()] = attribute;
}
removeAttribute(name: string) {
delete this.attributes[name.toLowerCase()];
}
get firstElementChild(): RRElement | null {
for (const child of this.childNodes)
if (child.RRNodeType === RRNodeType.Element) return child as RRElement;
return null;
}
get nextElementSibling(): RRElement | null {
const parentNode = this.parentNode;
if (!parentNode) return null;
const siblings = parentNode.childNodes;
const index = siblings.indexOf(this);
for (let i = index + 1; i < siblings.length; i++)
if (siblings[i] instanceof RRElement) return siblings[i] as RRElement;
return null;
}
querySelectorAll(selectors: string): BaseRRNode[] {
const result: RRElement[] = [];
if (this.ownerDocument !== null) {
(this.ownerDocument as RRDocument).nwsapi.select(
selectors,
this as unknown as Element,
(element) => {
if ((element as unknown as RRElement) !== this)
result.push(element as unknown as RRElement);
},
) as unknown as BaseRRNode[];
}
return result;
}
getElementById(elementId: string): RRElement | null {
if (this.id === elementId) return this;
for (const child of this.childNodes) {
if (child instanceof RRElement) {
const result = child.getElementById(elementId);
if (result !== null) return result;
}
}
return null;
}
getElementsByClassName(className: string): RRElement[] {
let elements: RRElement[] = [];
const queryClassList = new ClassList(className);
// Make sure this element has all queried class names.
if (
this instanceof RRElement &&
queryClassList.classes.filter((queriedClassName) =>
this.classList.classes.some((name) => name === queriedClassName),
).length == queryClassList.classes.length
)
elements.push(this);
for (const child of this.childNodes) {
if (child instanceof RRElement)
elements = elements.concat(child.getElementsByClassName(className));
}
return elements;
}
getElementsByTagName(tagName: string): RRElement[] {
let elements: RRElement[] = [];
const normalizedTagName = tagName.toUpperCase();
if (this instanceof RRElement && this.tagName === normalizedTagName)
elements.push(this);
for (const child of this.childNodes) {
if (child instanceof RRElement)
elements = elements.concat(child.getElementsByTagName(tagName));
}
return elements;
}
}
export class RRImageElement extends RRElement {
src = '';
width = 0;
height = 0;
onload: ((this: GlobalEventHandlers, ev: Event) => unknown) | null = null;
}
export class RRMediaElement extends BaseRRMediaElement {}
export class RRCanvasElement extends RRElement {
/**
* This is just a dummy implementation to prevent rrweb replayer from drawing mouse tail. If further analysis of canvas is needed, we may implement it with node-canvas.
*/
getContext(): CanvasRenderingContext2D | null {
return null;
}
}
export class RRStyleElement extends RRElement {
private _sheet: CSSStyleSheet | null = null;
get sheet() {
if (!this._sheet) {
let result = '';
for (const child of this.childNodes)
if (child.RRNodeType === RRNodeType.Text)
result += (child as RRText).textContent;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
this._sheet = cssom.parse(result);
}
return this._sheet;
}
}
export class RRIFrameElement extends RRElement {
width = '';
height = '';
src = '';
contentDocument: RRDocument = new RRDocument();
contentWindow: RRWindow = new RRWindow();
constructor(tagName: string) {
super(tagName);
const htmlElement = this.contentDocument.createElement('HTML');
this.contentDocument.appendChild(htmlElement);
htmlElement.appendChild(this.contentDocument.createElement('HEAD'));
htmlElement.appendChild(this.contentDocument.createElement('BODY'));
}
}
export class RRText extends BaseRRText {
readonly nodeName = '#text' as const;
}
export class RRComment extends BaseRRComment {
readonly nodeName = '#comment' as const;
}
export class RRCDATASection extends BaseRRCDATASection {
readonly nodeName = '#cdata-section' as const;
}
interface RRElementTagNameMap {
audio: RRMediaElement;
canvas: RRCanvasElement;
iframe: RRIFrameElement;
img: RRImageElement;
style: RRStyleElement;
video: RRMediaElement;
}
type RRElementType =
K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement;
================================================
FILE: packages/rrdom-nodejs/src/index.ts
================================================
import {
polyfillPerformance,
polyfillRAF,
polyfillEvent,
polyfillNode,
polyfillDocument,
} from './polyfill';
polyfillPerformance();
polyfillRAF();
polyfillEvent();
polyfillNode();
polyfillDocument();
export * from './document-nodejs';
================================================
FILE: packages/rrdom-nodejs/src/polyfill.ts
================================================
import { BaseRRNode } from 'rrdom';
import { RRDocument } from './document-nodejs';
/**
* Polyfill the performance for nodejs.
* Note: The performance api is available through the global object from nodejs v16.0.0.
* https://github.com/nodejs/node/pull/37970
*/
export function polyfillPerformance() {
if (typeof window !== 'undefined' || 'performance' in global) return;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-var-requires
const performance = require('perf_hooks').performance;
((global as Window & typeof globalThis).performance as unknown) = performance;
}
/**
* Polyfill requestAnimationFrame and cancelAnimationFrame for nodejs.
*/
export function polyfillRAF() {
if (typeof window !== 'undefined' || 'requestAnimationFrame' in global)
return;
const FPS = 60,
INTERVAL = 1_000 / FPS;
let timeoutHandle: NodeJS.Timeout | null = null,
rafCount = 0,
requests = Object.create(null) as Record void>;
function onFrameTimer() {
const currentRequests = requests;
requests = Object.create(null) as Record void>;
timeoutHandle = null;
Object.keys(currentRequests).forEach(function (id) {
const request = currentRequests[id];
if (request) request(Date.now());
});
}
function requestAnimationFrame(callback: (timestamp: number) => void) {
const cbHandle = ++rafCount;
requests[cbHandle] = callback;
if (timeoutHandle === null)
timeoutHandle = setTimeout(onFrameTimer, INTERVAL);
return cbHandle;
}
function cancelAnimationFrame(handleId: number) {
delete requests[handleId];
if (Object.keys(requests).length === 0 && timeoutHandle !== null) {
clearTimeout(timeoutHandle);
timeoutHandle = null;
}
}
(global as Window & typeof globalThis).requestAnimationFrame =
requestAnimationFrame;
(global as Window & typeof globalThis).cancelAnimationFrame =
cancelAnimationFrame;
}
/**
* Try to polyfill Event type.
* The implementation of Event so far is empty because rrweb doesn't strongly depend on it in nodejs mode.
* Note: The Event class is available through the global object from nodejs v15.0.0.
*/
export function polyfillEvent() {
if (typeof Event !== 'undefined') return;
(global.Event as unknown) = function () {
//
};
}
/**
* Polyfill Node type with BaseRRNode for nodejs.
*/
export function polyfillNode() {
if (typeof Node !== 'undefined') return;
(global.Node as unknown) = BaseRRNode;
}
/**
* Polyfill document object with RRDocument for nodejs.
*/
export function polyfillDocument() {
if (typeof document !== 'undefined') return;
const rrdom = new RRDocument();
(() => {
rrdom.appendChild(rrdom.createElement('html'));
rrdom.documentElement?.appendChild(rrdom.createElement('head'));
rrdom.documentElement?.appendChild(rrdom.createElement('body'));
})();
global.document = rrdom as unknown as Document;
}
================================================
FILE: packages/rrdom-nodejs/test/document-nodejs.test.ts
================================================
/**
* @vitest-environment jsdom
*/
import { describe, it, expect, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { NodeType as RRNodeType } from '@rrweb/types';
import {
RRCanvasElement,
RRCDATASection,
RRComment,
RRDocument,
RRElement,
RRIFrameElement,
RRImageElement,
RRMediaElement,
RRStyleElement,
RRText,
} from '../src/document-nodejs';
import { buildFromDom } from 'rrdom';
describe('RRDocument for nodejs environment', () => {
describe('RRDocument API', () => {
let rrdom: RRDocument;
beforeAll(() => {
// initialize rrdom
document.write(getHtml('main.html'));
rrdom = new RRDocument();
buildFromDom(document, undefined, rrdom);
});
it('can create different type of RRNodes', () => {
const document = rrdom.createDocument('', '');
expect(document).toBeInstanceOf(RRDocument);
const audio = rrdom.createElement('audio');
expect(audio).toBeInstanceOf(RRMediaElement);
const video = rrdom.createElement('video');
expect(video).toBeInstanceOf(RRMediaElement);
const iframe = rrdom.createElement('iframe');
expect(iframe).toBeInstanceOf(RRIFrameElement);
const image = rrdom.createElement('img');
expect(image).toBeInstanceOf(RRImageElement);
const canvas = rrdom.createElement('canvas');
expect(canvas).toBeInstanceOf(RRCanvasElement);
const style = rrdom.createElement('style');
expect(style).toBeInstanceOf(RRStyleElement);
const elementNS = rrdom.createElementNS(
'http://www.w3.org/2000/svg',
'div',
);
expect(elementNS).toBeInstanceOf(RRElement);
expect(elementNS.tagName).toEqual('DIV');
const text = rrdom.createTextNode('text');
expect(text).toBeInstanceOf(RRText);
expect(text.textContent).toEqual('text');
const comment = rrdom.createComment('comment');
expect(comment).toBeInstanceOf(RRComment);
expect(comment.textContent).toEqual('comment');
const CDATA = rrdom.createCDATASection('data');
expect(CDATA).toBeInstanceOf(RRCDATASection);
expect(CDATA.data).toEqual('data');
});
it('can get head element', () => {
expect(rrdom.head).toBeDefined();
expect(rrdom.head!.tagName).toBe('HEAD');
expect(rrdom.head!.parentElement).toBe(rrdom.documentElement);
});
it('can get body element', () => {
expect(rrdom.body).toBeDefined();
expect(rrdom.body!.tagName).toBe('BODY');
expect(rrdom.body!.parentElement).toBe(rrdom.documentElement);
});
it('can get implementation', () => {
expect(rrdom.implementation).toBeDefined();
expect(rrdom.implementation).toBe(rrdom);
});
it('can insert elements', () => {
expect(() =>
rrdom.insertBefore(rrdom.createDocumentType('', '', ''), null),
).toThrowErrorMatchingInlineSnapshot(
`[Error: RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRDoctype on RRDocument allowed.]`,
);
expect(() =>
rrdom.insertBefore(rrdom.createElement('div'), null),
).toThrowErrorMatchingInlineSnapshot(
`[Error: RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRElement on RRDocument allowed.]`,
);
const node = new RRDocument();
const doctype = rrdom.createDocumentType('', '', '');
const documentElement = node.createElement('html');
node.insertBefore(documentElement, null);
node.insertBefore(doctype, documentElement);
expect(node.childNodes.length).toEqual(2);
expect(node.childNodes[0]).toBe(doctype);
expect(node.childNodes[1]).toBe(documentElement);
expect(node.documentElement).toBe(documentElement);
});
it('get firstElementChild', () => {
expect(rrdom.firstElementChild).toBeDefined();
expect(rrdom.firstElementChild!.tagName).toEqual('HTML');
const div1 = rrdom.getElementById('block1');
expect(div1).toBeDefined();
expect(div1!.firstElementChild).toBeDefined();
expect(div1!.firstElementChild!.id).toEqual('block2');
const div2 = div1!.firstElementChild;
expect(div2!.firstElementChild!.id).toEqual('block3');
});
it('getElementsByTagName', () => {
for (let tagname of [
'HTML',
'BODY',
'HEAD',
'STYLE',
'META',
'TITLE',
'SCRIPT',
'LINK',
'DIV',
'H1',
'P',
'BUTTON',
'IMG',
'CANVAS',
'FORM',
'INPUT',
]) {
const expectedResult = document.getElementsByTagName(tagname).length;
expect(rrdom.getElementsByTagName(tagname).length).toEqual(
expectedResult,
);
expect(
rrdom.getElementsByTagName(tagname.toLowerCase()).length,
).toEqual(expectedResult);
for (let node of rrdom.getElementsByTagName(tagname)) {
expect(node.tagName).toEqual(tagname);
}
}
const node = new RRDocument();
expect(node.getElementsByTagName('h2').length).toEqual(0);
});
it('getElementsByClassName', () => {
for (let className of [
'blocks',
'blocks1',
':hover',
'blocks1 blocks',
'blocks blocks1',
':hover blocks1',
':hover blocks1 blocks',
':hover blocks1 block',
]) {
const msg = `queried class name: '${className}'`;
expect({
message: msg,
result: rrdom.getElementsByClassName(className).length,
}).toEqual({
message: msg,
result: document.getElementsByClassName(className).length,
});
}
const node = new RRDocument();
expect(node.getElementsByClassName('block').length).toEqual(0);
});
it('getElementById', () => {
for (let elementId of ['block1', 'block2', 'block3']) {
expect(rrdom.getElementById(elementId)).not.toBeNull();
expect(rrdom.getElementById(elementId)!.id).toEqual(elementId);
}
for (let elementId of ['block', 'blocks', 'blocks1'])
expect(rrdom.getElementById(elementId)).toBeNull();
const node = new RRDocument();
expect(node.getElementById('id')).toBeNull();
});
it('querySelectorAll querying tag name', () => {
expect(rrdom.querySelectorAll('H1')).toHaveLength(2);
expect(rrdom.querySelectorAll('H1')[0]).toBeInstanceOf(RRElement);
expect((rrdom.querySelectorAll('H1')[0] as RRElement).tagName).toEqual(
'H1',
);
expect(rrdom.querySelectorAll('H1')[1]).toBeInstanceOf(RRElement);
expect((rrdom.querySelectorAll('H1')[1] as RRElement).tagName).toEqual(
'H1',
);
});
it('querySelectorAll querying class name', () => {
for (let className of [
'.blocks',
'.blocks1',
'.\\:hover',
'.blocks1.blocks',
'.blocks.blocks1',
'.\\:hover.blocks1',
'.\\:hover.blocks1.blocks',
'.\\:hover.blocks1.block',
]) {
const msg = `queried class name: '${className}'`;
expect({
message: msg,
result: rrdom.querySelectorAll(className).length,
}).toEqual({
message: msg,
result: document.querySelectorAll(className).length,
});
}
for (let element of rrdom.querySelectorAll('.\\:hover')) {
expect(element).toBeInstanceOf(RRElement);
expect((element as RRElement).classList.classes).toContain(':hover');
}
});
it('querySelectorAll querying id', () => {
for (let query of ['#block1', '#block2', '#block3']) {
expect(rrdom.querySelectorAll(query).length).toEqual(1);
const targetElement = rrdom.querySelectorAll(query)[0] as RRElement;
expect(targetElement.id).toEqual(query.substring(1, query.length));
}
for (let query of ['#block', '#blocks', '#block1#block2'])
expect(rrdom.querySelectorAll(query).length).toEqual(0);
});
it('querySelectorAll', () => {
expect(rrdom.querySelectorAll('link[rel="stylesheet"]').length).toEqual(
1,
);
const targetLink = rrdom.querySelectorAll(
'link[rel="stylesheet"]',
)[0] as RRElement;
expect(targetLink.tagName).toEqual('LINK');
expect(targetLink.getAttribute('rel')).toEqual('stylesheet');
expect(rrdom.querySelectorAll('.blocks#block1').length).toEqual(1);
expect(rrdom.querySelectorAll('.blocks#block3').length).toEqual(0);
});
});
describe('RRElement API', () => {
let rrdom: RRDocument;
beforeAll(() => {
// initialize rrdom
document.write(getHtml('main.html'));
rrdom = new RRDocument();
buildFromDom(document, undefined, rrdom);
});
it('can get attribute', () => {
expect(
rrdom.getElementsByTagName('DIV')[0].getAttribute('class'),
).toEqual('blocks blocks1');
expect(
rrdom.getElementsByTagName('dIv')[0].getAttribute('cLaSs'),
).toEqual('blocks blocks1');
expect(rrdom.getElementsByTagName('DIV')[0].getAttribute('id')).toEqual(
'block1',
);
expect(rrdom.getElementsByTagName('div')[0].getAttribute('iD')).toEqual(
'block1',
);
expect(
rrdom.getElementsByTagName('p')[0].getAttribute('class'),
).toBeNull();
});
it('can set attribute', () => {
const node = rrdom.createElement('div');
expect(node.getAttribute('class')).toEqual(null);
node.setAttribute('class', 'className');
expect(node.getAttribute('cLass')).toEqual('className');
expect(node.getAttribute('iD')).toEqual(null);
node.setAttribute('iD', 'id');
expect(node.getAttribute('id')).toEqual('id');
});
it('can remove attribute', () => {
const node = rrdom.createElement('div');
node.setAttribute('Class', 'className');
expect(node.getAttribute('class')).toEqual('className');
node.removeAttribute('clAss');
expect(node.getAttribute('class')).toEqual(null);
node.removeAttribute('Id');
expect(node.getAttribute('id')).toEqual(null);
});
it('get nextElementSibling', () => {
expect(rrdom.documentElement!.firstElementChild).not.toBeNull();
expect(rrdom.documentElement!.firstElementChild!.tagName).toEqual('HEAD');
expect(
rrdom.documentElement!.firstElementChild!.nextElementSibling,
).not.toBeNull();
expect(
rrdom.documentElement!.firstElementChild!.nextElementSibling!.tagName,
).toEqual('BODY');
expect(
rrdom.documentElement!.firstElementChild!.nextElementSibling!
.nextElementSibling,
).toBeNull();
expect(rrdom.getElementsByTagName('h1').length).toEqual(2);
const element1 = rrdom.getElementsByTagName('h1')[0];
const element2 = rrdom.getElementsByTagName('h1')[1];
expect(element1.tagName).toEqual('H1');
expect(element2.tagName).toEqual('H1');
expect(element1.nextElementSibling).toEqual(element2);
expect(element2.nextElementSibling).not.toBeNull();
expect(element2.nextElementSibling!.id).toEqual('block1');
expect(element2.nextElementSibling!.nextElementSibling).toBeNull();
const node = rrdom.createElement('div');
expect(node.nextElementSibling).toBeNull();
});
it('can get CSS style declaration', () => {
const node = rrdom.createElement('div');
const style = node.style;
expect(style).toBeDefined();
expect(style.setProperty).toBeDefined();
expect(style.removeProperty).toBeDefined();
node.attributes.style =
'color: blue; background-color: red; width: 78%; height: 50vh !important;';
expect(node.style.color).toBe('blue');
expect(node.style.backgroundColor).toBe('red');
expect(node.style.width).toBe('78%');
expect(node.style.height).toBe('50vh');
});
it('can set CSS property', () => {
const node = rrdom.createElement('div');
const style = node.style;
style.setProperty('color', 'red');
expect(node.attributes.style).toEqual('color: red;');
// camelCase style is unacceptable
style.setProperty('backgroundColor', 'blue');
expect(node.attributes.style).toEqual('color: red;');
style.setProperty('height', '50vh', 'important');
expect(node.attributes.style).toEqual(
'color: red; height: 50vh !important;',
);
// kebab-case
style.setProperty('background-color', 'red');
expect(node.attributes.style).toEqual(
'color: red; height: 50vh !important; background-color: red;',
);
// remove the property
style.setProperty('background-color', null);
expect(node.attributes.style).toEqual(
'color: red; height: 50vh !important;',
);
});
it('can remove CSS property', () => {
const node = rrdom.createElement('div');
node.attributes.style =
'color: blue; background-color: red; width: 78%; height: 50vh;';
const style = node.style;
expect(style.removeProperty('color')).toEqual('blue');
expect(node.attributes.style).toEqual(
'background-color: red; width: 78%; height: 50vh;',
);
expect(style.removeProperty('height')).toEqual('50vh');
expect(node.attributes.style).toEqual(
'background-color: red; width: 78%;',
);
// kebab-case
expect(style.removeProperty('background-color')).toEqual('red');
expect(node.attributes.style).toEqual('width: 78%;');
style.setProperty('background-color', 'red');
expect(node.attributes.style).toEqual(
'width: 78%; background-color: red;',
);
expect(style.removeProperty('backgroundColor')).toEqual('');
expect(node.attributes.style).toEqual(
'width: 78%; background-color: red;',
);
// remove a non-exist property
expect(style.removeProperty('margin')).toEqual('');
});
it('can parse more inline styles correctly', () => {
const node = rrdom.createElement('div');
// general
node.attributes.style =
'display: inline-block; margin: 0 auto; border: 5px solid #BADA55; font-size: .75em; position:absolute;width: 33.3%; z-index:1337; font-family: "Goudy Bookletter 1911", Gill Sans Extrabold, sans-serif;';
const style = node.style;
expect(style.display).toEqual('inline-block');
expect(style.margin).toEqual('0px auto');
expect(style.border).toEqual('5px solid #bada55');
expect(style.fontSize).toEqual('.75em');
expect(style.position).toEqual('absolute');
expect(style.width).toEqual('33.3%');
expect(style.zIndex).toEqual('1337');
expect(style.fontFamily).toEqual(
'"Goudy Bookletter 1911", Gill Sans Extrabold, sans-serif',
);
// multiple of same property
node.attributes.style = 'color:rgba(0,0,0,1);color:white';
expect(style.color).toEqual('white');
// url
node.attributes.style =
'background-image: url("http://example.com/img.png")';
expect(node.style.backgroundImage).toEqual(
'url(http://example.com/img.png)',
);
// comment
node.attributes.style =
'top: 0; /* comment1 */ bottom: /* comment2 */42rem;';
expect(node.style.top).toEqual('0px');
expect(node.style.bottom).toEqual('42rem');
// empty comment
node.attributes.style = 'top: /**/0;';
expect(node.style.top).toEqual('0px');
// incomplete
node.attributes.style = 'overflow:';
expect(node.style.overflow).toEqual('');
});
it('querySelectorAll', () => {
const element = rrdom.getElementById('block2')!;
expect(element).toBeDefined();
expect(element.id).toEqual('block2');
const result = element.querySelectorAll('div');
expect(result.length).toBe(1);
expect((result[0]! as RRElement).tagName).toEqual('DIV');
expect(element.querySelectorAll('.blocks').length).toEqual(0);
const element2 = rrdom.getElementById('block1')!;
expect(element2).toBeDefined();
expect(element2.id).toEqual('block1');
expect(element2.querySelectorAll('div').length).toEqual(2);
expect(element2.querySelectorAll('.blocks').length).toEqual(1);
});
it('can attach shadow dom', () => {
const node = rrdom.createElement('div');
expect(node.shadowRoot).toBeNull();
node.attachShadow({ mode: 'open' });
expect(node.shadowRoot).not.toBeNull();
expect(node.shadowRoot!.RRNodeType).toBe(RRNodeType.Element);
expect(node.shadowRoot!.tagName).toBe('SHADOWROOT');
expect(node.parentNode).toBeNull();
});
it('can insert new child before an existing child', () => {
const node = rrdom.createElement('div');
const child1 = rrdom.createElement('h1');
const child2 = rrdom.createElement('h2');
expect(() =>
node.insertBefore(node, child1),
).toThrowErrorMatchingInlineSnapshot(
`[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.]`,
);
expect(node.insertBefore(child1, null)).toBe(child1);
expect(node.childNodes[0]).toBe(child1);
expect(child1.parentNode).toBe(node);
expect(child1.parentElement).toBe(node);
expect(node.insertBefore(child2, child1)).toBe(child2);
expect(node.childNodes.length).toBe(2);
expect(node.childNodes[0]).toBe(child2);
expect(node.childNodes[1]).toBe(child1);
expect(child2.parentNode).toBe(node);
expect(child2.parentElement).toBe(node);
});
it('style element', () => {
expect(rrdom.getElementsByTagName('style').length).not.toEqual(0);
expect(rrdom.getElementsByTagName('style')[0].tagName).toEqual('STYLE');
const styleElement = rrdom.getElementsByTagName(
'style',
)[0] as RRStyleElement;
expect(styleElement.sheet).toBeDefined();
expect(styleElement.sheet!.cssRules).toBeDefined();
expect(styleElement.sheet!.cssRules.length).toEqual(5);
const rules = styleElement.sheet!.cssRules;
expect(rules[0].cssText).toEqual(`h1 {color: 'black';}`);
expect(rules[1].cssText).toEqual(`.blocks {padding: 0;}`);
expect(rules[2].cssText).toEqual(`.blocks1 {margin: 0;}`);
expect(rules[3].cssText).toEqual(
`#block1 {width: 100px; height: 200px;}`,
);
expect(rules[4].cssText).toEqual(`@import url(main.css);`);
expect((rules[4] as CSSImportRule).href).toEqual('main.css');
expect(styleElement.sheet!.insertRule).toBeDefined();
const newRule = "p {color: 'black';}";
styleElement.sheet!.insertRule(newRule, 5);
expect(rules[5].cssText).toEqual(newRule);
expect(styleElement.sheet!.deleteRule).toBeDefined();
styleElement.sheet!.deleteRule(5);
expect(rules[5]).toBeUndefined();
expect(rules[4].cssText).toEqual(`@import url(main.css);`);
});
it('can create an RRIframeElement', () => {
const iframe = rrdom.createElement('iframe');
expect(iframe.tagName).toEqual('IFRAME');
expect(iframe.width).toEqual('');
expect(iframe.height).toEqual('');
expect(iframe.contentDocument).toBeDefined();
expect(iframe.contentDocument!.childNodes.length).toBe(1);
expect(iframe.contentDocument!.documentElement).toBeDefined();
expect(iframe.contentDocument!.head).toBeDefined();
expect(iframe.contentDocument!.body).toBeDefined();
expect(iframe.contentWindow).toBeDefined();
expect(iframe.contentWindow!.scrollTop).toEqual(0);
expect(iframe.contentWindow!.scrollLeft).toEqual(0);
expect(iframe.contentWindow!.scrollTo).toBeDefined();
// empty parameter and did nothing
iframe.contentWindow!.scrollTo();
expect(iframe.contentWindow!.scrollTop).toEqual(0);
expect(iframe.contentWindow!.scrollLeft).toEqual(0);
iframe.contentWindow!.scrollTo({ top: 10, left: 20 });
expect(iframe.contentWindow!.scrollTop).toEqual(10);
expect(iframe.contentWindow!.scrollLeft).toEqual(20);
});
it('should have a RRCanvasElement', () => {
const canvas = rrdom.createElement('canvas');
expect(canvas.getContext()).toBeNull();
});
});
});
function getHtml(fileName: string) {
const filePath = path.resolve(__dirname, `../../rrdom/test/html/${fileName}`);
return fs.readFileSync(filePath, 'utf8');
}
================================================
FILE: packages/rrdom-nodejs/test/polyfill.test.ts
================================================
import { describe, it, expect, vi } from 'vitest';
import { compare } from 'compare-versions';
import { RRDocument } from '../src/document-nodejs';
import {
polyfillPerformance,
polyfillRAF,
polyfillEvent,
polyfillNode,
polyfillDocument,
} from '../src/polyfill';
import { performance as nativePerformance } from 'perf_hooks';
import { BaseRRNode } from 'rrdom';
describe('polyfill for nodejs', () => {
it('should polyfill performance api', () => {
if (compare(process.version, 'v16.0.0', '<'))
expect(global.performance).toBeUndefined();
polyfillPerformance();
expect(global.performance).toBeDefined();
expect(performance).toBeDefined();
expect(performance.now).toBeDefined();
expect(performance.now()).toBeCloseTo(nativePerformance.now(), 1e-10);
});
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 = vi.fn() as unknown as Performance;
global.performance = fakePerformance;
polyfillPerformance();
expect(global.performance).toEqual(fakePerformance);
});
it('should polyfill requestAnimationFrame', () => {
expect(global.requestAnimationFrame).toBeUndefined();
expect(global.cancelAnimationFrame).toBeUndefined();
polyfillRAF();
expect(global.requestAnimationFrame).toBeDefined();
expect(global.cancelAnimationFrame).toBeDefined();
expect(requestAnimationFrame).toBeDefined();
expect(cancelAnimationFrame).toBeDefined();
vi.useFakeTimers();
const AnimationTime = 1_000; // target animation time(unit: ms)
const startTime = Date.now();
let frameCount = 0;
const rafCallback1 = () => {
const currentTime = Date.now();
frameCount++;
if (currentTime - startTime < AnimationTime) {
requestAnimationFrame(rafCallback1);
} else {
expect(frameCount).toBeGreaterThanOrEqual(55);
expect(frameCount).toBeLessThanOrEqual(65);
}
};
requestAnimationFrame(rafCallback1);
// Fast-forward until all timers have been executed
vi.runAllTimers();
let rafHandle;
const rafCallback2 = () => {
rafHandle = requestAnimationFrame(rafCallback2);
};
rafHandle = requestAnimationFrame(rafCallback2);
// If this function doesn't work, recursive function will never end.
cancelAnimationFrame(rafHandle);
vi.runAllTimers();
vi.useRealTimers();
});
it('should not polyfill requestAnimationFrame if it already exists', () => {
const fakeRequestAnimationFrame =
vi.fn() as unknown as typeof global.requestAnimationFrame;
global.requestAnimationFrame = fakeRequestAnimationFrame;
const fakeCancelAnimationFrame =
vi.fn() as unknown as typeof global.cancelAnimationFrame;
global.cancelAnimationFrame = fakeCancelAnimationFrame;
polyfillRAF();
expect(global.requestAnimationFrame).toBe(fakeRequestAnimationFrame);
expect(global.cancelAnimationFrame).toBe(fakeCancelAnimationFrame);
});
it('should polyfill Event type', () => {
// if the second version is greater
if (compare(process.version, 'v15.0.0', '<'))
expect(global.Event).toBeUndefined();
polyfillEvent();
expect(global.Event).toBeDefined();
expect(Event).toBeDefined();
});
it('should not polyfill Event type if it already exists', () => {
const fakeEvent = vi.fn() as unknown as typeof global.Event;
global.Event = fakeEvent;
polyfillEvent();
expect(global.Event).toBe(fakeEvent);
});
it('should polyfill Node type', () => {
expect(global.Node).toBeUndefined();
polyfillNode();
expect(global.Node).toBeDefined();
expect(Node).toBeDefined();
expect(Node).toEqual(BaseRRNode);
});
it('should not polyfill Node type if it already exists', () => {
const fakeNode = vi.fn() as unknown as typeof global.Node;
global.Node = fakeNode;
polyfillNode();
expect(global.Node).toBe(fakeNode);
});
it('should polyfill document object', () => {
expect(global.document).toBeUndefined();
polyfillDocument();
expect(global.document).toBeDefined();
expect(document).toBeDefined();
expect(document).toBeInstanceOf(RRDocument);
});
it('should not polyfill document object if it already exists', () => {
const fakeDocument = vi.fn() as unknown as typeof global.document;
global.document = fakeDocument;
polyfillDocument();
expect(global.document).toBe(fakeDocument);
});
});
================================================
FILE: packages/rrdom-nodejs/tsconfig.json
================================================
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
"rootDir": "src",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"references": [
{
"path": "../rrdom"
},
{
"path": "../types"
}
]
}
================================================
FILE: packages/rrdom-nodejs/vite.config.js
================================================
import path from 'path';
import config from '../../vite.config.default';
export default config(path.resolve(__dirname, 'src/index.ts'), 'rrdomNodejs');
================================================
FILE: packages/rrdom-nodejs/vitest.config.ts
================================================
///
import { defineProject, mergeConfig } from 'vitest/config';
import configShared from '../../vitest.config';
export default mergeConfig(configShared, defineProject({}));
================================================
FILE: packages/rrvideo/CHANGELOG.md
================================================
# rrvideo
## 2.0.0-alpha.20
### Patch Changes
- Updated dependencies []:
- rrweb-player@2.0.0-alpha.20
## 2.0.0-alpha.19
### Patch Changes
- Updated dependencies []:
- rrweb-player@2.0.0-alpha.19
## 2.0.0-alpha.18
### Patch Changes
- Updated dependencies []:
- rrweb-player@2.0.0-alpha.18
## 2.0.0-alpha.17
### Patch Changes
- Updated dependencies []:
- rrweb-player@2.0.0-alpha.17
## 2.0.0-alpha.16
### Patch Changes
- Updated dependencies []:
- rrweb-player@2.0.0-alpha.16
## 2.0.0-alpha.15
### Patch Changes
- Updated dependencies [[`2606a2a`](https://github.com/rrweb-io/rrweb/commit/2606a2a28f2a6d897b8ae4ea3ec40ef0eeacbfaf)]:
- rrweb-player@2.0.0-alpha.15
## 2.0.0-alpha.14
### Patch Changes
- Updated dependencies []:
- rrweb-player@2.0.0-alpha.14
## 2.0.0-alpha.13
### Patch Changes
- Updated dependencies []:
- rrweb-player@2.0.0-alpha.13
## 2.0.0-alpha.12
### Patch Changes
- Updated dependencies []:
- rrweb-player@2.0.0-alpha.12
## 2.0.0-alpha.11
### Patch Changes
- Updated dependencies [[`efdc167`](https://github.com/rrweb-io/rrweb/commit/efdc167ca6c039d04af83612e3d92498bb9b41a7)]:
- rrweb-player@2.0.0-alpha.11
## 2.0.0-alpha.10
### Patch Changes
- Updated dependencies []:
- rrweb-player@2.0.0-alpha.10
## 2.0.0-alpha.9
### Patch Changes
- [#1197](https://github.com/rrweb-io/rrweb/pull/1197) [`23d0138`](https://github.com/rrweb-io/rrweb/commit/23d01387f439db68d2874879242b6ade3e103f75) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - Refactor: Improve the video quality and add a progress bar for the CLI tool
- Updated dependencies [[`a01a12e`](https://github.com/rrweb-io/rrweb/commit/a01a12ef6769f26aa922ccd6ac76499f0837f0c2)]:
- rrweb-player@2.0.0-alpha.9
## 2.0.0-alpha.8
### Patch Changes
- Updated dependencies [[`b5e30cf`](https://github.com/rrweb-io/rrweb/commit/b5e30cf6cc7f5335d674ef1917a92bdf2895fe9e)]:
- rrweb-player@2.0.0-alpha.8
## 2.0.0-alpha.7
### Patch Changes
- [#1181](https://github.com/rrweb-io/rrweb/pull/1181) [`f1f5865`](https://github.com/rrweb-io/rrweb/commit/f1f5865dcf19db5637bbb12b220eb2aa0c0219ad) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - Refactor: Move rrvideo to rrweb's monorepo
- Updated dependencies []:
- rrweb-player@2.0.0-alpha.7
================================================
FILE: packages/rrvideo/README.md
================================================
# rrvideo
[中文文档](./README.zh_CN.md)
rrvideo is a tool for transforming the session recorded by [rrweb](https://github.com/rrweb-io/rrweb) into a video.

## Install rrvideo
1. Install [Node.JS](https://nodejs.org/en/download/)。
2. Run `npm i -g rrvideo` to install the rrvideo CLI.
## Use rrvideo
### Transform a rrweb session(in JSON format) into a video.
```shell
rrvideo --input PATH_TO_YOUR_RRWEB_EVENTS_FILE
```
Running this command will output a `rrvideo-output.webm` file in the current working directory.
### Config the output path
```shell
rrvideo --input PATH_TO_YOUR_RRWEB_EVENTS_FILE --output OUTPUT_PATH
```
### Config the replay
You can prepare a rrvideo config file and pass it to CLI.
```shell
rrvideo --input PATH_TO_YOUR_RRWEB_EVENTS_JSON_FILE --config PATH_TO_YOUR_RRVIDEO_CONFIG_FILE
```
You can find an example of the rrvideo config file [here](./rrvideo.config.example.json).
## Sponsors
[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.
### Gold Sponsors 🥇
### Silver Sponsors 🥈
### Bronze Sponsors 🥉
### Backers
## Core Team Members
## Who's using rrweb?
================================================
FILE: packages/rrvideo/README.zh_CN.md
================================================
# rrvideo
rrvideo 是用于将 [rrweb](https://github.com/rrweb-io/rrweb) 录制的数据转为视频格式的工具。

## 安装 rrvideo
1. 安装 [Node.JS](https://nodejs.org/en/download/)。
2. 执行 `npm i -g rrvideo` 以安装 rrvideo CLI。
## 使用 rrvideo
### 将一份 rrweb 录制的数据(JSON 格式)转换为视频。
```shell
rrvideo --input PATH_TO_YOUR_RRWEB_EVENTS_FILE
```
运行以上命令会在执行文件夹中生成一个 `rrvideo-output.webm` 文件。
### 指定输出路径
```shell
rrvideo --input PATH_TO_YOUR_RRWEB_EVENTS_FILE --output OUTPUT_PATH
```
### 对回放进行配置
通过编写一个 rrvideo 配置文件再传入 rrvideo CLI 的方式可以对回放进行一定的配置。
```shell
rrvideo --input PATH_TO_YOUR_RRWEB_EVENTS_JSON_FILE --config PATH_TO_YOUR_RRVIDEO_CONFIG_FILE
```
rrvideo 配置文件可参考[示例](./rrvideo.config.example.json)。
## Sponsors
[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.
### Gold Sponsors 🥇
### Silver Sponsors 🥈
### Bronze Sponsors 🥉
### Backers
## Core Team Members
## Who's using rrweb?
================================================
FILE: packages/rrvideo/jest.config.js
================================================
// eslint-disable-next-line tsdoc/syntax
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
================================================
FILE: packages/rrvideo/package.json
================================================
{
"name": "rrvideo",
"version": "2.0.0-alpha.20",
"description": "transform rrweb session into video",
"main": "build/index.js",
"bin": {
"rrvideo": "build/cli.js"
},
"files": [
"build",
"package.json"
],
"types": "build/index.d.ts",
"scripts": {
"install": "playwright install",
"build": "tsc",
"test": "jest",
"check-types": "tsc -noEmit",
"prepublish": "yarn build"
},
"author": "yanzhen@smartx.com",
"license": "MIT",
"devDependencies": {
"@types/fs-extra": "11.0.1",
"@types/jest": "^27.4.1",
"@types/minimist": "^1.2.1",
"@types/node": "^18.15.11",
"jest": "^27.5.1",
"ts-jest": "^27.1.3",
"@rrweb/types": "^2.0.0-alpha.20"
},
"dependencies": {
"@open-tech-world/cli-progress-bar": "^2.0.2",
"fs-extra": "^11.1.1",
"minimist": "^1.2.5",
"playwright": "^1.56.1",
"rrweb-player": "^2.0.0-alpha.20"
}
}
================================================
FILE: packages/rrvideo/rrvideo.config.example.json
================================================
{
"width": 1400,
"height": 900,
"speed": 4,
"skipInactive": true,
"mouseTail": {
"strokeStyle": "green",
"lineWidth": 2
}
}
================================================
FILE: packages/rrvideo/src/cli.ts
================================================
#!/usr/bin/env node
import * as fs from 'fs';
import * as path from 'path';
import minimist from 'minimist';
import { ProgressBar } from '@open-tech-world/cli-progress-bar';
import type Player from 'rrweb-player';
import { transformToVideo } from './index';
const argv = minimist(process.argv.slice(2));
if (!argv.input) {
throw new Error('please pass --input to your rrweb events file');
}
let config = {};
if (argv.config) {
const configPathStr = argv.config as string;
const configPath = path.isAbsolute(configPathStr)
? configPathStr
: path.resolve(process.cwd(), configPathStr);
config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as Omit<
ConstructorParameters[0]['props'],
'events'
>;
}
const pBar = new ProgressBar({ prefix: 'Transforming' });
const onProgressUpdate = (percent: number) => {
if (percent < 1) pBar.run({ value: percent * 100, total: 100 });
else
pBar.run({ value: 100, total: 100, prefix: 'Transformation Completed!' });
};
transformToVideo({
input: argv.input as string,
output: argv.output as string,
rrwebPlayer: config,
onProgressUpdate,
})
.then((file) => {
console.log(`Successfully transformed into "${file}".`);
})
.catch((error) => {
console.log('Failed to transform this session.');
console.error(error);
process.exit(1);
});
================================================
FILE: packages/rrvideo/src/index.ts
================================================
import * as fs from 'fs-extra';
import * as path from 'path';
import { chromium } from 'playwright';
import { EventType, eventWithTime } from '@rrweb/types';
import type Player from 'rrweb-player';
const rrwebScriptPath = path.resolve(
require.resolve('rrweb-player'),
'../../dist/rrweb-player.umd.cjs',
);
const rrwebStylePath = path.resolve(rrwebScriptPath, '../style.css');
const rrwebRaw = fs.readFileSync(rrwebScriptPath, 'utf-8');
const rrwebStyle = fs.readFileSync(rrwebStylePath, 'utf-8');
// The max valid scale value for the scaling method which can improve the video quality.
const MaxScaleValue = 2.5;
type RRvideoConfig = {
input: string;
output?: string;
headless?: boolean;
// A number between 0 and 1. The higher the value, the better the quality of the video.
resolutionRatio?: number;
// A callback function that will be called when the progress of the replay is updated.
onProgressUpdate?: (percent: number) => void;
rrwebPlayer?: Omit<
ConstructorParameters[0]['props'],
'events'
>;
};
const defaultConfig: Required = {
input: '',
output: 'rrvideo-output.webm',
headless: true,
// A good trade-off value between quality and file size.
resolutionRatio: 0.8,
onProgressUpdate: () => {
//
},
rrwebPlayer: {},
};
function getHtml(events: Array, config?: RRvideoConfig): string {
return `
`;
}
/**
* Preprocess all events to get a maximum view port size.
*/
function getMaxViewport(events: eventWithTime[]) {
let maxWidth = 0,
maxHeight = 0;
events.forEach((event) => {
if (event.type !== EventType.Meta) return;
if (event.data.width > maxWidth) maxWidth = event.data.width;
if (event.data.height > maxHeight) maxHeight = event.data.height;
});
return {
width: maxWidth,
height: maxHeight,
};
}
export async function transformToVideo(options: RRvideoConfig) {
const defaultVideoDir = '__rrvideo__temp__';
const config = { ...defaultConfig };
if (!options.input) throw new Error('input is required');
// If the output is not specified or undefined, use the default value.
if (!options.output) delete options.output;
Object.assign(config, options);
if (config.resolutionRatio > 1) config.resolutionRatio = 1; // The max value is 1.
const eventsPath = path.isAbsolute(config.input)
? config.input
: path.resolve(process.cwd(), config.input);
const outputPath = path.isAbsolute(config.output)
? config.output
: path.resolve(process.cwd(), config.output);
const events = JSON.parse(
fs.readFileSync(eventsPath, 'utf-8'),
) as eventWithTime[];
// Make the browser viewport fit the player size.
const maxViewport = getMaxViewport(events);
// Use the scaling method to improve the video quality.
const scaledViewport = {
width: Math.round(
maxViewport.width * (config.resolutionRatio ?? 1) * MaxScaleValue,
),
height: Math.round(
maxViewport.height * (config.resolutionRatio ?? 1) * MaxScaleValue,
),
};
Object.assign(config.rrwebPlayer, scaledViewport);
const browser = await chromium.launch({
headless: config.headless,
});
const context = await browser.newContext({
viewport: scaledViewport,
recordVideo: {
dir: defaultVideoDir,
size: scaledViewport,
},
});
const page = await context.newPage();
await page.goto('about:blank');
// Listen to console messages from the page
page.on('console', (msg) => {
console.log('[PAGE CONSOLE]', msg.type(), msg.text());
});
// Listen to page errors
page.on('pageerror', (error) => {
console.error('[PAGE ERROR]', error.message);
});
await page.exposeFunction(
'onReplayProgressUpdate',
(data: { payload: number }) => {
config.onProgressUpdate(data.payload);
},
);
// Wait for the replay to finish
await new Promise((resolve, reject) => {
const timeoutBuffer = 120000; // 2 minute timeout buffer
const videoStartTime = events[0]?.timestamp;
const videoEndTime = events[events.length - 1]?.timestamp;
const videoDuration = videoEndTime - videoStartTime;
const videoPlaybackSpeed = options.rrwebPlayer?.speed || 1;
const expectedPlaybackTime = videoDuration / videoPlaybackSpeed;
console.log(
`[DEBUG] Expected playback time: ${expectedPlaybackTime}ms (video duration: ${videoDuration}ms, playback speed: ${videoPlaybackSpeed}x)`,
);
const totalTimeout = expectedPlaybackTime + timeoutBuffer;
const timeout = setTimeout(() => {
console.error('[DEBUG] Replay timeout - finish event never fired');
reject(new Error('Replay timeout'));
}, totalTimeout); // playback + 2 minute timeout
void page
.exposeFunction('onReplayFinish', () => {
console.log('[DEBUG] Replay finished');
clearTimeout(timeout);
resolve();
})
.then(() => {
console.log('[DEBUG] Setting page content');
return page.setContent(getHtml(events, config));
})
.then(() => {
console.log('[DEBUG] Page content set successfully');
})
.catch((err) => {
console.error('[DEBUG] Error setting page content:', err);
clearTimeout(timeout);
reject(err);
});
});
const videoPath = (await page.video()?.path()) || '';
const cleanFiles = async (videoPath: string) => {
await fs.remove(videoPath);
if ((await fs.readdir(defaultVideoDir)).length === 0) {
await fs.remove(defaultVideoDir);
}
};
await context.close();
await Promise.all([
fs
.move(videoPath, outputPath, { overwrite: true })
.catch((e) => {
console.error(
"Can't create video file. Please check the output path.",
e,
);
})
.finally(() => void cleanFiles(videoPath)),
browser.close(),
]);
return outputPath;
}
================================================
FILE: packages/rrvideo/test/cli.test.ts
================================================
import { execSync } from 'child_process';
import * as fs from 'fs-extra';
import * as path from 'path';
import exampleEvents from './events/example';
describe('should be able to run cli', () => {
beforeAll(() => {
fs.mkdirSync(path.resolve(__dirname, './generated'));
fs.writeJsonSync(
path.resolve(__dirname, './generated/example.json'),
exampleEvents,
{
spaces: 2,
},
);
});
afterAll(async () => {
await fs.remove(path.resolve(__dirname, './generated'));
});
const execOptions = { timeout: 60_000 } as const;
const execOptionsWithOutput = { ...execOptions, stdio: 'inherit' } as const;
it('should throw error without input path', () => {
expect(() => {
execSync('node ./build/cli.js', { ...execOptions, stdio: 'pipe' });
}).toThrowError(/.*please pass --input to your rrweb events file.*/);
});
it('should generate a video without output path', () => {
execSync(
'node ./build/cli.js --input ./test/generated/example.json',
execOptionsWithOutput,
);
const outputFile = path.resolve(__dirname, '../rrvideo-output.webm');
expect(fs.existsSync(outputFile)).toBe(true);
fs.removeSync(outputFile);
});
it('should generate a video with specific output path', () => {
const outputFile = path.resolve(__dirname, './generated/output.webm');
execSync(
`node ./build/cli.js --input ./test/generated/example.json --output ${outputFile}`,
execOptionsWithOutput,
);
expect(fs.existsSync(outputFile)).toBe(true);
fs.removeSync(outputFile);
});
});
================================================
FILE: packages/rrvideo/test/events/example.ts
================================================
import { EventType, IncrementalSource } from '@rrweb/types';
import type { eventWithTime } from '@rrweb/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: [],
attributes: {},
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 + 200,
},
// input event
{
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Input,
text: 'valueA',
isChecked: false,
id: 26,
},
timestamp: now + 300,
},
];
export default events;
================================================
FILE: packages/rrvideo/test/tsconfig.json
================================================
{
"compilerOptions": {}
}
================================================
FILE: packages/rrvideo/tsconfig.json
================================================
{
"compilerOptions": {
"composite": true,
"target": "ES6",
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"outDir": "./build",
"rootDir": "./src",
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"exclude": ["build", "node_modules", "test"],
"references": [
{
"path": "../rrweb-player"
},
{
"path": "../types"
}
]
}
================================================
FILE: packages/rrweb/.gitignore
================================================
.vscode
.idea
node_modules
package-lock.json
# yarn.lock
tsconfig.tsbuildinfo
build
dist
es
lib
typings
temp
*.log
.env
__diff_output__
================================================
FILE: packages/rrweb/.release-it.json
================================================
{
"non-interactive": true,
"hooks": {
"before:init": ["npm run bundle", "npm run typings"]
},
"git": {
"requireCleanWorkingDir": false
},
"github": {
"release": true
}
}
================================================
FILE: packages/rrweb/CHANGELOG.md
================================================
# rrweb
## 2.0.0-alpha.20
### Patch Changes
- [#1763](https://github.com/rrweb-io/rrweb/pull/1763) [`6388fb5`](https://github.com/rrweb-io/rrweb/commit/6388fb5a468e1a860ab8bb5c6826c811dcc3100c) Thanks [@wfk007](https://github.com/wfk007)! - fix: wujie monkeypatches ownerDocument
- Updated dependencies [[`6388fb5`](https://github.com/rrweb-io/rrweb/commit/6388fb5a468e1a860ab8bb5c6826c811dcc3100c)]:
- @rrweb/utils@2.0.0-alpha.20
- rrweb-snapshot@2.0.0-alpha.20
- rrdom@2.0.0-alpha.20
- @rrweb/types@2.0.0-alpha.20
## 2.0.0-alpha.19
### Patch Changes
- [#1615](https://github.com/rrweb-io/rrweb/pull/1615) [`dc20cd4`](https://github.com/rrweb-io/rrweb/commit/dc20cd45cc63058325784444af6bd32ed2cace48) Thanks [@eoghanmurray](https://github.com/eoghanmurray)! - Improve performance of splitCssText for
Verify that block class bugs are fixed
================================================
FILE: packages/rrweb/test/html/canvas-webgl-image.html
================================================
Document
================================================
FILE: packages/rrweb/test/html/canvas-webgl-shader.html
================================================
canvas shader
================================================
FILE: packages/rrweb/test/html/canvas-webgl-square.html
================================================
canvas webgl square
================================================
FILE: packages/rrweb/test/html/canvas-webgl.html
================================================
canvas
================================================
FILE: packages/rrweb/test/html/canvas.html
================================================
canvas
================================================
FILE: packages/rrweb/test/html/dialog.html
================================================
I'm a dialog
================================================
FILE: packages/rrweb/test/html/empty.html
================================================
Empty
================================================
FILE: packages/rrweb/test/html/form.html
================================================
form fields
================================================
FILE: packages/rrweb/test/html/frame-image-blob-url.html
================================================
Frame with image
================================================
FILE: packages/rrweb/test/html/frame1.html
================================================
Frame 1
frame 1
================================================
FILE: packages/rrweb/test/html/frame2.html
================================================
Frame 2
frame 2
================================================
FILE: packages/rrweb/test/html/hello-world.html
================================================
Hello World!
Hello world!
================================================
FILE: packages/rrweb/test/html/ignore.html
================================================
ignore fields
Input ignored here:
Input ignored by selector here:
Input not ignored here:
================================================
FILE: packages/rrweb/test/html/image-blob-url.html
================================================
Image with blob:url
================================================
FILE: packages/rrweb/test/html/link.html
================================================
Link click
not link
link
================================================
FILE: packages/rrweb/test/html/main.html
================================================
Main
================================================
FILE: packages/rrweb/test/html/mask-text.html
================================================
Mask text
mask1
mask2
unmask1
================================================
FILE: packages/rrweb/test/html/move-node.html
================================================
1
================================================
FILE: packages/rrweb/test/html/mutation-observer.html
================================================
mutation observer
================================================
FILE: packages/rrweb/test/html/password.html
================================================
Document
Toggle show password
================================================
FILE: packages/rrweb/test/html/polyfilled-shadowdom-mutation.html
================================================
================================================
FILE: packages/rrweb/test/html/react-styled-components.html
================================================
react styled components
================================================
FILE: packages/rrweb/test/html/select2.html
================================================
Select2 3.5
Select2 is a jQuery replacement for select boxes.
In the 3.5 version it use a quite complicated DOM generation strategy which is a good battle-test for rrweb's recorder.
A
B
================================================
FILE: packages/rrweb/test/html/shadow-dom.html
================================================
Shadow DOM Observer
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellat odit
officiis necessitatibus laborum asperiores et adipisci dolores corporis,
vero distinctio voluptas, suscipit commodi architecto, aliquam fugit.
Nesciunt labore reiciendis blanditiis!
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellat odit
officiis necessitatibus laborum asperiores et adipisci dolores corporis,
vero distinctio voluptas, suscipit commodi architecto, aliquam fugit.
Nesciunt labore reiciendis blanditiis!
================================================
FILE: packages/rrweb/test/html/shuffle.html
================================================
shuffle
================================================
FILE: packages/rrweb/test/html/style.html
================================================
style
================================================
FILE: packages/rrweb/test/html/video.html
================================================
Video
Big Buck Bunny
Your browser does not support the video element.
================================================
FILE: packages/rrweb/test/integration.test.ts
================================================
import * as fs from 'fs';
import * as path from 'path';
import type * as puppeteer from 'puppeteer';
import { vi } from 'vitest';
import {
assertSnapshot,
startServer,
getServerURL,
launchPuppeteer,
waitForRAF,
waitForIFrameLoad,
replaceLast,
generateRecordSnippet,
ISuite,
} from './utils';
import type { recordOptions } from '../src/types';
import { eventWithTime, NodeType, EventType } from '@rrweb/types';
import { visitSnapshot } from 'rrweb-snapshot';
describe('record integration tests', function (this: ISuite) {
vi.setConfig({ testTimeout: 10_000 });
const getHtml = (
fileName: string,
options: recordOptions = {},
): string => {
const filePath = path.resolve(__dirname, `./html/${fileName}`);
const html = fs.readFileSync(filePath, 'utf8');
return replaceLast(
html,
'