diff --git a/.changeset/config.json b/.changeset/config.json index 9dea67bc61..e51f8aa68d 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -9,6 +9,17 @@ "commit": false, "fixed": [ [ + "@saola.ai/rrweb-all", + "@saola.ai/replay", + "@saola.ai/record", + "@saola.ai/rrweb-packer", + "@saola.ai/rrweb-utils", + "@saola.ai/rrweb-plugin-console-record", + "@saola.ai/rrweb-plugin-console-replay", + "@saola.ai/rrweb-plugin-sequential-id-record", + "@saola.ai/rrweb-plugin-sequential-id-replay", + "@saola.ai/rrweb-plugin-canvas-webrtc-record", + "@saola.ai/rrweb-plugin-canvas-webrtc-replay", "@saola.ai/rrweb", "@saola.ai/rrweb-snapshot", "@saola.ai/rrdom", diff --git a/.github/workflows/ci-cd.yml.disabled b/.github/workflows/ci-cd.yml.disabled index 6351b826bd..c0c7b306a3 100644 --- a/.github/workflows/ci-cd.yml.disabled +++ b/.github/workflows/ci-cd.yml.disabled @@ -41,5 +41,5 @@ jobs: if: failure() with: name: image-diff - path: packages/rrweb/test/*/__image_snapshots__/__diff_output__/*.png + path: packages/**/__image_snapshots__/__diff_output__/*.png if-no-files-found: ignore diff --git a/.vscode/rrweb-monorepo.code-workspace b/.vscode/rrweb-monorepo.code-workspace index 98338cf192..ecb1672def 100644 --- a/.vscode/rrweb-monorepo.code-workspace +++ b/.vscode/rrweb-monorepo.code-workspace @@ -40,6 +40,10 @@ "name": "@rrweb/types", "path": "../packages/types" }, + { + "name": "@rrweb/utils", + "path": "../packages/utils" + }, { "name": "@rrweb/packer", "path": "../packages/packer" @@ -88,12 +92,14 @@ "@rrweb/record", "@rrweb/replay", "@rrweb/types", + "@rrweb/utils", "@rrweb/packer", "@rrweb/rrweb-plugin-console-record", "@rrweb/rrweb-plugin-console-replay", "@rrweb/rrweb-plugin-sequential-id", "@rrweb/rrweb-plugin-canvas-webrtc-record", "@rrweb/rrweb-plugin-canvas-webrtc-replay" - ] + ], + "typescript.tsdk": " rrweb monorepo/node_modules/typescript/lib" } } diff --git a/README.md b/README.md index 17e6b5591a..e73c2a8768 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Since we want the record and replay sides to share a strongly typed data structu 1. Fork this repository. 2. Run `yarn install` in the root to install required dependencies for all sub-packages (note: `npm install` is _not_ recommended). -3. Run `yarn dev` in the root to get auto-building for all the sub-packages whenever you modify anything. +3. Run `yarn build:all` to build all packages and get a stable base, then `yarn dev` in the root to get auto-building for all the sub-packages whenever you modify anything. 4. Navigate to one of the sub-packages (in the `packages` folder) where you'd like to make a change. 5. Patch the code and run `yarn test` to run the tests, make sure they pass before you commit anything. Add test cases in order to avoid future regression. 6. If tests are failing, but the change in output is desirable, run `yarn test:update` and carefully commit the changes in test output. diff --git a/docs/recipes/optimize-storage.md b/docs/recipes/optimize-storage.md index a50118bf89..9a67d10331 100644 --- a/docs/recipes/optimize-storage.md +++ b/docs/recipes/optimize-storage.md @@ -49,7 +49,7 @@ rrweb.record({ rrweb.record({ emit(event) {}, sampling: { - // Configure which kins of mouse interaction should be recorded + // Configure which kinds of mouse interaction should be recorded mouseInteraction: { MouseUp: false, MouseDown: false, @@ -78,7 +78,7 @@ import { pack } from '@rrweb/packer'; rrweb.record({ emit(event) {}, - packFn: rrweb.pack, + packFn: pack, }); ``` @@ -88,7 +88,7 @@ And you need to pass packer.unpack as the `unpackFn` in replaying. import { unpack } from '@rrweb/packer'; const replayer = new rrweb.Replayer(events, { - unpackFn: rrweb.unpack, + unpackFn: unpack, }); ``` diff --git a/guide.md b/guide.md index bfdb2d14fd..764e359fb4 100644 --- a/guide.md +++ b/guide.md @@ -47,6 +47,7 @@ Besides the `rrweb` and `@rrweb/record` packages, rrweb also provides other pack - [@rrweb/replay](packages/replay): A package for replaying rrweb sessions. - [@rrweb/packer](packages/packer): A package for packing and unpacking rrweb data. - [@rrweb/types](packages/types): Contains types shared across rrweb packages. +- [@rrweb/utils](packages/utils): Contains utility functions shared across rrweb packages. - [web-extension](packages/web-extension): A web extension for rrweb. - [rrvideo](packages/rrvideo): A package for handling video operations in rrweb. - [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record): A plugin for recording console logs. diff --git a/guide.zh_CN.md b/guide.zh_CN.md index 1d56998d74..4078cb2b6a 100644 --- a/guide.zh_CN.md +++ b/guide.zh_CN.md @@ -43,7 +43,8 @@ rrweb 代码分为录制和回放两部分,大多数时候用户在被录制 - [@rrweb/record](packages/record):一个用于录制 rrweb 会话的包。 - [@rrweb/replay](packages/replay):一个用于回放 rrweb 会话的包。 - [@rrweb/packer](packages/packer):一个用于打包和解包 rrweb 数据的包。 -- [@rrweb/types](packages/types):包含 rrweb 中使用的类型定义。 +- [@rrweb/types](packages/types):包含 rrweb 包中共享的类型定义。 +- [@rrweb/utils](packages/utils):包含 rrweb 包中共享的工具函数。 - [web-extension](packages/web-extension):rrweb 的网页扩展。 - [rrvideo](packages/rrvideo):一个用于处理 rrweb 中视频操作的包。 - [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record):一个用于记录控制台日志的插件。 diff --git a/package.json b/package.json index 1fdbace462..e674515625 100644 --- a/package.json +++ b/package.json @@ -30,26 +30,29 @@ "@typescript-eslint/parser": "^5.62.0", "browserslist": "^4.22.1", "concurrently": "^7.1.0", + "cross-env": "^7.0.3", "esbuild-plugin-umd-wrapper": "^2.0.0", "eslint": "^8.53.0", - "eslint-plugin-compat": "^4.2.0", + "eslint-plugin-compat": "^5.0.0", "eslint-plugin-jest": "^27.6.0", "eslint-plugin-tsdoc": "^0.2.17", + "happy-dom": "^14.12.0", "markdownlint": "^0.25.1", "markdownlint-cli": "^0.31.1", "prettier": "2.8.4", - "turbo": "^2.0.3", - "typescript": "^4.9.5" + "turbo": "^2.0.4", + "typescript": "^5.4.5" }, "scripts": { - "build:all": "NODE_OPTIONS='--max-old-space-size=4096' yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references' 'yarn turbo run prepublish'", - "test": "yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references --check' 'yarn turbo run test --concurrency=1 --continue'", + "build:all": "NODE_OPTIONS='--max-old-space-size=4096' yarn turbo run prepublish", + "references:update": "yarn workspaces-to-typescript-project-references", + "test": "yarn turbo run test --concurrency=1 --continue", "test:watch": "yarn turbo run test:watch", "test:update": "yarn turbo run test:update", "check-types": "yarn turbo run check-types --continue", "format": "yarn prettier --write '**/*.{ts,md}'", "format:head": "git diff --name-only HEAD^ |grep '\\.ts$\\|\\.md$' |xargs yarn prettier --write", - "dev": "CLEAR_DIST_DIR=false yarn turbo run dev --concurrency=17", + "dev": "yarn turbo run dev --concurrency=18", "repl": "cd packages/rrweb && npm run repl", "live-stream": "cd packages/rrweb && yarn live-stream", "lint": "yarn run concurrently --success=all -r -m=1 'yarn run markdownlint docs' 'yarn eslint packages/*/src --ext .ts,.tsx,.js,.jsx,.svelte'", @@ -58,7 +61,8 @@ }, "resolutions": { "**/cssom": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "**/@types/dom-webcodecs": "0.1.5" + "**/@types/dom-webcodecs": "0.1.5", + "typescript": "^5.4.5" }, "browserslist": [ "defaults", diff --git a/packages/all/CHANGELOG.md b/packages/all/CHANGELOG.md index 44b1bfd6b3..c7b8963501 100644 --- a/packages/all/CHANGELOG.md +++ b/packages/all/CHANGELOG.md @@ -1,5 +1,27 @@ # @rrweb/all +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb-packer@2.0.13 + - @saola.ai/rrweb@2.0.13 + - @saola.ai/rrweb-types@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages + +- Updated dependencies [[`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c), [`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]: + - @rrweb/packer@2.0.0-alpha.17 + - rrweb@2.0.0-alpha.17 + - @rrweb/types@2.0.0-alpha.17 + ## 2.0.0 ### Major Changes diff --git a/packages/all/package.json b/packages/all/package.json index e2ec6cc061..db0833a818 100644 --- a/packages/all/package.json +++ b/packages/all/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrweb-all", - "version": "2.0.0", + "version": "2.0.13", "publishConfig": { "access": "public" }, @@ -10,11 +10,11 @@ ], "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "test": "vitest run", "test:watch": "vitest watch", "check-types": "tsc -noEmit", - "prepublish": "npm run build", + "prepublish": "tsc -noEmit && vite build", "lint": "yarn eslint src/**/*.ts" }, "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/all#readme", @@ -50,15 +50,15 @@ ], "devDependencies": { "puppeteer": "^20.9.0", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1", "vitest": "^1.4.0", - "typescript": "^4.7.3" + "typescript": "^5.4.5" }, "dependencies": { - "@saola.ai/rrweb-types": "^2.0.0", - "@saola.ai/rrweb-packer": "^2.0.0", - "@saola.ai/rrweb": "^2.0.0" + "@saola.ai/rrweb-types": "^2.0.13", + "@saola.ai/rrweb-packer": "^2.0.13", + "@saola.ai/rrweb": "^2.0.13" }, "browserslist": [ "supports es6-class" diff --git a/packages/all/test/cross-origin-iframe-packer.test.ts b/packages/all/test/cross-origin-iframe-packer.test.ts index f5df997977..ed8039d35c 100644 --- a/packages/all/test/cross-origin-iframe-packer.test.ts +++ b/packages/all/test/cross-origin-iframe-packer.test.ts @@ -149,7 +149,7 @@ describe('cross origin iframes & packer', function (this: ISuite) { const unpackedSnapshots = packedSnapshots.map((packed) => unpack(packed), ) as eventWithTime[]; - assertSnapshot(unpackedSnapshots); + await assertSnapshot(unpackedSnapshots); }); }); }); diff --git a/packages/packer/CHANGELOG.md b/packages/packer/CHANGELOG.md index 1f2f6138ea..9e037c4883 100644 --- a/packages/packer/CHANGELOG.md +++ b/packages/packer/CHANGELOG.md @@ -1,5 +1,23 @@ # @rrweb/packer +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb-types@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages + +- Updated dependencies [[`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158)]: + - @rrweb/types@2.0.0-alpha.17 + ## 2.0.0 ### Major Changes diff --git a/packages/packer/package.json b/packages/packer/package.json index 177d046af8..43d545c6e2 100644 --- a/packages/packer/package.json +++ b/packages/packer/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrweb-packer", - "version": "2.0.0", + "version": "2.0.13", "publishConfig": { "access": "public" }, @@ -10,11 +10,11 @@ ], "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "test": "vitest run", "test:watch": "vitest watch", "check-types": "tsc -noEmit", - "prepublish": "npm run build", + "prepublish": "tsc -noEmit && vite build", "lint": "yarn eslint src/**/*.ts" }, "bugs": { @@ -70,14 +70,14 @@ "package.json" ], "devDependencies": { - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1", "vitest": "^1.4.0", - "typescript": "^4.7.3" + "typescript": "^5.4.5" }, "dependencies": { "fflate": "^0.4.4", - "@saola.ai/rrweb-types": "^2.0.0" + "@saola.ai/rrweb-types": "^2.0.13" }, "browserslist": [ "supports es6-class" diff --git a/packages/packer/src/pack.ts b/packages/packer/src/pack.ts index 71bec26f53..218b2424b4 100644 --- a/packages/packer/src/pack.ts +++ b/packages/packer/src/pack.ts @@ -1,6 +1,6 @@ import { strFromU8, strToU8, zlibSync } from 'fflate'; import type { PackFn } from '@saola.ai/rrweb-types'; -import { eventWithTimeAndPacker, MARK } from './base'; +import { type eventWithTimeAndPacker, MARK } from './base'; export const pack: PackFn = (event) => { const _e: eventWithTimeAndPacker = { diff --git a/packages/packer/src/unpack.ts b/packages/packer/src/unpack.ts index e352b6f303..78123bf9ed 100644 --- a/packages/packer/src/unpack.ts +++ b/packages/packer/src/unpack.ts @@ -1,5 +1,5 @@ import { strFromU8, strToU8, unzlibSync } from 'fflate'; -import { eventWithTimeAndPacker, MARK } from './base'; +import { type eventWithTimeAndPacker, MARK } from './base'; import type { UnpackFn, eventWithTime } from '@saola.ai/rrweb-types'; export const unpack: UnpackFn = (raw: string) => { diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/CHANGELOG.md b/packages/plugins/rrweb-plugin-canvas-webrtc-record/CHANGELOG.md index 354b9cadfc..2d6e25305a 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-record/CHANGELOG.md +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/CHANGELOG.md @@ -1,5 +1,23 @@ # @rrweb/rrweb-plugin-canvas-webrtc-record +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]: + - rrweb@2.0.0-alpha.17 + ## 2.0.0 ### Major Changes diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json b/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json index c3e8710659..0e173ad2b0 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrweb-plugin-canvas-webrtc-record", - "version": "2.0.0", + "version": "2.0.13", "description": "", "type": "module", "main": "./dist/rrweb-plugin-canvas-webrtc-record.umd.cjs", @@ -25,9 +25,9 @@ ], "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "check-types": "tsc -noEmit", - "prepublish": "npm run build" + "prepublish": "tsc -noEmit && vite build" }, "repository": { "type": "git", @@ -43,12 +43,12 @@ }, "homepage": "https://github.com/rrweb-io/rrweb#readme", "devDependencies": { - "@saola.ai/rrweb": "^2.0.0", - "typescript": "^4.7.3", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1" + "@saola.ai/rrweb": "^2.0.13", + "typescript": "^5.4.5", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1" }, "peerDependencies": { - "@saola.ai/rrweb": "^2.0.0" + "@saola.ai/rrweb": "^2.0.13" } } diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/CHANGELOG.md b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/CHANGELOG.md index 793c832bea..e741fc5f13 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/CHANGELOG.md +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/CHANGELOG.md @@ -1,5 +1,23 @@ # @rrweb/rrweb-plugin-canvas-webrtc-replay +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]: + - rrweb@2.0.0-alpha.17 + ## 2.0.0 ### Major Changes diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json index e20516ecb5..ad134fb853 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrweb-plugin-canvas-webrtc-replay", - "version": "2.0.0", + "version": "2.0.13", "description": "", "type": "module", "main": "./dist/rrweb-plugin-canvas-webrtc-replay.umd.cjs", @@ -25,9 +25,9 @@ ], "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "check-types": "tsc -noEmit", - "prepublish": "npm run build" + "prepublish": "tsc -noEmit && vite build" }, "repository": { "type": "git", @@ -43,12 +43,12 @@ }, "homepage": "https://github.com/rrweb-io/rrweb#readme", "devDependencies": { - "@saola.ai/rrweb": "^2.0.0", - "typescript": "^4.7.3", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1" + "@saola.ai/rrweb": "^2.0.13", + "typescript": "^5.4.5", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1" }, "peerDependencies": { - "@saola.ai/rrweb": "^2.0.0" + "@saola.ai/rrweb": "^2.0.13" } } diff --git a/packages/plugins/rrweb-plugin-console-record/CHANGELOG.md b/packages/plugins/rrweb-plugin-console-record/CHANGELOG.md index b39dbbf24c..892749f7fc 100644 --- a/packages/plugins/rrweb-plugin-console-record/CHANGELOG.md +++ b/packages/plugins/rrweb-plugin-console-record/CHANGELOG.md @@ -1,5 +1,25 @@ # @rrweb/rrweb-plugin-console-record +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages + +- [#1530](https://github.com/rrweb-io/rrweb/pull/1530) [`874933b`](https://github.com/rrweb-io/rrweb/commit/874933b55069759b932b3365025449afc9b2f5c7) Thanks [@pauldambra](https://github.com/pauldambra)! - corrects behaviour of console.assert logging to only capture logs when the provided assertion is falsey + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]: + - rrweb@2.0.0-alpha.17 + ## 2.0.0 ### Major Changes diff --git a/packages/plugins/rrweb-plugin-console-record/package.json b/packages/plugins/rrweb-plugin-console-record/package.json index 5196f8119c..430dd6002f 100644 --- a/packages/plugins/rrweb-plugin-console-record/package.json +++ b/packages/plugins/rrweb-plugin-console-record/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrweb-plugin-console-record", - "version": "2.0.0", + "version": "2.0.13", "description": "", "type": "module", "main": "./dist/rrweb-plugin-console-record.umd.cjs", @@ -27,9 +27,9 @@ "dev": "vite build --watch", "test": "vitest run", "test:watch": "vitest watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "check-types": "tsc -noEmit", - "prepublish": "npm run build" + "prepublish": "tsc -noEmit && vite build" }, "repository": { "type": "git", @@ -45,14 +45,14 @@ }, "homepage": "https://github.com/rrweb-io/rrweb#readme", "devDependencies": { - "@saola.ai/rrweb": "^2.0.0", - "typescript": "^4.7.3", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1", + "@saola.ai/rrweb": "^2.0.13", + "typescript": "^5.4.5", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1", "vitest": "^1.4.0", "puppeteer": "^20.9.0" }, "peerDependencies": { - "@saola.ai/rrweb": "^2.0.0" + "@saola.ai/rrweb": "^2.0.13" } } diff --git a/packages/plugins/rrweb-plugin-console-record/src/index.ts b/packages/plugins/rrweb-plugin-console-record/src/index.ts index 9516816b37..cb38d2ab80 100644 --- a/packages/plugins/rrweb-plugin-console-record/src/index.ts +++ b/packages/plugins/rrweb-plugin-console-record/src/index.ts @@ -193,6 +193,12 @@ function initLogObserver( (original: (...args: Array) => void) => { return (...args: Array) => { original.apply(this, args); + + if (level === 'assert' && !!args[0]) { + // assert does not log if the first argument evaluates to true + return; + } + if (inStack) { // If we are already in a stack this means something from the following code is calling a console method // likely a proxy method called from stringify. We don't want to log this as it will cause an infinite loop @@ -203,7 +209,11 @@ function initLogObserver( const trace = ErrorStackParser.parse(new Error()) .map((stackFrame: StackFrame) => stackFrame.toString()) .splice(1); // splice(1) to omit the hijacked log function - const payload = args.map((s) => + + // assert does not log its first arg, that's only used for deciding whether to log + const argsForPayload = level === 'assert' ? args.slice(1) : args; + + const payload = argsForPayload.map((s) => stringify(s, logOptions.stringifyOptions), ); logCount++; diff --git a/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap b/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap index ad900ed916..cd0df64e96 100644 --- a/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap +++ b/packages/plugins/rrweb-plugin-console-record/test/__snapshots__/index.test.ts.snap @@ -356,11 +356,10 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"assert\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:2:15\\" + \\"__puppeteer_evaluation_script__:3:15\\" ], \\"payload\\": [ - \\"true\\", - \\"\\\\\\"assert\\\\\\"\\" + \\"\\\\\\"should log assert\\\\\\"\\" ] } } @@ -372,7 +371,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"count\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:3:15\\" + \\"__puppeteer_evaluation_script__:4:15\\" ], \\"payload\\": [ \\"\\\\\\"count\\\\\\"\\" @@ -387,7 +386,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"countReset\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:4:15\\" + \\"__puppeteer_evaluation_script__:5:15\\" ], \\"payload\\": [ \\"\\\\\\"count\\\\\\"\\" @@ -402,7 +401,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"debug\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:5:15\\" + \\"__puppeteer_evaluation_script__:6:15\\" ], \\"payload\\": [ \\"\\\\\\"debug\\\\\\"\\" @@ -417,7 +416,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"dir\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:6:15\\" + \\"__puppeteer_evaluation_script__:7:15\\" ], \\"payload\\": [ \\"\\\\\\"dir\\\\\\"\\" @@ -432,7 +431,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"dirxml\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:7:15\\" + \\"__puppeteer_evaluation_script__:8:15\\" ], \\"payload\\": [ \\"\\\\\\"dirxml\\\\\\"\\" @@ -447,7 +446,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"group\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:8:15\\" + \\"__puppeteer_evaluation_script__:9:15\\" ], \\"payload\\": [] } @@ -460,7 +459,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"groupCollapsed\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:9:15\\" + \\"__puppeteer_evaluation_script__:10:15\\" ], \\"payload\\": [] } @@ -473,7 +472,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"info\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:10:15\\" + \\"__puppeteer_evaluation_script__:11:15\\" ], \\"payload\\": [ \\"\\\\\\"info\\\\\\"\\" @@ -488,7 +487,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"log\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:11:15\\" + \\"__puppeteer_evaluation_script__:12:15\\" ], \\"payload\\": [ \\"\\\\\\"log\\\\\\"\\" @@ -503,7 +502,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"table\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:12:15\\" + \\"__puppeteer_evaluation_script__:13:15\\" ], \\"payload\\": [ \\"\\\\\\"table\\\\\\"\\" @@ -518,7 +517,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"time\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:13:15\\" + \\"__puppeteer_evaluation_script__:14:15\\" ], \\"payload\\": [] } @@ -531,7 +530,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"timeEnd\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:14:15\\" + \\"__puppeteer_evaluation_script__:15:15\\" ], \\"payload\\": [] } @@ -544,7 +543,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"timeLog\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:15:15\\" + \\"__puppeteer_evaluation_script__:16:15\\" ], \\"payload\\": [] } @@ -557,7 +556,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"trace\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:16:15\\" + \\"__puppeteer_evaluation_script__:17:15\\" ], \\"payload\\": [ \\"\\\\\\"trace\\\\\\"\\" @@ -572,7 +571,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"warn\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:17:15\\" + \\"__puppeteer_evaluation_script__:18:15\\" ], \\"payload\\": [ \\"\\\\\\"warn\\\\\\"\\" @@ -587,7 +586,7 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"clear\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:18:15\\" + \\"__puppeteer_evaluation_script__:19:15\\" ], \\"payload\\": [] } @@ -600,10 +599,10 @@ exports[`rrweb-plugin-console-record > should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"log\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:19:15\\" + \\"__puppeteer_evaluation_script__:20:15\\" ], \\"payload\\": [ - \\"\\\\\\"TypeError: a message\\\\\\\\n at __puppeteer_evaluation_script__:19:19\\\\\\\\nEnd of stack for Error object\\\\\\"\\" + \\"\\\\\\"TypeError: a message\\\\\\\\n at __puppeteer_evaluation_script__:20:19\\\\\\\\nEnd of stack for Error object\\\\\\"\\" ] } } diff --git a/packages/plugins/rrweb-plugin-console-record/test/index.test.ts b/packages/plugins/rrweb-plugin-console-record/test/index.test.ts index 6630a436c7..00f0d244b1 100644 --- a/packages/plugins/rrweb-plugin-console-record/test/index.test.ts +++ b/packages/plugins/rrweb-plugin-console-record/test/index.test.ts @@ -87,14 +87,17 @@ describe('rrweb-plugin-console-record', () => { 'window.snapshots', )) as eventWithTime[]; // The snapshots should containe 1 console log, not multiple. - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record console messages', async () => { await page.goto(`${serverUrl}test/html/log.html`); await page.evaluate(() => { - console.assert(0 === 0, 'assert'); + // truthy assert does not log + console.assert(0 === 0, 'should not log assert'); + // falsy assert does log + console.assert(false, 'should log assert'); console.count('count'); console.countReset('count'); console.debug('debug'); @@ -123,6 +126,6 @@ describe('rrweb-plugin-console-record', () => { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); }); diff --git a/packages/plugins/rrweb-plugin-console-replay/CHANGELOG.md b/packages/plugins/rrweb-plugin-console-replay/CHANGELOG.md index 324e492972..d264d26a87 100644 --- a/packages/plugins/rrweb-plugin-console-replay/CHANGELOG.md +++ b/packages/plugins/rrweb-plugin-console-replay/CHANGELOG.md @@ -1,5 +1,23 @@ # @rrweb/rrweb-plugin-console-replay +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]: + - rrweb@2.0.0-alpha.17 + ## 2.0.0 ### Major Changes diff --git a/packages/plugins/rrweb-plugin-console-replay/package.json b/packages/plugins/rrweb-plugin-console-replay/package.json index 744be0d35f..be65602323 100644 --- a/packages/plugins/rrweb-plugin-console-replay/package.json +++ b/packages/plugins/rrweb-plugin-console-replay/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrweb-plugin-console-replay", - "version": "2.0.0", + "version": "2.0.13", "description": "", "type": "module", "main": "./dist/rrweb-plugin-console-replay.umd.cjs", @@ -25,9 +25,9 @@ ], "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "check-types": "tsc -noEmit", - "prepublish": "npm run build" + "prepublish": "tsc -noEmit && vite build" }, "repository": { "type": "git", @@ -43,13 +43,13 @@ }, "homepage": "https://github.com/rrweb-io/rrweb#readme", "devDependencies": { - "@saola.ai/rrweb-plugin-console-record": "^2.0.0", - "@saola.ai/rrweb": "^2.0.0", - "typescript": "^4.7.3", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1" + "@saola.ai/rrweb-plugin-console-record": "^2.0.13", + "@saola.ai/rrweb": "^2.0.13", + "typescript": "^5.4.5", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1" }, "peerDependencies": { - "@saola.ai/rrweb": "^2.0.0" + "@saola.ai/rrweb": "^2.0.13" } } diff --git a/packages/plugins/rrweb-plugin-console-replay/src/index.ts b/packages/plugins/rrweb-plugin-console-replay/src/index.ts index 065320c551..152713829e 100644 --- a/packages/plugins/rrweb-plugin-console-replay/src/index.ts +++ b/packages/plugins/rrweb-plugin-console-replay/src/index.ts @@ -1,6 +1,6 @@ import { - LogLevel, - LogData, + type LogLevel, + type LogData, PLUGIN_NAME, } from '@saola.ai/rrweb-plugin-console-record'; import type { eventWithTime } from '@saola.ai/rrweb-types'; diff --git a/packages/plugins/rrweb-plugin-sequential-id-record/CHANGELOG.md b/packages/plugins/rrweb-plugin-sequential-id-record/CHANGELOG.md index d780eb5ae2..b3c0eb59bc 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-record/CHANGELOG.md +++ b/packages/plugins/rrweb-plugin-sequential-id-record/CHANGELOG.md @@ -1,5 +1,23 @@ # @rrweb/rrweb-plugin-sequential-id-record +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]: + - rrweb@2.0.0-alpha.17 + ## 2.0.0 ### Major Changes diff --git a/packages/plugins/rrweb-plugin-sequential-id-record/package.json b/packages/plugins/rrweb-plugin-sequential-id-record/package.json index 368489973c..65c4df5ca4 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-record/package.json +++ b/packages/plugins/rrweb-plugin-sequential-id-record/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrweb-plugin-sequential-id-record", - "version": "2.0.0", + "version": "2.0.13", "description": "", "type": "module", "main": "./dist/rrweb-plugin-sequential-id-record.umd.cjs", @@ -25,9 +25,9 @@ ], "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "check-types": "tsc -noEmit", - "prepublish": "npm run build" + "prepublish": "tsc -noEmit && vite build" }, "repository": { "type": "git", @@ -43,12 +43,12 @@ }, "homepage": "https://github.com/rrweb-io/rrweb#readme", "devDependencies": { - "@saola.ai/rrweb": "^2.0.0", - "typescript": "^4.7.3", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1" + "@saola.ai/rrweb": "^2.0.13", + "typescript": "^5.4.5", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1" }, "peerDependencies": { - "@saola.ai/rrweb": "^2.0.0" + "@saola.ai/rrweb": "^2.0.13" } } diff --git a/packages/plugins/rrweb-plugin-sequential-id-replay/CHANGELOG.md b/packages/plugins/rrweb-plugin-sequential-id-replay/CHANGELOG.md index eeeb0eefd5..c1bb348b94 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-replay/CHANGELOG.md +++ b/packages/plugins/rrweb-plugin-sequential-id-replay/CHANGELOG.md @@ -1,5 +1,23 @@ # @rrweb/rrweb-plugin-sequential-id-replay +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]: + - rrweb@2.0.0-alpha.17 + ## 2.0.0 ### Major Changes diff --git a/packages/plugins/rrweb-plugin-sequential-id-replay/package.json b/packages/plugins/rrweb-plugin-sequential-id-replay/package.json index 70bc3ec9c5..89b120dff9 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-replay/package.json +++ b/packages/plugins/rrweb-plugin-sequential-id-replay/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrweb-plugin-sequential-id-replay", - "version": "2.0.0", + "version": "2.0.13", "description": "", "type": "module", "main": "./dist/rrweb-plugin-sequential-id-replay.umd.cjs", @@ -25,9 +25,9 @@ ], "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "check-types": "tsc -noEmit", - "prepublish": "npm run build" + "prepublish": "tsc -noEmit && vite build" }, "repository": { "type": "git", @@ -43,13 +43,13 @@ }, "homepage": "https://github.com/rrweb-io/rrweb#readme", "devDependencies": { - "@saola.ai/rrweb-plugin-sequential-id-record": "^2.0.0", - "@saola.ai/rrweb": "^2.0.0", - "typescript": "^4.7.3", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1" + "@saola.ai/rrweb-plugin-sequential-id-record": "^2.0.13", + "@saola.ai/rrweb": "^2.0.13", + "typescript": "^5.4.5", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1" }, "peerDependencies": { - "@saola.ai/rrweb": "^2.0.0" + "@saola.ai/rrweb": "^2.0.13" } } diff --git a/packages/record/CHANGELOG.md b/packages/record/CHANGELOG.md index 1c24c04e4f..cb387a6097 100644 --- a/packages/record/CHANGELOG.md +++ b/packages/record/CHANGELOG.md @@ -1,5 +1,27 @@ # @rrweb/record +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb@2.0.13 + - @saola.ai/rrweb-types@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages + +- [#1515](https://github.com/rrweb-io/rrweb/pull/1515) [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6) Thanks [@okejminja](https://github.com/okejminja)! - Added support for deprecated addRule & removeRule methods + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]: + - rrweb@2.0.0-alpha.17 + - @rrweb/types@2.0.0-alpha.17 + ## 2.0.0 ### Major Changes diff --git a/packages/record/package.json b/packages/record/package.json index f3db181c7a..58849dba2f 100644 --- a/packages/record/package.json +++ b/packages/record/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/record", - "version": "2.0.0", + "version": "2.0.13", "publishConfig": { "access": "public" }, @@ -10,11 +10,11 @@ ], "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "test": "vitest run", "test:watch": "vitest watch", "check-types": "tsc -noEmit", - "prepublish": "npm run build", + "prepublish": "tsc -noEmit && vite build", "lint": "yarn eslint src/**/*.ts" }, "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/record#readme", @@ -49,14 +49,14 @@ ], "devDependencies": { "puppeteer": "^20.9.0", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1", "vitest": "^1.4.0", - "typescript": "^4.7.3" + "typescript": "^5.4.5" }, "dependencies": { - "@saola.ai/rrweb-types": "^2.0.0", - "@saola.ai/rrweb": "^2.0.0" + "@saola.ai/rrweb-types": "^2.0.13", + "@saola.ai/rrweb": "^2.0.13" }, "browserslist": [ "supports es6-class" diff --git a/packages/replay/CHANGELOG.md b/packages/replay/CHANGELOG.md index 4833c3147c..16374c7fca 100644 --- a/packages/replay/CHANGELOG.md +++ b/packages/replay/CHANGELOG.md @@ -1,5 +1,25 @@ # @rrweb/replay +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb@2.0.13 + - @saola.ai/rrweb-types@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- [`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c) Thanks [@Juice10](https://github.com/Juice10)! - Keep package version in sync with other packages + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]: + - rrweb@2.0.0-alpha.17 + - @rrweb/types@2.0.0-alpha.17 + ## 2.0.0 ### Major Changes diff --git a/packages/replay/package.json b/packages/replay/package.json index dd9a770727..9c9cedf021 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/replay", - "version": "2.0.0", + "version": "2.0.13", "publishConfig": { "access": "public" }, @@ -10,11 +10,11 @@ ], "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "test": "vitest run", "test:watch": "vitest watch", "check-types": "tsc -noEmit", - "prepublish": "npm run build", + "prepublish": "tsc -noEmit && vite build", "lint": "yarn eslint src/**/*.ts" }, "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/replay#readme", @@ -50,14 +50,14 @@ ], "devDependencies": { "puppeteer": "^20.9.0", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1", "vitest": "^1.4.0", - "typescript": "^4.7.3" + "typescript": "^5.4.5" }, "dependencies": { - "@saola.ai/rrweb-types": "^2.0.0", - "@saola.ai/rrweb": "^2.0.0" + "@saola.ai/rrweb-types": "^2.0.13", + "@saola.ai/rrweb": "^2.0.13" }, "browserslist": [ "supports es6-class" diff --git a/packages/replay/src/index.ts b/packages/replay/src/index.ts index 51cbb4b7db..75ccf41858 100644 --- a/packages/replay/src/index.ts +++ b/packages/replay/src/index.ts @@ -6,4 +6,9 @@ import { } from '@saola.ai/rrweb'; import '@saola.ai/rrweb/dist/style.css'; -export { Replayer, playerConfig, PlayerMachineState, SpeedMachineState }; +export { + Replayer, + type playerConfig, + type PlayerMachineState, + type SpeedMachineState, +}; diff --git a/packages/rrdom-nodejs/CHANGELOG.md b/packages/rrdom-nodejs/CHANGELOG.md index b198d1467e..0383ed3015 100644 --- a/packages/rrdom-nodejs/CHANGELOG.md +++ b/packages/rrdom-nodejs/CHANGELOG.md @@ -1,5 +1,31 @@ # rrdom-nodejs +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb-snapshot@2.0.13 + - @saola.ai/rrdom@2.0.13 + +## 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.12 ### Patch Changes diff --git a/packages/rrdom-nodejs/package.json b/packages/rrdom-nodejs/package.json index a320ae33d5..15f10e1552 100644 --- a/packages/rrdom-nodejs/package.json +++ b/packages/rrdom-nodejs/package.json @@ -1,13 +1,13 @@ { "name": "@saola.ai/rrdom-nodejs", - "version": "2.0.12", + "version": "2.0.13", "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "check-types": "tsc -noEmit", "test": "vitest run", "test:watch": "vitest watch", - "prepublish": "npm run build", + "prepublish": "tsc -noEmit && vite build", "lint": "yarn eslint src/**/*.ts" }, "keywords": [ @@ -46,19 +46,16 @@ "compare-versions": "^4.1.3", "eslint": "^8.15.0", "puppeteer": "^9.1.1", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1", "vitest": "^1.4.0", - "typescript": "^4.7.3" + "typescript": "^5.4.5" }, "dependencies": { "cssom": "^0.5.0", "cssstyle": "^2.3.0", "nwsapi": "^2.2.0", - "@saola.ai/rrdom": "^2.0.12", - "@saola.ai/rrweb-snapshot": "^2.0.12" - }, - "browserslist": [ - "supports es6-class" - ] + "@saola.ai/rrdom": "^2.0.13", + "@saola.ai/rrweb-snapshot": "^2.0.13" + } } diff --git a/packages/rrdom-nodejs/src/document-nodejs.ts b/packages/rrdom-nodejs/src/document-nodejs.ts index 7821cd4188..35564bc39d 100644 --- a/packages/rrdom-nodejs/src/document-nodejs.ts +++ b/packages/rrdom-nodejs/src/document-nodejs.ts @@ -12,8 +12,8 @@ import { BaseRRNode, BaseRRText, ClassList, - IRRDocument, - CSSStyleDeclaration, + type IRRDocument, + type CSSStyleDeclaration, } from '@saola.ai/rrdom'; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires const nwsapi = require('nwsapi'); diff --git a/packages/rrdom/CHANGELOG.md b/packages/rrdom/CHANGELOG.md index 03521ab112..d61490fbf0 100644 --- a/packages/rrdom/CHANGELOG.md +++ b/packages/rrdom/CHANGELOG.md @@ -1,5 +1,32 @@ # rrdom +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb-snapshot@2.0.13 + +## 2.0.0-alpha.17 + +### Minor Changes + +- [#1503](https://github.com/rrweb-io/rrweb/pull/1503) [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158) Thanks [@Juice10](https://github.com/Juice10)! - Support top-layer components. Fixes #1381. + +### Patch Changes + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`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 + +## 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 + ## 2.0.12 ### Patch Changes diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index 23a4e8e32c..fcaf5ecfc7 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrdom", - "version": "2.0.12", + "version": "2.0.13", "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/rrdom#readme", "license": "MIT", "type": "module", @@ -30,28 +30,28 @@ }, "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "check-types": "tsc -noEmit", "test": "vitest run", "test:watch": "vitest", - "prepublish": "npm run build", + "prepublish": "tsc -noEmit && vite build", "lint": "yarn eslint src/**/*.ts" }, "bugs": { "url": "https://github.com/rrweb-io/rrweb/issues" }, "devDependencies": { - "@saola.ai/rrweb-types": "^2.0.12", + "@saola.ai/rrweb-types": "^2.0.13", "@types/puppeteer": "^5.4.4", "@typescript-eslint/eslint-plugin": "^5.23.0", "@typescript-eslint/parser": "^5.23.0", "eslint": "^8.15.0", "puppeteer": "^17.1.3", - "typescript": "^4.9.0", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1" + "typescript": "^5.4.5", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1" }, "dependencies": { - "@saola.ai/rrweb-snapshot": "^2.0.12" + "@saola.ai/rrweb-snapshot": "^2.0.13" } } diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 52c4561f1e..3994b8ec61 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,7 +1,7 @@ import { NodeType as RRNodeType, Mirror as NodeMirror, - elementNode, + type elementNode, } from '@saola.ai/rrweb-snapshot'; import type { canvasMutationData, @@ -21,6 +21,7 @@ import type { } from './document'; import type { RRCanvasElement, + RRDialogElement, RRElement, RRIFrameElement, RRMediaElement, @@ -285,6 +286,29 @@ function diffAfterUpdatingChildren( ); break; } + case 'DIALOG': { + const dialog = oldElement as HTMLDialogElement; + const rrDialog = newRRElement as unknown as RRDialogElement; + const wasOpen = dialog.open; + const wasModal = dialog.matches('dialog:modal'); + const shouldBeOpen = rrDialog.open; + const shouldBeModal = rrDialog.isModal; + + const modalChanged = wasModal !== shouldBeModal; + const openChanged = wasOpen !== shouldBeOpen; + + if (modalChanged || (wasOpen && openChanged)) dialog.close(); + if (shouldBeOpen && (openChanged || modalChanged)) { + try { + if (shouldBeModal) dialog.showModal(); + else dialog.show(); + } catch (e) { + console.warn(e); + } + } + + break; + } } break; } @@ -330,12 +354,20 @@ function diffProps( } }; } else if (newTree.tagName === 'IFRAME' && name === 'srcdoc') continue; - else oldTree.setAttribute(name, newValue); + else { + try { + oldTree.setAttribute(name, newValue); + } catch (err) { + // We want to continue diffing so we quietly catch + // this exception. Otherwise, this can throw and bubble up to + // the `ReplayerEvents.Flush` listener and break rendering + console.warn(err); + } + } } for (const { name } of Array.from(oldAttributes)) if (!(name in newAttributes)) oldTree.removeAttribute(name); - newTree.scrollLeft && (oldTree.scrollLeft = newTree.scrollLeft); newTree.scrollTop && (oldTree.scrollTop = newTree.scrollTop); } diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index c82c56cea0..45ab2fede2 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -474,7 +474,8 @@ export class BaseRRElement extends BaseRRNode implements IRRElement { } public getAttribute(name: string): string | null { - return this.attributes[name] || null; + if (this.attributes[name] === undefined) return null; + return this.attributes[name]; } public setAttribute(name: string, attribute: string) { @@ -547,6 +548,30 @@ export class BaseRRMediaElement extends BaseRRElement { } } +export class BaseRRDialogElement extends BaseRRElement { + public readonly tagName = 'DIALOG' as const; + public readonly nodeName = 'DIALOG' as const; + + get isModal() { + return this.getAttribute('rr_open_mode') === 'modal'; + } + get open() { + return this.getAttribute('open') !== null; + } + public close() { + this.removeAttribute('open'); + this.removeAttribute('rr_open_mode'); + } + public show() { + this.setAttribute('open', ''); + this.setAttribute('rr_open_mode', 'non-modal'); + } + public showModal() { + this.setAttribute('open', ''); + this.setAttribute('rr_open_mode', 'modal'); + } +} + export class BaseRRText extends BaseRRNode implements IRRText { public readonly nodeType: number = NodeType.TEXT_NODE; public readonly nodeName = '#text' as const; diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts index c1c1d0ff83..a2d6e9e73b 100644 --- a/packages/rrdom/src/index.ts +++ b/packages/rrdom/src/index.ts @@ -31,6 +31,7 @@ import { type IRRDocumentType, type IRRText, type IRRComment, + BaseRRDialogElement, } from './document'; export class RRDocument extends BaseRRDocument { @@ -104,6 +105,9 @@ export class RRDocument extends BaseRRDocument { case 'STYLE': element = new RRStyleElement(upperTagName); break; + case 'DIALOG': + element = new RRDialogElement(upperTagName); + break; default: element = new RRElement(upperTagName); break; @@ -151,6 +155,8 @@ export class RRElement extends BaseRRElement { export class RRMediaElement extends BaseRRMediaElement {} +export class RRDialogElement extends BaseRRDialogElement {} + export class RRCanvasElement extends RRElement implements IRRElement { public rr_dataURL: string | null = null; public canvasMutations: { diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index ce38bbdd2f..b00b66a4be 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -338,6 +338,32 @@ describe('diff algorithm for rrdom', () => { expect((node as Node as HTMLElement).className).toBe('node'); }); + it('ignores invalid attributes', () => { + const tagName = 'DIV'; + const node = document.createElement(tagName); + const sn = Object.assign({}, elementSn, { + attributes: { '@click': 'foo' }, + tagName, + }); + mirror.add(node, sn); + + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + const sn2 = Object.assign({}, elementSn, { + attributes: { '@click': 'foo' }, + tagName, + }); + rrDocument.mirror.add(rrNode, sn2); + + rrNode.attributes = { id: 'node1', class: 'node', '@click': 'foo' }; + diff(node, rrNode, replayer); + expect((node as Node as HTMLElement).id).toBe('node1'); + expect((node as Node as HTMLElement).className).toBe('node'); + expect('@click' in (node as Node as HTMLElement)).toBe(false); + expect(warn).toHaveBeenCalledTimes(1); + warn.mockClear(); + }); + it('can update exist properties', () => { const tagName = 'DIV'; const node = document.createElement(tagName); diff --git a/packages/rrdom/test/diff/dialog.test.ts b/packages/rrdom/test/diff/dialog.test.ts new file mode 100644 index 0000000000..11a80e6ec5 --- /dev/null +++ b/packages/rrdom/test/diff/dialog.test.ts @@ -0,0 +1,112 @@ +/** + * @vitest-environment happy-dom + */ +import { vi, MockInstance } from 'vitest'; +import { + NodeType as RRNodeType, + createMirror, + Mirror as NodeMirror, + serializedNodeWithId, +} from 'rrweb-snapshot'; +import { RRDocument } from '../../src'; +import { diff, ReplayerHandler } from '../../src/diff'; + +describe('diff algorithm for rrdom', () => { + let mirror: NodeMirror; + let replayer: ReplayerHandler; + let warn: MockInstance; + let elementSn: serializedNodeWithId; + let elementSn2: serializedNodeWithId; + + beforeEach(() => { + mirror = createMirror(); + replayer = { + mirror, + applyCanvas: () => {}, + applyInput: () => {}, + applyScroll: () => {}, + applyStyleSheetMutation: () => {}, + afterAppend: () => {}, + }; + document.write(''); + // Mock the original console.warn function to make the test fail once console.warn is called. + warn = vi.spyOn(console, 'warn'); + + elementSn = { + type: RRNodeType.Element, + tagName: 'DIALOG', + attributes: {}, + childNodes: [], + id: 1, + }; + + elementSn2 = { + ...elementSn, + attributes: {}, + }; + }); + + afterEach(() => { + // Check that warn was not called (fail on warning) + expect(warn).not.toBeCalled(); + vi.resetAllMocks(); + }); + describe('diff dialog elements', () => { + vi.setConfig({ testTimeout: 60_000 }); + + it('should trigger `showModal` on rr_open_mode:modal attributes', () => { + const tagName = 'DIALOG'; + const node = document.createElement(tagName) as HTMLDialogElement; + vi.spyOn(node, 'matches').mockReturnValue(false); // matches is used to check if the dialog was opened with showModal + const showModalFn = vi.spyOn(node, 'showModal'); + + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.attributes = { rr_open_mode: 'modal', open: '' }; + + mirror.add(node, elementSn); + rrDocument.mirror.add(rrNode, elementSn); + diff(node, rrNode, replayer); + + expect(showModalFn).toBeCalled(); + }); + + it('should trigger `close` on rr_open_mode removed', () => { + const tagName = 'DIALOG'; + const node = document.createElement(tagName) as HTMLDialogElement; + node.showModal(); + vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal + const closeFn = vi.spyOn(node, 'close'); + + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.attributes = {}; + + mirror.add(node, elementSn); + rrDocument.mirror.add(rrNode, elementSn); + diff(node, rrNode, replayer); + + expect(closeFn).toBeCalled(); + }); + + it('should not trigger `close` on rr_open_mode is kept', () => { + const tagName = 'DIALOG'; + const node = document.createElement(tagName) as HTMLDialogElement; + vi.spyOn(node, 'matches').mockReturnValue(true); // matches is used to check if the dialog was opened with showModal + node.setAttribute('rr_open_mode', 'modal'); + node.setAttribute('open', ''); + const closeFn = vi.spyOn(node, 'close'); + + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.attributes = { rr_open_mode: 'modal', open: '' }; + + mirror.add(node, elementSn); + rrDocument.mirror.add(rrNode, elementSn); + diff(node, rrNode, replayer); + + expect(closeFn).not.toBeCalled(); + expect(node.open).toBe(true); + }); + }); +}); diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index d191fdda3f..3c622e81e3 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -7,6 +7,7 @@ import * as puppeteer from 'puppeteer'; import { vi } from 'vitest'; import { JSDOM } from 'jsdom'; import { + buildNodeWithSN, cdataNode, commentNode, documentNode, @@ -207,6 +208,33 @@ describe('RRDocument for browser environment', () => { 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', () => { diff --git a/packages/rrvideo/CHANGELOG.md b/packages/rrvideo/CHANGELOG.md index 88bbeb1527..a6de24633f 100644 --- a/packages/rrvideo/CHANGELOG.md +++ b/packages/rrvideo/CHANGELOG.md @@ -1,17 +1,33 @@ # rrvideo +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb-player@2.0.13 + +## 2.0.0-alpha.17 + ## 2.0.12 ### Patch Changes - Updated dependencies []: - - @saola.ai/rrweb-player@2.0.12 + - rrweb-player@2.0.0-alpha.17 + +## 2.0.0-alpha.16 + +- @saola.ai/rrweb-player@2.0.12 ## 2.0.11 ### Patch Changes - Updated dependencies []: + - rrweb-player@2.0.0-alpha.16 - @saola.ai/rrweb-player@2.0.11 ## 2.0.10 diff --git a/packages/rrvideo/package.json b/packages/rrvideo/package.json index 804b66ba3a..f4a92e729e 100644 --- a/packages/rrvideo/package.json +++ b/packages/rrvideo/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrvideo", - "version": "2.0.12", + "version": "2.0.13", "description": "transform rrweb session into video", "main": "build/index.js", "bin": { @@ -27,13 +27,13 @@ "@types/node": "^18.15.11", "jest": "^27.5.1", "ts-jest": "^27.1.3", - "@saola.ai/rrweb-types": "^2.0.12" + "@saola.ai/rrweb-types": "^2.0.13" }, "dependencies": { "@open-tech-world/cli-progress-bar": "^2.0.2", "fs-extra": "^11.1.1", "minimist": "^1.2.5", "playwright": "^1.32.1", - "@saola.ai/rrweb-player": "^2.0.12" + "@saola.ai/rrweb-player": "^2.0.13" } } diff --git a/packages/rrweb-player/.svelte-kit/ambient.d.ts b/packages/rrweb-player/.svelte-kit/ambient.d.ts index da2bb5235a..1d9b52fd46 100644 --- a/packages/rrweb-player/.svelte-kit/ambient.d.ts +++ b/packages/rrweb-player/.svelte-kit/ambient.d.ts @@ -30,50 +30,63 @@ declare module '$env/static/private' { export const npm_package_exports___node_polyfills_types: string; export const MANPATH: string; export const npm_package_scripts_test_cross_platform_build: string; + export const rvm_bin_path: string; export const TERM_PROGRAM: string; export const npm_package_exports___vite_import: string; export const npm_package_exports___hooks_import: string; export const NODE: string; export const npm_package_dependencies_sade: string; export const INIT_CWD: string; + export const GEM_HOME: string; export const NVM_CD_FLAGS: string; + export const PYENV_ROOT: string; export const npm_package_devDependencies_typescript: string; export const npm_package_homepage: string; export const npm_config_version_git_tag: string; - export const SHELL: string; export const TERM: string; + export const SHELL: string; export const npm_package_devDependencies_vite: string; + export const CLICOLOR: string; export const npm_package_dependencies_devalue: string; - export const TMPDIR: string; + export const IRBRC: string; export const HOMEBREW_REPOSITORY: string; + export const TMPDIR: string; export const npm_package_scripts_lint: string; export const npm_config_init_license: string; export const TERM_PROGRAM_VERSION: string; export const npm_package_dependencies_set_cookie_parser: string; export const npm_package_dependencies_cookie: string; + export const MY_RUBY_HOME: string; + export const red: string; export const TERM_SESSION_ID: string; export const npm_package_devDependencies_svelte_preprocess: string; export const npm_config_registry: string; + export const LC_ALL: string; export const npm_package_dependencies_import_meta_resolve: string; export const npm_package_repository_url: string; export const npm_package_readmeFilename: string; - export const USER: string; export const NVM_DIR: string; + export const USER: string; export const npm_package_exports___node_import: string; export const npm_package_description: string; export const npm_package_exports___package_json: string; export const npm_package_dependencies_esm_env: string; export const npm_package_license: string; + export const COMMAND_MODE: string; export const npm_package_exports___import: string; + export const rvm_path: string; export const npm_package_repository_directory: string; export const SSH_AUTH_SOCK: string; export const __CF_USER_TEXT_ENCODING: string; export const npm_package_bin_svelte_kit: string; + export const BASH_SILENCE_DEPRECATION_WARNING: string; export const npm_execpath: string; export const npm_package_devDependencies__types_sade: string; export const npm_package_peerDependencies__sveltejs_vite_plugin_svelte: string; export const npm_package_devDependencies_svelte: string; + export const LSCOLORS: string; export const YARN_IGNORE_PATH: string; + export const rvm_prefix: string; export const PATH: string; export const npm_config_argv: string; export const npm_package_scripts_postinstall: string; @@ -84,12 +97,14 @@ declare module '$env/static/private' { export const __CFBundleIdentifier: string; export const npm_package_keywords_4: string; export const PWD: string; + export const JAVA_HOME: string; export const npm_lifecycle_event: string; export const npm_package_types: string; export const npm_package_devDependencies__sveltejs_vite_plugin_svelte: string; export const npm_package_repository_type: string; export const npm_package_keywords_0: string; export const npm_package_name: string; + export const ITERM_PROFILE: string; export const npm_package_scripts_generate_types: string; export const npm_package_scripts_test_integration: string; export const npm_package_devDependencies__types_connect: string; @@ -108,23 +123,28 @@ declare module '$env/static/private' { export const npm_package_dependencies_sirv: string; export const XPC_SERVICE_NAME: string; export const npm_package_version: string; + export const rvm_version: string; export const SHLVL: string; export const HOME: string; + export const COLORFGBG: string; export const npm_package_type: string; export const npm_package_scripts_generate_version: string; export const npm_package_scripts_test: string; export const npm_package_scripts_check_all: string; + export const LC_TERMINAL_VERSION: string; export const npm_package_exports___vite_types: string; export const npm_package_exports___hooks_types: string; export const npm_config_save_prefix: string; export const npm_config_strict_ssl: string; export const HOMEBREW_PREFIX: string; export const npm_config_version_git_message: string; + export const ITERM_SESSION_ID: string; export const LOGNAME: string; export const npm_package_scripts_format: string; export const npm_package_peerDependencies_vite: string; export const npm_lifecycle_script: string; export const npm_package_peerDependencies_svelte: string; + export const GEM_PATH: string; export const LC_CTYPE: string; export const npm_config_ignore_path: string; export const npm_package_devDependencies__types_set_cookie_parser: string; @@ -134,11 +154,13 @@ declare module '$env/static/private' { export const npm_config_version_git_sign: string; export const npm_config_ignore_scripts: string; export const npm_config_user_agent: string; - export const HOMEBREW_CELLAR: string; export const INFOPATH: string; + export const HOMEBREW_CELLAR: string; export const npm_package_files_2: string; export const npm_package_devDependencies__types_node: string; export const npm_package_devDependencies__playwright_test: string; + export const DISPLAY: string; + export const LC_TERMINAL: string; export const npm_package_files_1: string; export const npm_package_devDependencies_dts_buddy: string; export const npm_package_files_0: string; @@ -146,11 +168,13 @@ declare module '$env/static/private' { export const npm_package_dependencies_kleur: string; export const npm_config_init_version: string; export const npm_config_ignore_optional: string; + export const RUBY_VERSION: string; export const SECURITYSESSIONID: string; export const npm_package_exports___node_types: string; export const npm_package_files_6: string; export const npm_package_scripts_check: string; export const npm_package_files_5: string; + export const COLORTERM: string; export const npm_node_execpath: string; export const npm_package_scripts_test_unit: string; export const npm_package_files_4: string; @@ -190,50 +214,63 @@ declare module '$env/dynamic/private' { npm_package_exports___node_polyfills_types: string; MANPATH: string; npm_package_scripts_test_cross_platform_build: string; + rvm_bin_path: string; TERM_PROGRAM: string; npm_package_exports___vite_import: string; npm_package_exports___hooks_import: string; NODE: string; npm_package_dependencies_sade: string; INIT_CWD: string; + GEM_HOME: string; NVM_CD_FLAGS: string; + PYENV_ROOT: string; npm_package_devDependencies_typescript: string; npm_package_homepage: string; npm_config_version_git_tag: string; - SHELL: string; TERM: string; + SHELL: string; npm_package_devDependencies_vite: string; + CLICOLOR: string; npm_package_dependencies_devalue: string; - TMPDIR: string; + IRBRC: string; HOMEBREW_REPOSITORY: string; + TMPDIR: string; npm_package_scripts_lint: string; npm_config_init_license: string; TERM_PROGRAM_VERSION: string; npm_package_dependencies_set_cookie_parser: string; npm_package_dependencies_cookie: string; + MY_RUBY_HOME: string; + red: string; TERM_SESSION_ID: string; npm_package_devDependencies_svelte_preprocess: string; npm_config_registry: string; + LC_ALL: string; npm_package_dependencies_import_meta_resolve: string; npm_package_repository_url: string; npm_package_readmeFilename: string; - USER: string; NVM_DIR: string; + USER: string; npm_package_exports___node_import: string; npm_package_description: string; npm_package_exports___package_json: string; npm_package_dependencies_esm_env: string; npm_package_license: string; + COMMAND_MODE: string; npm_package_exports___import: string; + rvm_path: string; npm_package_repository_directory: string; SSH_AUTH_SOCK: string; __CF_USER_TEXT_ENCODING: string; npm_package_bin_svelte_kit: string; + BASH_SILENCE_DEPRECATION_WARNING: string; npm_execpath: string; npm_package_devDependencies__types_sade: string; npm_package_peerDependencies__sveltejs_vite_plugin_svelte: string; npm_package_devDependencies_svelte: string; + LSCOLORS: string; YARN_IGNORE_PATH: string; + rvm_prefix: string; PATH: string; npm_config_argv: string; npm_package_scripts_postinstall: string; @@ -244,12 +281,14 @@ declare module '$env/dynamic/private' { __CFBundleIdentifier: string; npm_package_keywords_4: string; PWD: string; + JAVA_HOME: string; npm_lifecycle_event: string; npm_package_types: string; npm_package_devDependencies__sveltejs_vite_plugin_svelte: string; npm_package_repository_type: string; npm_package_keywords_0: string; npm_package_name: string; + ITERM_PROFILE: string; npm_package_scripts_generate_types: string; npm_package_scripts_test_integration: string; npm_package_devDependencies__types_connect: string; @@ -268,23 +307,28 @@ declare module '$env/dynamic/private' { npm_package_dependencies_sirv: string; XPC_SERVICE_NAME: string; npm_package_version: string; + rvm_version: string; SHLVL: string; HOME: string; + COLORFGBG: string; npm_package_type: string; npm_package_scripts_generate_version: string; npm_package_scripts_test: string; npm_package_scripts_check_all: string; + LC_TERMINAL_VERSION: string; npm_package_exports___vite_types: string; npm_package_exports___hooks_types: string; npm_config_save_prefix: string; npm_config_strict_ssl: string; HOMEBREW_PREFIX: string; npm_config_version_git_message: string; + ITERM_SESSION_ID: string; LOGNAME: string; npm_package_scripts_format: string; npm_package_peerDependencies_vite: string; npm_lifecycle_script: string; npm_package_peerDependencies_svelte: string; + GEM_PATH: string; LC_CTYPE: string; npm_config_ignore_path: string; npm_package_devDependencies__types_set_cookie_parser: string; @@ -294,11 +338,13 @@ declare module '$env/dynamic/private' { npm_config_version_git_sign: string; npm_config_ignore_scripts: string; npm_config_user_agent: string; - HOMEBREW_CELLAR: string; INFOPATH: string; + HOMEBREW_CELLAR: string; npm_package_files_2: string; npm_package_devDependencies__types_node: string; npm_package_devDependencies__playwright_test: string; + DISPLAY: string; + LC_TERMINAL: string; npm_package_files_1: string; npm_package_devDependencies_dts_buddy: string; npm_package_files_0: string; @@ -306,11 +352,13 @@ declare module '$env/dynamic/private' { npm_package_dependencies_kleur: string; npm_config_init_version: string; npm_config_ignore_optional: string; + RUBY_VERSION: string; SECURITYSESSIONID: string; npm_package_exports___node_types: string; npm_package_files_6: string; npm_package_scripts_check: string; npm_package_files_5: string; + COLORTERM: string; npm_node_execpath: string; npm_package_scripts_test_unit: string; npm_package_files_4: string; diff --git a/packages/rrweb-player/CHANGELOG.md b/packages/rrweb-player/CHANGELOG.md index 92aa1f29ed..03fd79ace2 100644 --- a/packages/rrweb-player/CHANGELOG.md +++ b/packages/rrweb-player/CHANGELOG.md @@ -1,5 +1,25 @@ # rrweb-player +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb-packer@2.0.13 + - @saola.ai/replay@2.0.13 + +## 2.0.0-alpha.17 + +### Patch Changes + +- Updated dependencies [[`db20184`](https://github.com/rrweb-io/rrweb/commit/db201841accd2b5df3cd7c88779aa62ab158501c)]: + - @rrweb/packer@2.0.0-alpha.17 + - @rrweb/replay@2.0.0-alpha.17 + +## 2.0.0-alpha.16 + ## 2.0.12 ### Patch Changes diff --git a/packages/rrweb-player/package.json b/packages/rrweb-player/package.json index 8d6ea74f16..8737cc5d64 100644 --- a/packages/rrweb-player/package.json +++ b/packages/rrweb-player/package.json @@ -1,8 +1,8 @@ { "name": "@saola.ai/rrweb-player", - "version": "2.0.12", + "version": "2.0.13", "devDependencies": { - "@saola.ai/rrweb-types": "^2.0.12", + "@saola.ai/rrweb-types": "^2.0.13", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/package": "^2.0.0", @@ -16,12 +16,12 @@ "svelte-preprocess": "^5.0.3", "svelte2tsx": "^0.7.6", "tslib": "^2.0.0", - "vite": "^5.2.8" + "vite": "^5.3.1" }, "dependencies": { "@tsconfig/svelte": "^1.0.0", - "@saola.ai/replay": "^2.0.0", - "@saola.ai/rrweb-packer": "^2.0.0" + "@saola.ai/replay": "^2.0.13", + "@saola.ai/rrweb-packer": "^2.0.13" }, "scripts": { "dev": "vite build --watch", diff --git a/packages/rrweb-snapshot/CHANGELOG.md b/packages/rrweb-snapshot/CHANGELOG.md index a0752b4f0a..079cfe8f4f 100644 --- a/packages/rrweb-snapshot/CHANGELOG.md +++ b/packages/rrweb-snapshot/CHANGELOG.md @@ -1,5 +1,33 @@ # rrweb-snapshot +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +## 2.0.0-alpha.17 + +### Minor Changes + +- [#1503](https://github.com/rrweb-io/rrweb/pull/1503) [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158) Thanks [@Juice10](https://github.com/Juice10)! - Record dialog's modal status for replay in rrweb. (Currently triggering `dialog.showModal()` is not supported in rrweb-snapshot's rebuild) + +### Patch Changes + +- [#1417](https://github.com/rrweb-io/rrweb/pull/1417) [`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - fix: duplicate textContent for style elements cause incremental style mutations to be invalid + +- [#1533](https://github.com/rrweb-io/rrweb/pull/1533) [`d350da8`](https://github.com/rrweb-io/rrweb/commit/d350da8552d8616dd118ee550bdfbce082986562) Thanks [@jeffdnguyen](https://github.com/jeffdnguyen)! - Fix `url()` rewrite for nested stylesheets by rewriting during stringification instead of after + +- [#1509](https://github.com/rrweb-io/rrweb/pull/1509) [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414) Thanks [@Juice10](https://github.com/Juice10)! - Reverse monkey patch built in methods to support LWC (and other frameworks like angular which monkey patch built in methods). + +## 2.0.0-alpha.16 + +### Patch Changes + +- [#1386](https://github.com/rrweb-io/rrweb/pull/1386) [`a2c8a1a`](https://github.com/rrweb-io/rrweb/commit/a2c8a1a37bfcf8389b280af792262c8263a979a3) Thanks [@ababik](https://github.com/ababik)! - Fix that the optional `maskInputFn` was being accidentally ignored during the creation of the full snapshot + +- [#1512](https://github.com/rrweb-io/rrweb/pull/1512) [`d08624c`](https://github.com/rrweb-io/rrweb/commit/d08624cb28add386c3618a0e6607424c3f1884d8) Thanks [@eoghanmurray](https://github.com/eoghanmurray)! - optimisation: skip mask check on leaf elements + ## 2.0.12 ## 2.0.11 diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index 0f8d24b45e..f45c60ec66 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -1,18 +1,20 @@ { "name": "@saola.ai/rrweb-snapshot", - "version": "2.0.12", + "version": "2.0.13", "description": "rrweb's component to take a snapshot of DOM, aka DOM serializer", "scripts": { "prepare": "npm run prepack", "prepack": "npm run build", - "retest": "jest", - "test": "vitest run", + "retest": "vitest run", + "test": "yarn build && vitest run", "test:watch": "vitest watch", - "test:update": "vitest run --update", + "retest:update": "vitest run --update", + "test:update": "yarn build && vitest run --update", + "bench": "vite build && vitest bench", "dev": "vite build --watch", - "build": "yarn check-types && vite build", + "build": "yarn turbo prepublish -F @saola.ai/rrweb-snapshot", "check-types": "tsc --noEmit", - "prepublish": "npm run build", + "prepublish": "yarn check-types && vite build", "lint": "yarn eslint src" }, "type": "module", @@ -52,16 +54,19 @@ }, "homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme", "devDependencies": { + "@rrweb/utils": "^2.0.0-alpha.17", "@types/jsdom": "^20.0.0", "@types/node": "^18.15.11", "@types/puppeteer": "^5.4.4", - "cross-env": "^5.2.0", "puppeteer": "^17.1.3", "ts-node": "^7.0.1", "tslib": "^1.9.3", - "typescript": "^4.7.3", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1", + "typescript": "^5.4.5", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1", "vitest": "^1.4.0" + }, + "dependencies": { + "postcss": "^8.4.38" } } diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index d2755a03e5..ec85468af6 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -1,987 +1,41 @@ -/** - * This file is a fork of https://github.com/reworkcss/css/blob/master/lib/parse/index.js - * I fork it because: - * 1. The css library was built for node.js which does not have tree-shaking supports. - * 2. Rewrites into typescript give us a better type interface. - */ -/* eslint-disable tsdoc/syntax */ +import type { AcceptedPlugin, Rule } from 'postcss'; -export interface ParserOptions { - /** Silently fail on parse errors */ - silent?: boolean; - /** - * The path to the file containing css. - * Makes errors and source maps more helpful, by letting them know where code comes from. - */ - source?: string; -} - -/** - * Error thrown during parsing. - */ -export interface ParserError { - /** The full error message with the source position. */ - message?: string; - /** The error message without position. */ - reason?: string; - /** The value of options.source if passed to css.parse. Otherwise undefined. */ - filename?: string; - line?: number; - column?: number; - /** The portion of code that couldn't be parsed. */ - source?: string; -} - -export interface Loc { - line?: number; - column?: number; -} - -/** - * Base AST Tree Node. - */ -export interface Node { - /** The possible values are the ones listed in the Types section on https://github.com/reworkcss/css page. */ - type?: string; - /** A reference to the parent node, or null if the node has no parent. */ - parent?: Node; - /** Information about the position in the source string that corresponds to the node. */ - position?: { - start?: Loc; - end?: Loc; - /** The value of options.source if passed to css.parse. Otherwise undefined. */ - source?: string; - /** The full source string passed to css.parse. */ - content?: string; - }; -} - -export interface NodeWithRules extends Node { - /** Array of nodes with the types rule, comment and any of the at-rule types. */ - rules: Array; -} - -export interface Rule extends Node { - /** The list of selectors of the rule, split on commas. Each selector is trimmed from whitespace and comments. */ - selectors?: string[]; - /** Array of nodes with the types declaration and comment. */ - declarations?: Array; -} - -export interface Declaration extends Node { - /** The property name, trimmed from whitespace and comments. May not be empty. */ - property?: string; - /** The value of the property, trimmed from whitespace and comments. Empty values are allowed. */ - value?: string; -} - -/** - * A rule-level or declaration-level comment. Comments inside selectors, properties and values etc. are lost. - */ -export interface Comment extends Node { - comment?: string; -} - -/** - * The @charset at-rule. - */ -export interface Charset extends Node { - /** The part following @charset. */ - charset?: string; -} - -/** - * The @custom-media at-rule - */ -export interface CustomMedia extends Node { - /** The ---prefixed name. */ - name?: string; - /** The part following the name. */ - media?: string; -} - -/** - * The @document at-rule. - */ -export interface Document extends NodeWithRules { - /** The part following @document. */ - document?: string; - /** The vendor prefix in @document, or undefined if there is none. */ - vendor?: string; -} - -/** - * The @font-face at-rule. - */ -export interface FontFace extends Node { - /** Array of nodes with the types declaration and comment. */ - declarations?: Array; -} - -/** - * The @host at-rule. - */ -export type Host = NodeWithRules; - -/** - * The @import at-rule. - */ -export interface Import extends Node { - /** The part following @import. */ - import?: string; -} - -/** - * The @keyframes at-rule. - */ -export interface KeyFrames extends Node { - /** The name of the keyframes rule. */ - name?: string; - /** The vendor prefix in @keyframes, or undefined if there is none. */ - vendor?: string; - /** Array of nodes with the types keyframe and comment. */ - keyframes?: Array; -} - -export interface KeyFrame extends Node { - /** The list of "selectors" of the keyframe rule, split on commas. Each “selector” is trimmed from whitespace. */ - values?: string[]; - /** Array of nodes with the types declaration and comment. */ - declarations?: Array; -} - -/** - * The @media at-rule. - */ -export interface Media extends NodeWithRules { - /** The part following @media. */ - media?: string; -} - -/** - * The @namespace at-rule. - */ -export interface Namespace extends Node { - /** The part following @namespace. */ - namespace?: string; -} - -/** - * The @page at-rule. - */ -export interface Page extends Node { - /** The list of selectors of the rule, split on commas. Each selector is trimmed from whitespace and comments. */ - selectors?: string[]; - /** Array of nodes with the types declaration and comment. */ - declarations?: Array; -} - -/** - * The @supports at-rule. - */ -export interface Supports extends NodeWithRules { - /** The part following @supports. */ - supports?: string; -} - -/** All at-rules. */ -export type AtRule = - | Charset - | CustomMedia - | Document - | FontFace - | Host - | Import - | KeyFrames - | Media - | Namespace - | Page - | Supports; - -/** - * A collection of rules - */ -export interface StyleRules extends NodeWithRules { - source?: string; - /** Array of Errors. Errors collected during parsing when option silent is true. */ - parsingErrors?: ParserError[]; -} - -/** - * The root node returned by css.parse. - */ -export interface Stylesheet extends Node { - stylesheet?: StyleRules; -} - -// http://www.w3.org/TR/CSS21/grammar.html -// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027 -const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g; - -export function parse(css: string, options: ParserOptions = {}): Stylesheet { - /** - * Positional. - */ - - let lineno = 1; - let column = 1; - - /** - * Update lineno and column based on `str`. - */ - - function updatePosition(str: string) { - const lines = str.match(/\n/g); - if (lines) { - lineno += lines.length; - } - const i = str.lastIndexOf('\n'); - column = i === -1 ? column + str.length : str.length - i; - } - - /** - * Mark position and patch `node.position`. - */ - - function position() { - const start = { line: lineno, column }; - return ( - node: Rule | Declaration | Comment | AtRule | Stylesheet | KeyFrame, - ) => { - node.position = new Position(start); - whitespace(); - return node; - }; - } - - /** - * Store position information for a node - */ - - class Position { - public static content: string; - public content!: string; - public start!: Loc; - public end!: Loc; - public source?: string; - - constructor(start: Loc) { - this.start = start; - this.end = { line: lineno, column }; - this.source = options.source; - this.content = Position.content; - } - } - - /** - * Non-enumerable source string - */ - - Position.content = css; - - const errorsList: ParserError[] = []; - - function error(msg: string) { - const err = new Error( - `${options.source || ''}:${lineno}:${column}: ${msg}`, - ) as ParserError; - err.reason = msg; - err.filename = options.source; - err.line = lineno; - err.column = column; - err.source = css; - - if (options.silent) { - errorsList.push(err); - } else { - throw err; - } - } - - /** - * Parse stylesheet. - */ - - function stylesheet(): Stylesheet { - const rulesList = rules(); +const MEDIA_SELECTOR = /(max|min)-device-(width|height)/; +const MEDIA_SELECTOR_GLOBAL = new RegExp(MEDIA_SELECTOR.source, 'g'); +const mediaSelectorPlugin: AcceptedPlugin = { + postcssPlugin: 'postcss-custom-selectors', + prepare() { return { - type: 'stylesheet', - stylesheet: { - source: options.source, - rules: rulesList, - parsingErrors: errorsList, + postcssPlugin: 'postcss-custom-selectors', + AtRule: function (atrule) { + if (atrule.params.match(MEDIA_SELECTOR_GLOBAL)) { + atrule.params = atrule.params.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2'); + } }, }; - } - - /** - * Opening brace. - */ - - function open() { - return match(/^{\s*/); - } - - /** - * Closing brace. - */ - - function close() { - return match(/^}/); - } - - /** - * Parse ruleset. - */ - - function rules() { - let node: Rule | void; - const rules: Rule[] = []; - whitespace(); - comments(rules); - while (css.length && css.charAt(0) !== '}' && (node = atrule() || rule())) { - if (node) { - rules.push(node); - comments(rules); - } - } - return rules; - } - - /** - * Match `re` and return captures. - */ - - function match(re: RegExp) { - const m = re.exec(css); - if (!m) { - return; - } - const str = m[0]; - updatePosition(str); - css = css.slice(str.length); - return m; - } - - /** - * Parse whitespace. - */ - - function whitespace() { - match(/^\s*/); - } - - /** - * Parse comments; - */ - - function comments(rules: Rule[] = []) { - let c: Comment | void; - while ((c = comment())) { - if (c) { - rules.push(c); - } - c = comment(); - } - return rules; - } - - /** - * Parse comment. - */ - - function comment() { - const pos = position(); - if ('/' !== css.charAt(0) || '*' !== css.charAt(1)) { - return; - } - - let i = 2; - while ( - '' !== css.charAt(i) && - ('*' !== css.charAt(i) || '/' !== css.charAt(i + 1)) - ) { - ++i; - } - i += 2; - - if ('' === css.charAt(i - 1)) { - return error('End of comment missing'); - } - - const str = css.slice(2, i - 2); - column += 2; - updatePosition(str); - css = css.slice(i); - column += 2; - - return pos({ - type: 'comment', - comment: str, - }); - } - - /** - * Parse selector. - */ - - // originally from https://github.com/NxtChg/pieces/blob/3eb39c8287a97632e9347a24f333d52d916bc816/js/css_parser/css_parse.js#L46C1-L47C1 - const selectorMatcher = new RegExp( - '^((' + - [ - /[^\\]"(?:\\"|[^"])*"/.source, // consume any quoted parts (checking that the double quote isn't itself escaped) - /[^\\]'(?:\\'|[^'])*'/.source, // same but for single quotes - '[^{]', - ].join('|') + - ')+)', - ); - - function selector() { - whitespace(); - while (css[0] == '}') { - error('extra closing bracket'); - css = css.slice(1); - whitespace(); - } - - const m = match(selectorMatcher); - if (!m) { - return; - } - - /* @fix Remove all comments from selectors - * http://ostermiller.org/findcomment.html */ - const cleanedInput = m[0] - .trim() - .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '') - - // Handle strings by replacing commas inside them - .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, (m) => { - return m.replace(/,/g, '\u200C'); - }); - - // Split using a custom function and restore commas in strings - return customSplit(cleanedInput).map((s) => - s.replace(/\u200C/g, ',').trim(), - ); - } - - /** - * Split selector correctly, ensuring not to split on comma if inside (). - */ - - function customSplit(input: string) { - const result = []; - let currentSegment = ''; - let depthParentheses = 0; // Track depth of parentheses - let depthBrackets = 0; // Track depth of square brackets - let currentStringChar = null; - - for (const char of input) { - const hasStringEscape = currentSegment.endsWith('\\'); - - if (currentStringChar) { - if (currentStringChar === char && !hasStringEscape) { - currentStringChar = null; + }, +}; + +// Simplified from https://github.com/giuseppeg/postcss-pseudo-classes/blob/master/index.js +const pseudoClassPlugin: AcceptedPlugin = { + postcssPlugin: 'postcss-hover-classes', + prepare: function () { + const fixed: Rule[] = []; + return { + Rule: function (rule) { + if (fixed.indexOf(rule) !== -1) { + return; } - } else if (char === '(') { - depthParentheses++; - } else if (char === ')') { - depthParentheses--; - } else if (char === '[') { - depthBrackets++; - } else if (char === ']') { - depthBrackets--; - } else if ('\'"'.includes(char)) { - currentStringChar = char; - } - - // Split point is a comma that is not inside parentheses or square brackets - if (char === ',' && depthParentheses === 0 && depthBrackets === 0) { - result.push(currentSegment); - currentSegment = ''; - } else { - currentSegment += char; - } - } - - // Add the last segment - if (currentSegment) { - result.push(currentSegment); - } - - return result; - } - - /** - * Parse declaration. - */ - - function declaration(): Declaration | void | never { - const pos = position(); - - // prop - // eslint-disable-next-line no-useless-escape - const propMatch = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/); - if (!propMatch) { - return; - } - const prop = trim(propMatch[0]); - - // : - if (!match(/^:\s*/)) { - return error(`property missing ':'`); - } - - // val - // eslint-disable-next-line no-useless-escape - const val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/); - - const ret = pos({ - type: 'declaration', - property: prop.replace(commentre, ''), - value: val ? trim(val[0]).replace(commentre, '') : '', - }); - - // ; - match(/^[;\s]*/); - - return ret; - } - - /** - * Parse declarations. - */ - - function declarations() { - const decls: Array = []; - - if (!open()) { - return error(`missing '{'`); - } - comments(decls); - - // declarations - let decl; - while ((decl = declaration())) { - if ((decl as unknown) !== false) { - decls.push(decl); - comments(decls); - } - decl = declaration(); - } - - if (!close()) { - return error(`missing '}'`); - } - return decls; - } - - /** - * Parse keyframe. - */ - - function keyframe() { - let m; - const vals = []; - const pos = position(); - - while ((m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/))) { - vals.push(m[1]); - match(/^,\s*/); - } - - if (!vals.length) { - return; - } - - return pos({ - type: 'keyframe', - values: vals, - declarations: declarations() as Declaration[], - }); - } - - /** - * Parse keyframes. - */ - - function atkeyframes() { - const pos = position(); - let m = match(/^@([-\w]+)?keyframes\s*/); - - if (!m) { - return; - } - const vendor = m[1]; - - // identifier - m = match(/^([-\w]+)\s*/); - if (!m) { - return error('@keyframes missing name'); - } - const name = m[1]; - - if (!open()) { - return error(`@keyframes missing '{'`); - } - - let frame; - let frames = comments(); - while ((frame = keyframe())) { - frames.push(frame); - frames = frames.concat(comments()); - } - - if (!close()) { - return error(`@keyframes missing '}'`); - } - - return pos({ - type: 'keyframes', - name, - vendor, - keyframes: frames, - }); - } - - /** - * Parse supports. - */ - - function atsupports() { - const pos = position(); - const m = match(/^@supports *([^{]+)/); - - if (!m) { - return; - } - const supports = trim(m[1]); - - if (!open()) { - return error(`@supports missing '{'`); - } - - const style = comments().concat(rules()); - - if (!close()) { - return error(`@supports missing '}'`); - } - - return pos({ - type: 'supports', - supports, - rules: style, - }); - } - - /** - * Parse host. - */ - - function athost() { - const pos = position(); - const m = match(/^@host\s*/); - - if (!m) { - return; - } - - if (!open()) { - return error(`@host missing '{'`); - } - - const style = comments().concat(rules()); - - if (!close()) { - return error(`@host missing '}'`); - } - - return pos({ - type: 'host', - rules: style, - }); - } - - /** - * Parse media. - */ - - function atmedia() { - const pos = position(); - const m = match(/^@media *([^{]+)/); - - if (!m) { - return; - } - const media = trim(m[1]); - - if (!open()) { - return error(`@media missing '{'`); - } - - const style = comments().concat(rules()); - - if (!close()) { - return error(`@media missing '}'`); - } - - return pos({ - type: 'media', - media, - rules: style, - }); - } - - /** - * Parse custom-media. - */ - - function atcustommedia() { - const pos = position(); - const m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/); - if (!m) { - return; - } - - return pos({ - type: 'custom-media', - name: trim(m[1]), - media: trim(m[2]), - }); - } - - /** - * Parse paged media. - */ - - function atpage() { - const pos = position(); - const m = match(/^@page */); - if (!m) { - return; - } - - const sel = selector() || []; - - if (!open()) { - return error(`@page missing '{'`); - } - let decls = comments(); - - // declarations - let decl; - while ((decl = declaration())) { - decls.push(decl); - decls = decls.concat(comments()); - } - - if (!close()) { - return error(`@page missing '}'`); - } - - return pos({ - type: 'page', - selectors: sel, - declarations: decls, - }); - } - - /** - * Parse document. - */ - - function atdocument() { - const pos = position(); - const m = match(/^@([-\w]+)?document *([^{]+)/); - if (!m) { - return; - } - - const vendor = trim(m[1]); - const doc = trim(m[2]); - - if (!open()) { - return error(`@document missing '{'`); - } - - const style = comments().concat(rules()); - - if (!close()) { - return error(`@document missing '}'`); - } - - return pos({ - type: 'document', - document: doc, - vendor, - rules: style, - }); - } - - /** - * Parse font-face. - */ - - function atfontface() { - const pos = position(); - const m = match(/^@font-face\s*/); - if (!m) { - return; - } - - if (!open()) { - return error(`@font-face missing '{'`); - } - let decls = comments(); - - // declarations - let decl; - while ((decl = declaration())) { - decls.push(decl); - decls = decls.concat(comments()); - } - - if (!close()) { - return error(`@font-face missing '}'`); - } - - return pos({ - type: 'font-face', - declarations: decls, - }); - } - - /** - * Parse import - */ - - const atimport = _compileAtrule('import'); - - /** - * Parse charset - */ - - const atcharset = _compileAtrule('charset'); - - /** - * Parse namespace - */ - - const atnamespace = _compileAtrule('namespace'); - - /** - * Parse non-block at-rules - */ - - function _compileAtrule(name: string) { - const re = new RegExp( - '^@' + - name + - '\\s*((?:' + - [ - /[^\\]"(?:\\"|[^"])*"/.source, // consume any quoted parts (checking that the double quote isn't itself escaped) - /[^\\]'(?:\\'|[^'])*'/.source, // same but for single quotes - '[^;]', - ].join('|') + - ')+);', - ); - return () => { - const pos = position(); - const m = match(re); - if (!m) { - return; - } - const ret: Record = { type: name }; - ret[name] = m[1].trim(); - return pos(ret); + fixed.push(rule); + rule.selectors.forEach(function (selector) { + if (selector.includes(':hover')) { + rule.selector += ',\n' + selector.replace(/:hover/g, '.\\:hover'); + } + }); + }, }; - } - - /** - * Parse at rule. - */ - - function atrule() { - if (css[0] !== '@') { - return; - } - - return ( - atkeyframes() || - atmedia() || - atcustommedia() || - atsupports() || - atimport() || - atcharset() || - atnamespace() || - atdocument() || - atpage() || - athost() || - atfontface() - ); - } - - /** - * Parse rule. - */ - - function rule() { - const pos = position(); - const sel = selector(); - - if (!sel) { - return error('selector missing'); - } - comments(); - - return pos({ - type: 'rule', - selectors: sel, - declarations: declarations() as Declaration[], - }); - } - - return addParent(stylesheet()); -} - -/** - * Trim `str`. - */ - -function trim(str: string) { - return str ? str.replace(/^\s+|\s+$/g, '') : ''; -} - -/** - * Adds non-enumerable parent node reference to each node. - */ - -function addParent(obj: Stylesheet, parent?: Stylesheet): Stylesheet { - const isNode = obj && typeof obj.type === 'string'; - const childParent = isNode ? obj : parent; - - for (const k of Object.keys(obj)) { - const value = obj[k as keyof Stylesheet]; - if (Array.isArray(value)) { - value.forEach((v) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - addParent(v, childParent); - }); - } else if (value && typeof value === 'object') { - addParent(value as Stylesheet, childParent); - } - } - - if (isNode) { - Object.defineProperty(obj, 'parent', { - configurable: true, - writable: true, - enumerable: false, - value: parent || null, - }); - } + }, +}; - return obj; -} +export { mediaSelectorPlugin, pseudoClassPlugin }; diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index ccb6624c66..6641c2aa41 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -1,13 +1,16 @@ -import { Rule, Media, NodeWithRules, parse } from './css'; +import { mediaSelectorPlugin, pseudoClassPlugin } from './css'; import { - serializedNodeWithId, + type serializedNodeWithId, + type serializedElementNodeWithId, + type serializedTextNodeWithId, NodeType, - tagMap, - elementNode, - BuildCache, - legacyAttributes, + type tagMap, + type elementNode, + type BuildCache, + type legacyAttributes, } from './types'; import { isElement, Mirror, isNodeMetaEqual } from './utils'; +import postcss from 'postcss'; const tagMap: tagMap = { script: 'noscript', @@ -57,15 +60,6 @@ function getTagName(n: elementNode): string { return tagName; } -// based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping -function escapeRegExp(str: string) { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -const MEDIA_SELECTOR = /(max|min)-device-(width|height)/; -const MEDIA_SELECTOR_GLOBAL = new RegExp(MEDIA_SELECTOR.source, 'g'); -const HOVER_SELECTOR = /([^\\]):hover/; -const HOVER_SELECTOR_GLOBAL = new RegExp(HOVER_SELECTOR.source, 'g'); export function adaptCssForReplay( cssText: string, cache: BuildCache, @@ -74,70 +68,11 @@ export function adaptCssForReplay( const cachedStyle = cache?.stylesWithHoverClass.get(cssText); if (cachedStyle) return cachedStyle; - const ast = parse(cssText, { - silent: true, - }); - - if (!ast.stylesheet) { - return cssText; - } - - const selectors: string[] = []; - const medias: string[] = []; - function getSelectors(rule: Rule | Media | NodeWithRules) { - if ('selectors' in rule && rule.selectors) { - rule.selectors.forEach((selector: string) => { - if (HOVER_SELECTOR.test(selector)) { - selectors.push(selector); - } - }); - } - if ('media' in rule && rule.media && MEDIA_SELECTOR.test(rule.media)) { - medias.push(rule.media); - } - if ('rules' in rule && rule.rules) { - rule.rules.forEach(getSelectors); - } - } - getSelectors(ast.stylesheet); - - let result = cssText; - if (selectors.length > 0) { - const selectorMatcher = new RegExp( - selectors - .filter((selector, index) => selectors.indexOf(selector) === index) - .sort((a, b) => b.length - a.length) - .map((selector) => { - return escapeRegExp(selector); - }) - .join('|'), - 'g', - ); - result = result.replace(selectorMatcher, (selector) => { - const newSelector = selector.replace( - HOVER_SELECTOR_GLOBAL, - '$1.\\:hover', - ); - return `${selector}, ${newSelector}`; - }); - } - if (medias.length > 0) { - const mediaMatcher = new RegExp( - medias - .filter((media, index) => medias.indexOf(media) === index) - .sort((a, b) => b.length - a.length) - .map((media) => { - return escapeRegExp(media); - }) - .join('|'), - 'g', - ); - result = result.replace(mediaMatcher, (media) => { - // not attempting to maintain min-device-width along with min-width - // (it's non standard) - return media.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2'); - }); - } + const ast: { css: string } = postcss([ + mediaSelectorPlugin, + pseudoClassPlugin, + ]).process(cssText); + let result = ast.css; if (removeAnimationCss) result = result.replace(/animation.+?;/g, ''); cache?.stylesWithHoverClass.set(cssText, result); return result; @@ -150,6 +85,79 @@ export function createCache(): BuildCache { }; } +/** + * undo splitCssText/markCssSplits + * (would move to utils.ts but uses `adaptCssForReplay`) + */ +export function applyCssSplits( + n: serializedElementNodeWithId, + cssText: string, + hackCss: boolean, + cache: BuildCache, + removeAnimationCss: boolean, +): void { + const childTextNodes: serializedTextNodeWithId[] = []; + for (const scn of n.childNodes) { + if (scn.type === NodeType.Text) { + childTextNodes.push(scn); + } + } + const cssTextSplits = cssText.split('/* rr_split */'); + while ( + cssTextSplits.length > 1 && + cssTextSplits.length > childTextNodes.length + ) { + // unexpected: remerge the last two so that we don't discard any css + cssTextSplits.splice(-2, 2, cssTextSplits.slice(-2).join('')); + } + for (let i = 0; i < childTextNodes.length; i++) { + const childTextNode = childTextNodes[i]; + const cssTextSection = cssTextSplits[i]; + if (childTextNode && cssTextSection) { + // id will be assigned when these child nodes are + // iterated over in buildNodeWithSN + childTextNode.textContent = hackCss + ? adaptCssForReplay(cssTextSection, cache, removeAnimationCss) + : cssTextSection; + } + } +} + +/** + * Normally a +
hover me
" @@ -487,7 +613,7 @@ exports[`integration tests > [html file]: with-style-sheet.html 1`] = ` with style sheet - + " `; @@ -498,11 +624,224 @@ exports[`integration tests > [html file]: with-style-sheet-with-import.html 1`] with style sheet with import - + + " `; +exports[`integration tests > should be able to record elements even when .childNodes has been monkey patched 1`] = ` +"{ + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Document\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 14 + } + ], + \\"id\\": 13 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 16 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"a\\", + \\"id\\": 22 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"b\\", + \\"id\\": 25 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"c\\", + \\"id\\": 28 + } + ], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"d\\", + \\"id\\": 31 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 32 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 33 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 +}" +`; + exports[`shadow DOM integration tests > snapshot shadow DOM 1`] = ` "{ \\"type\\": 0, @@ -755,12 +1094,13 @@ exports[`shadow DOM integration tests > snapshot shadow DOM 1`] = ` { \\"type\\": 2, \\"tagName\\": \\"style\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"_cssText\\": \\":host { display: inline-block; width: 650px; font-family: \\\\\\"Roboto Slab\\\\\\"; contain: content; }:host([background]) { background: var(--background-color, #9E9E9E); border-radius: 10px; padding: 10px; }#panels { box-shadow: rgba(0, 0, 0, 0.3) 0px 2px 2px; background: white; border-radius: 3px; padding: 16px; height: 250px; overflow: auto; }#tabs { display: inline-flex; user-select: none; }#tabs slot { display: inline-flex; }#tabs ::slotted(*) { font: 400 16px / 22px Roboto; padding: 16px 8px; margin: 0px; text-align: center; width: 100px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; cursor: pointer; border-top-left-radius: 3px; border-top-right-radius: 3px; background: linear-gradient(rgb(250, 250, 250), rgb(238, 238, 238)); border: none; }#tabs ::slotted([aria-selected=\\\\\\"true\\\\\\"]) { font-weight: 600; background: white; box-shadow: none; }#tabs ::slotted(:focus) { z-index: 1; }#panels ::slotted([aria-hidden=\\\\\\"true\\\\\\"]) { display: none; }\\" + }, \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\":host { display: inline-block; width: 650px; font-family: \\\\\\"Roboto Slab\\\\\\"; contain: content; }:host([background]) { background: var(--background-color, #9E9E9E); border-radius: 10px; padding: 10px; }#panels { box-shadow: rgba(0, 0, 0, 0.3) 0px 2px 2px; background: white; border-radius: 3px; padding: 16px; height: 250px; overflow: auto; }#tabs { display: inline-flex; user-select: none; }#tabs slot { display: inline-flex; }#tabs ::slotted(*) { font: 400 16px / 22px Roboto; padding: 16px 8px; margin: 0px; text-align: center; width: 100px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; cursor: pointer; border-top-left-radius: 3px; border-top-right-radius: 3px; background: linear-gradient(rgb(250, 250, 250), rgb(238, 238, 238)); border: none; }#tabs ::slotted([aria-selected=\\\\\\"true\\\\\\"]) { font-weight: 600; background: white; box-shadow: none; }#tabs ::slotted(:focus) { z-index: 1; }#panels ::slotted([aria-hidden=\\\\\\"true\\\\\\"]) { display: none; }\\", - \\"isStyle\\": true, + \\"textContent\\": \\"\\", \\"id\\": 38 } ], diff --git a/packages/rrweb-snapshot/test/alt-css/alt-style.css b/packages/rrweb-snapshot/test/alt-css/alt-style.css new file mode 100644 index 0000000000..bda88053d8 --- /dev/null +++ b/packages/rrweb-snapshot/test/alt-css/alt-style.css @@ -0,0 +1,12 @@ +body { + margin: 0; + background: url('../should-be-in-root-folder.jpg'); + border-image: url('data:image/svg+xml;utf8,'); + } + p { + color: red; + background: url('./should-be-in-alt-css-folder.jpg'); + } + body > p { + color: yellow; + } diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts index fc8e5a2946..75e261c102 100644 --- a/packages/rrweb-snapshot/test/css.test.ts +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -1,255 +1,250 @@ -import { describe, it, expect } from 'vitest'; -import { parse, Rule, Media } from '../src/css'; -import { fixSafariColons, escapeImportStatement } from './../src/utils'; +/** + * @vitest-environment jsdom + */ +import { describe, it, beforeEach, expect } from 'vitest'; +import { mediaSelectorPlugin, pseudoClassPlugin } from '../src/css'; +import postcss, { type AcceptedPlugin } from 'postcss'; +import { JSDOM } from 'jsdom'; +import { splitCssText, stringifyStylesheet } from './../src/utils'; +import { applyCssSplits } from './../src/rebuild'; +import { + NodeType, + type serializedElementNodeWithId, + type BuildCache, + type textNode, +} from '../src/types'; +import { Window } from 'happy-dom'; describe('css parser', () => { - it('should save the filename and source', () => { - const css = 'booty {\n size: large;\n}\n'; - const ast = parse(css, { - source: 'booty.css', + function parse(plugin: AcceptedPlugin, input: string): string { + const ast = postcss([plugin]).process(input, {}); + return ast.css; + } + + describe('mediaSelectorPlugin', () => { + it('selectors without device remain unchanged', () => { + const cssText = + '@media only screen and (min-width: 1200px) { .a { width: 10px; }}'; + expect(parse(mediaSelectorPlugin, cssText)).toEqual(cssText); }); - expect(ast.stylesheet!.source).toEqual('booty.css'); - - const position = ast.stylesheet!.rules[0].position!; - expect(position.start).toBeTruthy(); - expect(position.end).toBeTruthy(); - expect(position.source).toEqual('booty.css'); - expect(position.content).toEqual(css); + it('can adapt media rules to replay context', () => { + [ + ['min', 'width'], + ['min', 'height'], + ['max', 'width'], + ['max', 'height'], + ].forEach(([first, second]) => { + expect( + parse( + mediaSelectorPlugin, + `@media only screen and (${first}-device-${second}: 1200px) { .a { width: 10px; }}`, + ), + ).toEqual( + `@media only screen and (${first}-${second}: 1200px) { .a { width: 10px; }}`, + ); + }); + }); }); - it('should throw when a selector is missing', () => { - expect(() => { - parse('{size: large}'); - }).toThrow(); + describe('pseudoClassPlugin', () => { + it('parses nested commas in selectors correctly', () => { + const cssText = + 'body > ul :is(li:not(:first-of-type) a.current, li:not(:first-of-type).active a) {background: red;}'; + expect(parse(pseudoClassPlugin, cssText)).toEqual(cssText); + }); - expect(() => { - parse('b { color: red; }\n{ color: green; }\na { color: blue; }'); - }).toThrow(); - }); + it("doesn't ignore :hover within :is brackets", () => { + const cssText = + 'body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a) {background: red;}'; + expect(parse(pseudoClassPlugin, cssText)) + .toEqual(`body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a), +body > ul :is(li:not(:first-of-type) a.\\:hover, li:not(:first-of-type).active a) {background: red;}`); + }); - it('should throw when a broken comment is found', () => { - expect(() => { - parse('thing { color: red; } /* b { color: blue; }'); - }).toThrow(); + it('should parse selector with comma nested inside ()', () => { + const cssText = + '[_nghost-ng-c4172599085]:not(.fit-content).aim-select:hover:not(:disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--invalid, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--active) { border-color: rgb(84, 84, 84); }'; + expect(parse(pseudoClassPlugin, cssText)) + .toEqual(`[_nghost-ng-c4172599085]:not(.fit-content).aim-select:hover:not(:disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--invalid, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--active), +[_nghost-ng-c4172599085]:not(.fit-content).aim-select.\\:hover:not(:disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--invalid, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--active) { border-color: rgb(84, 84, 84); }`); + }); - expect(() => { - parse('/*'); - }).toThrow(); + it('ignores ( in strings', () => { + const cssText = + 'li[attr="weirdly("] a:hover, li[attr="weirdly)"] a {background-color: red;}'; + expect(parse(pseudoClassPlugin, cssText)) + .toEqual(`li[attr="weirdly("] a:hover, li[attr="weirdly)"] a, +li[attr="weirdly("] a.\\:hover {background-color: red;}`); + }); - /* Nested comments should be fine */ - expect(() => { - parse('/* /* */'); - }).not.toThrow(); - }); + it('ignores escaping in strings', () => { + const cssText = `li[attr="weirder\\"("] a:hover, li[attr="weirder\\")"] a {background-color: red;}`; + expect(parse(pseudoClassPlugin, cssText)) + .toEqual(`li[attr="weirder\\"("] a:hover, li[attr="weirder\\")"] a, +li[attr="weirder\\"("] a.\\:hover {background-color: red;}`); + }); - it('should allow empty property value', () => { - expect(() => { - parse('p { color:; }'); - }).not.toThrow(); + it('ignores comma in string', () => { + const cssText = 'li[attr="has,comma"] a:hover {background: red;}'; + expect(parse(pseudoClassPlugin, cssText)).toEqual( + `li[attr="has,comma"] a:hover, +li[attr="has,comma"] a.\\:hover {background: red;}`, + ); + }); }); +}); - it('should not throw with silent option', () => { - expect(() => { - parse('thing { color: red; } /* b { color: blue; }', { silent: true }); - }).not.toThrow(); +describe('css splitter', () => { + it('finds css textElement splits correctly', () => { + const window = new Window({ url: 'https://localhost:8080' }); + const document = window.document; + document.head.innerHTML = ''; + const style = document.querySelector('style'); + if (style) { + // as authored, e.g. no spaces + style.append('.a{background-color:black;}'); + + // how it is currently stringified (spaces present) + const expected = [ + '.a { background-color: red; }', + '.a { background-color: black; }', + ]; + const browserSheet = expected.join(''); + expect(stringifyStylesheet(style.sheet!)).toEqual(browserSheet); + + expect(splitCssText(browserSheet, style)).toEqual(expected); + } }); - it('should list the parsing errors and continue parsing', () => { - const result = parse( - 'foo { color= red; } bar { color: blue; } baz {}} boo { display: none}', - { - silent: true, - source: 'foo.css', - }, - ); - - const rules = result.stylesheet!.rules; - expect(rules.length).toBeGreaterThan(2); - - const errors = result.stylesheet!.parsingErrors!; - expect(errors.length).toEqual(2); - - expect(errors[0]).toHaveProperty('message'); - expect(errors[0]).toHaveProperty('reason'); - expect(errors[0]).toHaveProperty('filename'); - expect(errors[0]).toHaveProperty('line'); - expect(errors[0]).toHaveProperty('column'); - expect(errors[0]).toHaveProperty('source'); - expect(errors[0].filename).toEqual('foo.css'); + it('finds css textElement splits correctly when comments are present', () => { + const window = new Window({ url: 'https://localhost:8080' }); + const document = window.document; + // as authored, with comment, missing semicolons + document.head.innerHTML = + ''; + const style = document.querySelector('style'); + if (style) { + style.append('/* author comment */.a{color:red}.b{color:green}'); + + // how it is currently stringified (spaces present) + const expected = [ + '.a { color: red; } .b { color: blue; }', + '.a { color: red; } .b { color: green; }', + ]; + const browserSheet = expected.join(''); + expect(splitCssText(browserSheet, style)).toEqual(expected); + } }); - it('should parse selector with comma nested inside ()', () => { - const result = parse( - '[_nghost-ng-c4172599085]:not(.fit-content).aim-select:hover:not(:disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--disabled, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--invalid, [_nghost-ng-c4172599085]:not(.fit-content).aim-select--active) { border-color: rgb(84, 84, 84); }', - ); - - expect(result.parent).toEqual(null); - - const rules = result.stylesheet!.rules; - expect(rules.length).toEqual(1); - - let rule = rules[0] as Rule; - expect(rule.parent).toEqual(result); - expect(rule.selectors?.length).toEqual(1); - - let decl = rule.declarations![0]; - expect(decl.parent).toEqual(rule); + it('finds css textElement splits correctly when vendor prefixed rules have been removed', () => { + const style = JSDOM.fragment(``).querySelector('style'); + if (style) { + // as authored, with newlines + style.appendChild( + JSDOM.fragment(`.x { + -webkit-transition: all 4s ease; + content: 'try to keep a newline'; + transition: all 4s ease; +}`), + ); + // TODO: splitCssText can't handle it yet if both start with .x + style.appendChild( + JSDOM.fragment(`.y { + -moz-transition: all 5s ease; + transition: all 5s ease; +}`), + ); + // browser .rules would usually omit the vendored versions and modifies the transition value + const expected = [ + '.x { content: "try to keep a newline"; background: red; transition: 4s; }', + '.y { transition: 5s; }', + ]; + const browserSheet = expected.join(''); + + // can't do this as JSDOM doesn't have style.sheet + // also happy-dom doesn't strip out vendor-prefixed rules like a real browser does + //expect(stringifyStylesheet(style.sheet!)).toEqual(browserSheet); + + expect(splitCssText(browserSheet, style)).toEqual(expected); + } }); +}); - it('parses { and } in attribute selectors correctly', () => { - const result = parse('foo[someAttr~="{someId}"] { color: red; }'); - const rules = result.stylesheet!.rules; - - expect(rules.length).toEqual(1); - - const rule = rules[0] as Rule; - - expect(rule.selectors![0]).toEqual('foo[someAttr~="{someId}"]'); +describe('applyCssSplits css rejoiner', function () { + const mockLastUnusedArg = null as unknown as BuildCache; + const halfCssText = '.a { background-color: red; }'; + const otherHalfCssText = halfCssText.replace('.a', '.x'); + const markedCssText = [halfCssText, otherHalfCssText].join('/* rr_split */'); + let sn: serializedElementNodeWithId; + + beforeEach(() => { + sn = { + type: NodeType.Element, + tagName: 'style', + childNodes: [ + { + type: NodeType.Text, + textContent: '', + }, + { + type: NodeType.Text, + textContent: '', + }, + ], + } as serializedElementNodeWithId; }); - it('should set parent property', () => { - const result = parse( - 'thing { test: value; }\n' + - '@media (min-width: 100px) { thing { test: value; } }', + it('applies css splits correctly', () => { + // happy path + applyCssSplits(sn, markedCssText, false, mockLastUnusedArg); + expect((sn.childNodes[0] as textNode).textContent).toEqual(halfCssText); + expect((sn.childNodes[1] as textNode).textContent).toEqual( + otherHalfCssText, ); - - expect(result.parent).toEqual(null); - - const rules = result.stylesheet!.rules; - expect(rules.length).toEqual(2); - - let rule = rules[0] as Rule; - expect(rule.parent).toEqual(result); - expect(rule.declarations!.length).toEqual(1); - - let decl = rule.declarations![0]; - expect(decl.parent).toEqual(rule); - - const media = rules[1] as Media; - expect(media.parent).toEqual(result); - expect(media.rules!.length).toEqual(1); - - rule = media.rules![0] as Rule; - expect(rule.parent).toEqual(media); - - expect(rule.declarations!.length).toEqual(1); - decl = rule.declarations![0]; - expect(decl.parent).toEqual(rule); - }); - - it('parses : in attribute selectors correctly', () => { - const out1 = fixSafariColons('[data-foo] { color: red; }'); - expect(out1).toEqual('[data-foo] { color: red; }'); - - const out2 = fixSafariColons('[data-foo:other] { color: red; }'); - expect(out2).toEqual('[data-foo\\:other] { color: red; }'); - - const out3 = fixSafariColons('[data-aa\\:other] { color: red; }'); - expect(out3).toEqual('[data-aa\\:other] { color: red; }'); }); - it('parses nested commas in selectors correctly', () => { - const result = parse( - ` -body > ul :is(li:not(:first-of-type) a:hover, li:not(:first-of-type).active a) { - background: red; -} -`, + it('applies css splits correctly even when there are too many child nodes', () => { + let sn3 = { + type: NodeType.Element, + tagName: 'style', + childNodes: [ + { + type: NodeType.Text, + textContent: '', + }, + { + type: NodeType.Text, + textContent: '', + }, + { + type: NodeType.Text, + textContent: '', + }, + ], + } as serializedElementNodeWithId; + applyCssSplits(sn3, markedCssText, false, mockLastUnusedArg); + expect((sn3.childNodes[0] as textNode).textContent).toEqual(halfCssText); + expect((sn3.childNodes[1] as textNode).textContent).toEqual( + otherHalfCssText, ); - expect((result.stylesheet!.rules[0] as Rule)!.selectors!.length).toEqual(1); - - const trickresult = parse( - ` -li[attr="weirdly("] a:hover, li[attr="weirdly)"] a { - background-color: red; -} -`, - ); - expect( - (trickresult.stylesheet!.rules[0] as Rule)!.selectors!.length, - ).toEqual(2); - - const weirderresult = parse( - ` -li[attr="weirder\\"("] a:hover, li[attr="weirder\\")"] a { - background-color: red; -} -`, - ); - expect( - (weirderresult.stylesheet!.rules[0] as Rule)!.selectors!.length, - ).toEqual(2); - - const commainstrresult = parse( - ` -li[attr="has,comma"] a:hover { - background-color: red; -} -`, - ); - expect( - (commainstrresult.stylesheet!.rules[0] as Rule)!.selectors!.length, - ).toEqual(1); + expect((sn3.childNodes[2] as textNode).textContent).toEqual(''); }); - it('parses imports with quotes correctly', () => { - const out1 = escapeImportStatement({ - cssText: `@import url("/foo.css;900;800"");`, - href: '/foo.css;900;800"', - media: { - length: 0, - }, - layerName: null, - supportsText: null, - } as unknown as CSSImportRule); - expect(out1).toEqual(`@import url("/foo.css;900;800\\"");`); - - const out2 = escapeImportStatement({ - cssText: `@import url("/foo.css;900;800"") supports(display: flex);`, - href: '/foo.css;900;800"', - media: { - length: 0, - }, - layerName: null, - supportsText: 'display: flex', - } as unknown as CSSImportRule); - expect(out2).toEqual( - `@import url("/foo.css;900;800\\"") supports(display: flex);`, + it('maintains entire css text when there are too few child nodes', () => { + let sn1 = { + type: NodeType.Element, + tagName: 'style', + childNodes: [ + { + type: NodeType.Text, + textContent: '', + }, + ], + } as serializedElementNodeWithId; + applyCssSplits(sn1, markedCssText, false, mockLastUnusedArg); + expect((sn1.childNodes[0] as textNode).textContent).toEqual( + halfCssText + otherHalfCssText, ); - - const out3 = escapeImportStatement({ - cssText: `@import url("/foo.css;900;800"");`, - href: '/foo.css;900;800"', - media: { - length: 1, - mediaText: 'print, screen', - }, - layerName: null, - supportsText: null, - } as unknown as CSSImportRule); - expect(out3).toEqual(`@import url("/foo.css;900;800\\"") print, screen;`); - - const out4 = escapeImportStatement({ - cssText: `@import url("/foo.css;900;800"") layer(layer-1);`, - href: '/foo.css;900;800"', - media: { - length: 0, - }, - layerName: 'layer-1', - supportsText: null, - } as unknown as CSSImportRule); - expect(out4).toEqual(`@import url("/foo.css;900;800\\"") layer(layer-1);`); - - const out5 = escapeImportStatement({ - cssText: `@import url("/foo.css;900;800"") layer;`, - href: '/foo.css;900;800"', - media: { - length: 0, - }, - layerName: '', - supportsText: null, - } as unknown as CSSImportRule); - expect(out5).toEqual(`@import url("/foo.css;900;800\\"") layer;`); }); }); diff --git a/packages/rrweb-snapshot/test/css/style-with-import.css b/packages/rrweb-snapshot/test/css/style-with-import.css index 5fa59d8039..a24d901947 100644 --- a/packages/rrweb-snapshot/test/css/style-with-import.css +++ b/packages/rrweb-snapshot/test/css/style-with-import.css @@ -1,2 +1,3 @@ @import '//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&family=Roboto:wght@100;300;400;500;700&display=swap"'; @import './style.css'; +@import '../alt-css/alt-style.css'; diff --git a/packages/rrweb-snapshot/test/css/style.css b/packages/rrweb-snapshot/test/css/style.css index 2b3faf2a77..29b1da8ec8 100644 --- a/packages/rrweb-snapshot/test/css/style.css +++ b/packages/rrweb-snapshot/test/css/style.css @@ -1,11 +1,11 @@ body { margin: 0; - background: url('../a.jpg'); + background: url('../should-be-in-root-folder.jpg'); border-image: url('data:image/svg+xml;utf8,'); } p { color: red; - background: url('./b.jpg'); + background: url('./should-be-in-css-folder.jpg'); } body > p { color: yellow; diff --git a/packages/rrweb-snapshot/test/html/dialog.html b/packages/rrweb-snapshot/test/html/dialog.html new file mode 100644 index 0000000000..2380b8fade --- /dev/null +++ b/packages/rrweb-snapshot/test/html/dialog.html @@ -0,0 +1,5 @@ + + + I'm a dialog + + diff --git a/packages/rrweb-snapshot/test/html/monkey-patched-elements.html b/packages/rrweb-snapshot/test/html/monkey-patched-elements.html new file mode 100644 index 0000000000..a48b8fd328 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/monkey-patched-elements.html @@ -0,0 +1,45 @@ + + + + + + Document + + + +
    +
  • a
  • +
  • b
  • +
  • c
  • +
  • d
  • +
+ + diff --git a/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html index 6b45f65bc5..d98ff7b9fa 100644 --- a/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html +++ b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html @@ -7,6 +7,10 @@ with style sheet with import + diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index 8f4476ecb9..9fa04baf65 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -1,10 +1,20 @@ import * as fs from 'fs'; -import * as path from 'path'; import * as http from 'http'; -import * as url from 'url'; +import * as path from 'path'; import * as puppeteer from 'puppeteer'; -import { vi, assert, describe, it, beforeAll, afterAll, expect } from 'vitest'; -import { waitForRAF, getServerURL } from './utils'; +import * as url from 'url'; +import { + afterAll, + assert, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; + +import { getServerURL, waitForRAF } from './utils'; const htmlFolder = path.join(__dirname, 'html'); const htmls = fs.readdirSync(htmlFolder).map((filePath) => { @@ -60,6 +70,15 @@ function sanitizeSnapshot(snapshot: string): string { return snapshot.replace(/localhost:[0-9]+/g, 'localhost:3030'); } +async function snapshot(page: puppeteer.Page, code: string): Promise { + await waitForRAF(page); + const result = (await page.evaluate(`${code} + const snapshot = rrwebSnapshot.snapshot(document); + JSON.stringify(snapshot, null, 2); + `)) as string; + return result; +} + function assertSnapshot(snapshot: string): void { expect(sanitizeSnapshot(snapshot)).toMatchSnapshot(); } @@ -68,6 +87,7 @@ interface ISuite { server: http.Server; serverURL: string; browser: puppeteer.Browser; + page: puppeteer.Page; code: string; } @@ -100,6 +120,9 @@ describe('integration tests', function (this: ISuite) { if (html.filePath.substring(html.filePath.length - 1) === '~') { continue; } + // monkey patching breaks rebuild code + if (html.filePath.includes('monkey-patched-elements.html')) continue; + const title = '[html file]: ' + html.filePath; it(title, async () => { const page: puppeteer.Page = await browser.newPage(); @@ -153,7 +176,7 @@ describe('integration tests', function (this: ISuite) { 'blob:http://localhost:xxxx/...', ); - assertSnapshot(rebuildHtml); + await assertSnapshot(rebuildHtml); }); } @@ -235,7 +258,6 @@ iframe.contentDocument.querySelector('center').clientHeight it('correctly saves cross-origin images offline', async () => { const page: puppeteer.Page = await browser.newPage(); - await page.goto('about:blank', { waitUntil: 'load', }); @@ -348,7 +370,7 @@ iframe.contentDocument.querySelector('center').clientHeight it('should save background-clip: text; as the more compatible -webkit-background-clip: test;', async () => { const page: puppeteer.Page = await browser.newPage(); - await page.goto(`http://localhost:3030/html/background-clip-text.html`, { + await page.goto(`${serverURL}/html/background-clip-text.html`, { waitUntil: 'load', }); await waitForRAF(page); // wait for page to render @@ -366,13 +388,10 @@ iframe.contentDocument.querySelector('center').clientHeight it('images with inline onload should work', async () => { const page: puppeteer.Page = await browser.newPage(); - await page.goto( - 'http://localhost:3030/html/picture-with-inline-onload.html', - { - waitUntil: 'load', - }, - ); - await page.waitForSelector('img', { timeout: 1000 }); + await page.goto(`${serverURL}/html/picture-with-inline-onload.html`, { + waitUntil: 'load', + }); + await page.waitForSelector('img', { timeout: 2000 }); await page.evaluate(`${code}`); await page.evaluate(` var snapshot = rrwebSnapshot.snapshot(document, { @@ -386,6 +405,22 @@ iframe.contentDocument.querySelector('center').clientHeight )) as string; assert(fnName === 'onload'); }); + + it('should be able to record elements even when .childNodes has been monkey patched', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto(`${serverURL}/html/monkey-patched-elements.html`, { + waitUntil: 'load', + }); + await waitForRAF(page); // wait for page to render + const snapshotResult = JSON.stringify( + await page.evaluate(`${code}; + rrwebSnapshot.snapshot(document); + `), + null, + 2, + ); + expect(snapshotResult).toMatchSnapshot(); + }); }); describe('iframe integration tests', function (this: ISuite) { @@ -427,6 +462,53 @@ describe('iframe integration tests', function (this: ISuite) { null, 2, ); + await assertSnapshot(snapshotResult); + }); +}); + +describe('dialog integration tests', function (this: ISuite) { + vi.setConfig({ testTimeout: 30_000 }); + let server: ISuite['server']; + let serverURL: ISuite['serverURL']; + let browser: ISuite['browser']; + let code: ISuite['code']; + let page: ISuite['page']; + + beforeAll(async () => { + server = await startServer(); + serverURL = getServerURL(server); + browser = await puppeteer.launch({ + // headless: false, + }); + + code = fs.readFileSync( + path.resolve(__dirname, '../dist/rrweb-snapshot.umd.cjs'), + 'utf-8', + ); + }); + + beforeEach(async () => { + page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html/dialog.html`, { + waitUntil: 'load', + }); + }); + + afterAll(async () => { + await browser.close(); + await server.close(); + }); + + it('should capture open attribute for non modal dialogs', async () => { + page.evaluate('document.querySelector("dialog").show()'); + const snapshotResult = await snapshot(page, code); + assertSnapshot(snapshotResult); + }); + + it('should capture open attribute for modal dialogs', async () => { + await page.evaluate('document.querySelector("dialog").showModal()'); + const snapshotResult = await snapshot(page, code); assertSnapshot(snapshotResult); }); }); @@ -470,6 +552,6 @@ describe('shadow DOM integration tests', function (this: ISuite) { null, 2, ); - assertSnapshot(snapshotResult); + await assertSnapshot(snapshotResult); }); }); diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index c71e1e8510..a0994a2f88 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -3,14 +3,34 @@ */ import * as fs from 'fs'; import * as path from 'path'; -import { describe, it, beforeEach, expect } from 'vitest'; +import { beforeEach, describe, expect as _expect, it } from 'vitest'; import { adaptCssForReplay, buildNodeWithSN, createCache, } from '../src/rebuild'; import { NodeType } from '../src/types'; -import { createMirror, Mirror } from '../src/utils'; +import { createMirror, Mirror, normalizeCssString } from '../src/utils'; + +const expect = _expect as unknown as { + (actual: T): { + toMatchCss(expected: string): void; + } & ReturnType; +} & typeof _expect; + +expect.extend({ + toMatchCss: function (received: string, expected: string) { + const pass = normalizeCssString(received) === normalizeCssString(expected); + const message: () => string = () => + pass + ? '' + : `Received (${received}) is not the same as expected (${expected})`; + return { + message, + pass, + }; + }, +}); function getDuration(hrtime: [number, number]) { const [seconds, nanoseconds] = hrtime; @@ -52,6 +72,32 @@ describe('rebuild', function () { }); }); + describe('rr_width/rr_height', function () { + it('rebuild blocked element with correct dimensions', function () { + const node = buildNodeWithSN( + { + id: 1, + tagName: 'svg', + type: NodeType.Element, + isSVG: true, + attributes: { + rr_width: '50px', + rr_height: '50px', + }, + childNodes: [], + }, + { + doc: document, + mirror, + hackCss: false, + cache, + }, + ) as HTMLDivElement; + expect(node.style.width).toBe('50px'); + expect(node.style.height).toBe('50px'); + }); + }); + describe('shadowDom', function () { it('rebuild shadowRoot without siblings', function () { const node = buildNodeWithSN( @@ -86,19 +132,19 @@ describe('rebuild', function () { describe('add hover class to hover selector related rules', function () { it('will do nothing to css text without :hover', () => { const cssText = 'body { color: white }'; - expect(adaptCssForReplay(cssText, cache)).toEqual(cssText); + expect(adaptCssForReplay(cssText, cache)).toMatchCss(cssText); }); it('can add hover class to css text', () => { const cssText = '.a:hover { color: white }'; - expect(adaptCssForReplay(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toMatchCss( '.a:hover, .a.\\:hover { color: white }', ); }); it('can correctly add hover when in middle of selector', () => { const cssText = 'ul li a:hover img { color: white }'; - expect(adaptCssForReplay(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toMatchCss( 'ul li a:hover img, ul li a.\\:hover img { color: white }', ); }); @@ -111,14 +157,15 @@ img, ul li.specified c:hover img { color: white }`; - expect(adaptCssForReplay(cssText, cache)).toEqual( - `ul li.specified a:hover img, ul li.specified a.\\:hover img, + expect(adaptCssForReplay(cssText, cache)).toMatchCss( + `ul li.specified a:hover img, ul li.multiline b:hover -img, ul li.multiline -b.\\:hover img, -ul li.specified c:hover img, ul li.specified c.\\:hover img { +ul li.specified c:hover img, +ul li.specified a.\\:hover img, +ul li.multiline b.\\:hover img, +ul li.specified c.\\:hover img { color: white }`, ); @@ -126,48 +173,48 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img { it('can add hover class within media query', () => { const cssText = '@media screen { .m:hover { color: white } }'; - expect(adaptCssForReplay(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toMatchCss( '@media screen { .m:hover, .m.\\:hover { color: white } }', ); }); it('can add hover class when there is multi selector', () => { const cssText = '.a, .b:hover, .c { color: white }'; - expect(adaptCssForReplay(cssText, cache)).toEqual( - '.a, .b:hover, .b.\\:hover, .c { color: white }', + expect(adaptCssForReplay(cssText, cache)).toMatchCss( + '.a, .b:hover, .c, .b.\\:hover { color: white }', ); }); it('can add hover class when there is a multi selector with the same prefix', () => { const cssText = '.a:hover, .a:hover::after { color: white }'; - expect(adaptCssForReplay(cssText, cache)).toEqual( - '.a:hover, .a.\\:hover, .a:hover::after, .a.\\:hover::after { color: white }', + expect(adaptCssForReplay(cssText, cache)).toMatchCss( + '.a:hover, .a:hover::after, .a.\\:hover, .a.\\:hover::after { color: white }', ); }); it('can add hover class when :hover is not the end of selector', () => { const cssText = 'div:hover::after { color: white }'; - expect(adaptCssForReplay(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toMatchCss( 'div:hover::after, div.\\:hover::after { color: white }', ); }); it('can add hover class when the selector has multi :hover', () => { const cssText = 'a:hover b:hover { color: white }'; - expect(adaptCssForReplay(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toMatchCss( 'a:hover b:hover, a.\\:hover b.\\:hover { color: white }', ); }); it('will ignore :hover in css value', () => { const cssText = '.a::after { content: ":hover" }'; - expect(adaptCssForReplay(cssText, cache)).toEqual(cssText); + expect(adaptCssForReplay(cssText, cache)).toMatchCss(cssText); }); it('can adapt media rules to replay context', () => { const cssText = '@media only screen and (min-device-width : 1200px) { .a { width: 10px; }}'; - expect(adaptCssForReplay(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toMatchCss( '@media only screen and (min-width : 1200px) { .a { width: 10px; }}', ); }); @@ -210,7 +257,7 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img { // previously that part was being incorrectly consumed by the selector regex const should_not_modify = ".tailwind :is(.before\\:content-\\[\\'\\'\\])::before { --tw-content: \":hover\"; content: var(--tw-content); }.tailwind :is(.\\[\\&\\>li\\]\\:before\\:content-\\[\\'-\\'\\] > li)::before { color: pink; }"; - expect(adaptCssForReplay(should_not_modify, cache)).toEqual( + expect(adaptCssForReplay(should_not_modify, cache)).toMatchCss( should_not_modify, ); }); @@ -219,7 +266,7 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img { // the ':hover' in the below is a decoy which is not part of the selector, const should_not_modify = '@import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,700;1,400&display=:hover");'; - expect(adaptCssForReplay(should_not_modify, cache)).toEqual( + expect(adaptCssForReplay(should_not_modify, cache)).toMatchCss( should_not_modify, ); }); diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index de1d79eb6d..5778eb0aff 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -2,64 +2,79 @@ * @vitest-environment jsdom */ import { JSDOM } from 'jsdom'; -import { describe, it, expect } from 'vitest'; -import { - absoluteToStylesheet, - serializeNodeWithId, +import { describe, expect, it } from 'vitest'; + +import snapshot, { _isBlockedElement, + serializeNodeWithId, } from '../src/snapshot'; -import snapshot from '../src/snapshot'; -import { serializedNodeWithId, elementNode } from '../src/types'; -import { Mirror } from '../src/utils'; +import { elementNode, serializedNodeWithId } from '../src/types'; +import { Mirror, absolutifyURLs } from '../src/utils'; + +const serializeNode = (node: Node): serializedNodeWithId | null => { + return serializeNodeWithId(node, { + doc: document, + mirror: new Mirror(), + blockClass: 'blockblock', + blockSelector: null, + maskTextClass: 'maskmask', + maskTextSelector: null, + skipChild: false, + inlineStylesheet: true, + maskTextFn: undefined, + maskInputFn: undefined, + slimDOMOptions: {}, + }); +}; describe('absolute url to stylesheet', () => { const href = 'http://localhost/css/style.css'; it('can handle relative path', () => { - expect(absoluteToStylesheet('url(a.jpg)', href)).toEqual( + expect(absolutifyURLs('url(a.jpg)', href)).toEqual( `url(http://localhost/css/a.jpg)`, ); }); it('can handle same level path', () => { - expect(absoluteToStylesheet('url("./a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("./a.jpg")', href)).toEqual( `url("http://localhost/css/a.jpg")`, ); }); it('can handle parent level path', () => { - expect(absoluteToStylesheet('url("../a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("../a.jpg")', href)).toEqual( `url("http://localhost/a.jpg")`, ); }); it('can handle absolute path', () => { - expect(absoluteToStylesheet('url("/a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("/a.jpg")', href)).toEqual( `url("http://localhost/a.jpg")`, ); }); it('can handle external path', () => { - expect(absoluteToStylesheet('url("http://localhost/a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("http://localhost/a.jpg")', href)).toEqual( `url("http://localhost/a.jpg")`, ); }); it('can handle single quote path', () => { - expect(absoluteToStylesheet(`url('./a.jpg')`, href)).toEqual( + expect(absolutifyURLs(`url('./a.jpg')`, href)).toEqual( `url('http://localhost/css/a.jpg')`, ); }); it('can handle no quote path', () => { - expect(absoluteToStylesheet('url(./a.jpg)', href)).toEqual( + expect(absolutifyURLs('url(./a.jpg)', href)).toEqual( `url(http://localhost/css/a.jpg)`, ); }); it('can handle multiple no quote paths', () => { expect( - absoluteToStylesheet( + absolutifyURLs( 'background-image: url(images/b.jpg);background: #aabbcc url(images/a.jpg) 50% 50% repeat;', href, ), @@ -70,11 +85,11 @@ describe('absolute url to stylesheet', () => { }); it('can handle data url image', () => { + expect(absolutifyURLs('url(data:image/gif;base64,ABC)', href)).toEqual( + 'url(data:image/gif;base64,ABC)', + ); expect( - absoluteToStylesheet('url(data:image/gif;base64,ABC)', href), - ).toEqual('url(data:image/gif;base64,ABC)'); - expect( - absoluteToStylesheet( + absolutifyURLs( 'url(data:application/font-woff;base64,d09GMgABAAAAAAm)', href, ), @@ -83,7 +98,7 @@ describe('absolute url to stylesheet', () => { it('preserves quotes around inline svgs with spaces', () => { expect( - absoluteToStylesheet( + absolutifyURLs( "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M3'/%3E%3C/svg%3E\")", href, ), @@ -91,7 +106,7 @@ describe('absolute url to stylesheet', () => { "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M3'/%3E%3C/svg%3E\")", ); expect( - absoluteToStylesheet( + absolutifyURLs( 'url(\'data:image/svg+xml;utf8,\')', href, ), @@ -99,7 +114,7 @@ describe('absolute url to stylesheet', () => { 'url(\'data:image/svg+xml;utf8,\')', ); expect( - absoluteToStylesheet( + absolutifyURLs( 'url("data:image/svg+xml;utf8,")', href, ), @@ -108,7 +123,7 @@ describe('absolute url to stylesheet', () => { ); }); it('can handle empty path', () => { - expect(absoluteToStylesheet(`url('')`, href)).toEqual(`url('')`); + expect(absolutifyURLs(`url('')`, href)).toEqual(`url('')`); }); }); @@ -139,22 +154,6 @@ describe('isBlockedElement()', () => { }); describe('style elements', () => { - const serializeNode = (node: Node): serializedNodeWithId | null => { - return serializeNodeWithId(node, { - doc: document, - mirror: new Mirror(), - blockClass: 'blockblock', - blockSelector: null, - maskTextClass: 'maskmask', - maskTextSelector: null, - skipChild: false, - inlineStylesheet: true, - maskTextFn: undefined, - maskInputFn: undefined, - slimDOMOptions: {}, - }); - }; - const render = (html: string): HTMLStyleElement => { document.write(html); return document.querySelector('style')!; @@ -163,44 +162,32 @@ describe('style elements', () => { it('should serialize all rules of stylesheet when the sheet has a single child node', () => { const styleEl = render(``); styleEl.sheet?.insertRule('section { color: blue; }'); - expect(serializeNode(styleEl.childNodes[0])).toMatchObject({ - isStyle: true, + expect(serializeNode(styleEl)).toMatchObject({ rootId: undefined, - textContent: 'section {color: blue;}body {color: red;}', - type: 3, + attributes: { + _cssText: 'section {color: blue;}body {color: red;}', + }, + type: 2, }); }); - it('should serialize individual text nodes on stylesheets with multiple child nodes', () => { + it('should serialize all rules on stylesheets with mix of insertion type', () => { const styleEl = render(``); + styleEl.sheet?.insertRule('section.lost { color: unseeable; }'); // browser throws this away after append styleEl.append(document.createTextNode('section { color: blue; }')); - expect(serializeNode(styleEl.childNodes[1])).toMatchObject({ - isStyle: true, + styleEl.sheet?.insertRule('section.working { color: pink; }'); + expect(serializeNode(styleEl)).toMatchObject({ rootId: undefined, - textContent: 'section { color: blue; }', - type: 3, + attributes: { + _cssText: + 'section.working {color: pink;}body {color: red;}/* rr_split */section {color: blue;}', + }, + type: 2, }); }); }); describe('scrollTop/scrollLeft', () => { - const serializeNode = (node: Node): serializedNodeWithId | null => { - return serializeNodeWithId(node, { - doc: document, - mirror: new Mirror(), - blockClass: 'blockblock', - blockSelector: null, - maskTextClass: 'maskmask', - maskTextSelector: null, - skipChild: false, - inlineStylesheet: true, - maskTextFn: undefined, - maskInputFn: undefined, - slimDOMOptions: {}, - newlyAddedElement: false, - }); - }; - const render = (html: string): HTMLDivElement => { document.write(html); return document.querySelector('div')!; @@ -222,23 +209,6 @@ describe('scrollTop/scrollLeft', () => { }); describe('form', () => { - const serializeNode = (node: Node): serializedNodeWithId | null => { - return serializeNodeWithId(node, { - doc: document, - mirror: new Mirror(), - blockClass: 'blockblock', - blockSelector: null, - maskTextClass: 'maskmask', - maskTextSelector: null, - skipChild: false, - inlineStylesheet: true, - maskTextFn: undefined, - maskInputFn: undefined, - slimDOMOptions: {}, - newlyAddedElement: false, - }); - }; - const render = (html: string): HTMLTextAreaElement => { document.write(html); return document.querySelector('textarea')!; diff --git a/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts b/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts new file mode 100644 index 0000000000..1e42bab1a6 --- /dev/null +++ b/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts @@ -0,0 +1,37 @@ +/** + * @vitest-environment jsdom + */ +import { bench } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { stringifyStylesheet } from '../src/utils'; +import * as CSSOM from 'cssom'; + +describe('stringifyStylesheet', () => { + let benchmarkStylesheet: CSSStyleSheet; + + const cssText = fs.readFileSync( + path.resolve(__dirname, './css/benchmark.css'), + 'utf8', + ); + benchmarkStylesheet = CSSOM.parse(cssText); + benchmarkStylesheet.href = 'https://example.com/style.css'; + + it.skip('stringify', () => { + // written just to ensure it's working + const cssText = '.x { background: url(./relative.jpg) }'; + const styleSheet = CSSOM.parse(cssText); + styleSheet.href = 'https://example.com/style.css'; + expect(stringifyStylesheet(styleSheet)).toEqual( + 'x {background: url(https://example.com/relative.jpg);}', + ); + }); + + bench( + 'stringify', + () => { + stringifyStylesheet(benchmarkStylesheet); + }, + { time: 1000 }, + ); +}); diff --git a/packages/rrweb-snapshot/test/utils.test.ts b/packages/rrweb-snapshot/test/utils.test.ts index 2b09602ae4..885f4ec385 100644 --- a/packages/rrweb-snapshot/test/utils.test.ts +++ b/packages/rrweb-snapshot/test/utils.test.ts @@ -3,7 +3,12 @@ */ import { describe, it, test, expect } from 'vitest'; import { NodeType, serializedNode } from '../src/types'; -import { extractFileExtension, isNodeMetaEqual } from '../src/utils'; +import { + escapeImportStatement, + extractFileExtension, + fixSafariColons, + isNodeMetaEqual, +} from '../src/utils'; import type { serializedNodeWithId } from '@saola.ai/rrweb-snapshot'; describe('utils', () => { @@ -199,4 +204,80 @@ describe('utils', () => { expect(extension).toBe('js'); }); }); + + describe('escapeImportStatement', () => { + it('parses imports with quotes correctly', () => { + const out1 = escapeImportStatement({ + cssText: `@import url("/foo.css;900;800"");`, + href: '/foo.css;900;800"', + media: { + length: 0, + }, + layerName: null, + supportsText: null, + } as unknown as CSSImportRule); + expect(out1).toEqual(`@import url("/foo.css;900;800\\"");`); + + const out2 = escapeImportStatement({ + cssText: `@import url("/foo.css;900;800"") supports(display: flex);`, + href: '/foo.css;900;800"', + media: { + length: 0, + }, + layerName: null, + supportsText: 'display: flex', + } as unknown as CSSImportRule); + expect(out2).toEqual( + `@import url("/foo.css;900;800\\"") supports(display: flex);`, + ); + + const out3 = escapeImportStatement({ + cssText: `@import url("/foo.css;900;800"");`, + href: '/foo.css;900;800"', + media: { + length: 1, + mediaText: 'print, screen', + }, + layerName: null, + supportsText: null, + } as unknown as CSSImportRule); + expect(out3).toEqual(`@import url("/foo.css;900;800\\"") print, screen;`); + + const out4 = escapeImportStatement({ + cssText: `@import url("/foo.css;900;800"") layer(layer-1);`, + href: '/foo.css;900;800"', + media: { + length: 0, + }, + layerName: 'layer-1', + supportsText: null, + } as unknown as CSSImportRule); + expect(out4).toEqual( + `@import url("/foo.css;900;800\\"") layer(layer-1);`, + ); + + const out5 = escapeImportStatement({ + cssText: `@import url("/foo.css;900;800"") layer;`, + href: '/foo.css;900;800"', + media: { + length: 0, + }, + layerName: '', + supportsText: null, + } as unknown as CSSImportRule); + expect(out5).toEqual(`@import url("/foo.css;900;800\\"") layer;`); + }); + }); + describe('fixSafariColons', () => { + it('parses : in attribute selectors correctly', () => { + const out1 = fixSafariColons('[data-foo] { color: red; }'); + expect(out1).toEqual('[data-foo] { color: red; }'); + + const out2 = fixSafariColons('[data-foo:other] { color: red; }'); + expect(out2).toEqual('[data-foo\\:other] { color: red; }'); + + const out3 = fixSafariColons('[data-aa\\:other] { color: red; }'); + expect(out3).toEqual('[data-aa\\:other] { color: red; }'); + }); + }); }); diff --git a/packages/rrweb-snapshot/tsconfig.json b/packages/rrweb-snapshot/tsconfig.json index 82d5cc086b..cd2eb36538 100644 --- a/packages/rrweb-snapshot/tsconfig.json +++ b/packages/rrweb-snapshot/tsconfig.json @@ -1,7 +1,13 @@ { "extends": "../../tsconfig.base.json", - "include": ["src"], - "exclude": ["vite.config.ts", "vitest.config.ts", "test"], + "include": [ + "src" + ], + "exclude": [ + "vite.config.ts", + "vitest.config.ts", + "test" + ], "compilerOptions": { "rootDir": "src", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" diff --git a/packages/rrweb-snapshot/vitest.config.ts b/packages/rrweb-snapshot/vitest.config.ts index 39888437cf..1b5a8b7e3e 100644 --- a/packages/rrweb-snapshot/vitest.config.ts +++ b/packages/rrweb-snapshot/vitest.config.ts @@ -6,7 +6,7 @@ export default mergeConfig( configShared, defineProject({ test: { - // ... custom test config here + globals: true, }, }), ); diff --git a/packages/rrweb/CHANGELOG.md b/packages/rrweb/CHANGELOG.md index 8686ccbba0..69641c0e3d 100644 --- a/packages/rrweb/CHANGELOG.md +++ b/packages/rrweb/CHANGELOG.md @@ -1,5 +1,52 @@ # rrweb +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb-snapshot@2.0.13 + - @saola.ai/rrdom@2.0.13 + - @saola.ai/rrweb-types@2.0.13 + - @saola.ai/rrweb-utils@2.0.13 + +## 2.0.0-alpha.17 + +### Minor Changes + +- [#1503](https://github.com/rrweb-io/rrweb/pull/1503) [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158) Thanks [@Juice10](https://github.com/Juice10)! - Support top-layer components. Fixes #1381. + +### Patch Changes + +- [#1417](https://github.com/rrweb-io/rrweb/pull/1417) [`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - fix: duplicate textContent for style elements cause incremental style mutations to be invalid + +- [#1527](https://github.com/rrweb-io/rrweb/pull/1527) [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c) Thanks [@arredgroup](https://github.com/arredgroup)! - Export takeFullSnapshot function for a recording process + +- [#1515](https://github.com/rrweb-io/rrweb/pull/1515) [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6) Thanks [@okejminja](https://github.com/okejminja)! - Added support for deprecated addRule & removeRule methods + +- [#1509](https://github.com/rrweb-io/rrweb/pull/1509) [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414) Thanks [@Juice10](https://github.com/Juice10)! - Reverse monkey patch built in methods to support LWC (and other frameworks like angular which monkey patch built in methods). + +- 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 + - @rrweb/types@2.0.0-alpha.17 + - @rrweb/utils@2.0.0-alpha.17 + +## 2.0.0-alpha.16 + +### Patch Changes + +- [#1386](https://github.com/rrweb-io/rrweb/pull/1386) [`a2c8a1a`](https://github.com/rrweb-io/rrweb/commit/a2c8a1a37bfcf8389b280af792262c8263a979a3) Thanks [@ababik](https://github.com/ababik)! - Fix that the optional `maskInputFn` was being accidentally ignored during the creation of the full snapshot + +- [#1512](https://github.com/rrweb-io/rrweb/pull/1512) [`d08624c`](https://github.com/rrweb-io/rrweb/commit/d08624cb28add386c3618a0e6607424c3f1884d8) Thanks [@eoghanmurray](https://github.com/eoghanmurray)! - optimisation: skip mask check on leaf elements + +- 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 + - @rrweb/types@2.0.0-alpha.16 + ## 2.0.12 ### Patch Changes diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 24e790a36d..3345ec6f26 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -1,24 +1,25 @@ { "name": "@saola.ai/rrweb", - "version": "2.0.12", + "version": "2.0.13", "description": "record and replay the web", "scripts": { "prepare": "npm run prepack", "prepack": "npm run build", - "retest": "vitest run --exclude test/benchmark", + "retest": "cross-env PUPPETEER_HEADLESS=true yarn retest:headful", + "retest:headful": "vitest run --exclude test/benchmark", "build-and-test": "yarn build && yarn retest", - "test:headless": "PUPPETEER_HEADLESS=true yarn build-and-test", - "test:headful": "PUPPETEER_HEADLESS=false yarn build-and-test", + "test:headless": "cross-env PUPPETEER_HEADLESS=true yarn build-and-test", + "test:headful": "cross-env PUPPETEER_HEADLESS=false yarn build-and-test", "test": "yarn test:headless", - "test:watch": "yarn build && PUPPETEER_HEADLESS=true yarn vitest --exclude test/benchmark", + "test:watch": "yarn build && cross-env PUPPETEER_HEADLESS=true yarn vitest --exclude test/benchmark", "test:update": "yarn test:headless --update", - "retest:update": "PUPPETEER_HEADLESS=true yarn retest --update", + "retest:update": "cross-env PUPPETEER_HEADLESS=true yarn retest --update", "repl": "yarn build && node scripts/repl.js", "live-stream": "yarn build && node scripts/stream.js", "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "check-types": "tsc -noEmit", - "prepublish": "npm run build", + "prepublish": "tsc -noEmit && vite build", "lint": "yarn eslint src", "benchmark": "vitest run --maxConcurrency 1 --no-file-parallelism test/benchmark" }, @@ -68,7 +69,6 @@ "@types/node": "^18.15.11", "@types/offscreencanvas": "^2019.6.4", "construct-style-sheets-polyfill": "^3.1.0", - "cross-env": "^5.2.0", "fast-mhtml": "^1.1.9", "identity-obj-proxy": "^3.0.0", "ignore-styles": "^5.0.1", @@ -78,17 +78,18 @@ "simple-peer-light": "^9.10.0", "ts-node": "^10.9.1", "tslib": "^2.3.1", - "typescript": "^4.7.3", - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1" + "typescript": "^5.4.5", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1" }, "dependencies": { - "@saola.ai/rrweb-types": "^2.0.12", + "@saola.ai/rrweb-types": "^2.0.13", + "@saola.ai/rrweb-utils": "^2.0.13", "@types/css-font-loading-module": "0.0.7", "@xstate/fsm": "^1.4.0", "base64-arraybuffer": "^1.0.1", "mitt": "^3.0.0", - "@saola.ai/rrdom": "^2.0.12", - "@saola.ai/rrweb-snapshot": "^2.0.12" + "@saola.ai/rrdom": "^2.0.13", + "@saola.ai/rrweb-snapshot": "^2.0.13" } } diff --git a/packages/rrweb/src/index.ts b/packages/rrweb/src/index.ts index e013aab4e7..cbb8c566d9 100644 --- a/packages/rrweb/src/index.ts +++ b/packages/rrweb/src/index.ts @@ -17,19 +17,24 @@ export { type eventWithTime, } from '@saola.ai/rrweb-types'; +// exports style.css from replay +import './replay/styles/style.css'; + export type { recordOptions, ReplayPlugin } from './types'; const { addCustomEvent } = record; const { freezePage } = record; +const { takeFullSnapshot } = record; export { record, addCustomEvent, freezePage, + takeFullSnapshot, Replayer, - playerConfig, - PlayerMachineState, - SpeedMachineState, + type playerConfig, + type PlayerMachineState, + type SpeedMachineState, canvasMutation, _mirror as mirror, utils, diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 7320a3c61e..a5bdd977b8 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -1,7 +1,7 @@ import { snapshot, - MaskInputOptions, - SlimDOMOptions, + type MaskInputOptions, + type SlimDOMOptions, createMirror, } from '@saola.ai/rrweb-snapshot'; import { initObservers, mutationBuffers } from './observer'; @@ -19,14 +19,14 @@ import { import type { recordOptions } from '../types'; import { EventType, - eventWithoutTime, - eventWithTime, + type eventWithoutTime, + type eventWithTime, IncrementalSource, - listenerHandler, - mutationCallbackParam, - scrollCallback, - canvasMutationParam, - adoptedStyleSheetParam, + type listenerHandler, + type mutationCallbackParam, + type scrollCallback, + type canvasMutationParam, + type adoptedStyleSheetParam, } from '@saola.ai/rrweb-types'; import type { CrossOriginIframeMessageEventContent } from '../types'; import { IframeManager } from './iframe-manager'; @@ -39,6 +39,7 @@ import { registerErrorHandler, unregisterErrorHandler, } from './error-handler'; +import dom from '@rrweb/utils'; let wrappedEmit!: (e: eventWithoutTime, isCheckout?: boolean) => void; @@ -383,6 +384,7 @@ function record( inlineStylesheet, maskAllInputs: maskInputOptions, maskTextFn, + maskInputFn, slimDOM: slimDOMOptions, dataURLOptions, recordCanvas, @@ -395,7 +397,8 @@ function record( stylesheetManager.trackLinkElement(n as HTMLLinkElement); } if (hasShadowRoot(n)) { - shadowDomManager.addShadowRoot(n.shadowRoot, document); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + shadowDomManager.addShadowRoot(dom.shadowRoot(n as Node)!, document); } }, onIframeLoad: (iframe, childSn) => { diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 23ad493654..53537d03d2 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -32,6 +32,7 @@ import { getShadowHost, closestElementOfNode, } from '../utils'; +import dom from '@rrweb/utils'; type DoubleLinkedListNode = { previous: DoubleLinkedListNode | null; @@ -168,6 +169,7 @@ export default class MutationBuffer { private addedSet = new Set(); private movedSet = new Set(); private droppedSet = new Set(); + private removesSubTreeCache = new Set(); private mutationCb: observerParam['mutationCb']; private blockClass: observerParam['blockClass']; @@ -285,16 +287,27 @@ export default class MutationBuffer { return nextId; }; const pushAdd = (n: Node) => { - if ( - !n.parentNode || - !inDom(n) || - (n.parentNode as Element).tagName === 'TEXTAREA' - ) { + const parent = dom.parentNode(n); + if (!parent || !inDom(n)) { return; } - const parentId = isShadowRoot(n.parentNode) + let cssCaptured = false; + if (n.nodeType === Node.TEXT_NODE) { + const parentTag = (parent as Element).tagName; + if (parentTag === 'TEXTAREA') { + // genTextAreaValueMutation already called via parent + return; + } else if (parentTag === 'STYLE' && this.addedSet.has(parent)) { + // css content will be recorded via parent's _cssText attribute when + // mutation adds entire + + + + + + + + + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 34d81da850..0ae70af289 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -81,7 +81,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('can record form interactions', async () => { @@ -98,10 +98,10 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); - it('can record textarea mutations correctly', async () => { + it('can record and replay textarea mutations correctly', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); await page.setContent(getHtml.call(this, 'empty.html')); @@ -112,20 +112,29 @@ describe('record integration tests', function (this: ISuite) { const ta = document.createElement('textarea'); ta.innerText = 'pre value'; document.body.append(ta); + + const ta2 = document.createElement('textarea'); + ta2.id = 'ta2'; + document.body.append(ta2); }); - await page.waitForTimeout(5); + await waitForRAF(page); await page.evaluate(() => { const t = document.querySelector('textarea') as HTMLTextAreaElement; t.innerText = 'ok'; // this mutation should be recorded + + const ta2t = document.createTextNode('added'); + document.getElementById('ta2').append(ta2t); }); - await page.waitForTimeout(5); + await waitForRAF(page); await page.evaluate(() => { const t = document.querySelector('textarea') as HTMLTextAreaElement; (t.childNodes[0] as Text).appendData('3'); // this mutation is also valid + + document.getElementById('ta2').remove(); // done with this }); - await page.waitForTimeout(5); + await waitForRAF(page); await page.type('textarea', '1'); // types (inserts) at index 0, in front of existing text - await page.waitForTimeout(5); + await waitForRAF(page); await page.evaluate(() => { const t = document.querySelector('textarea') as HTMLTextAreaElement; // user has typed so childNode content should now be ignored @@ -136,13 +145,13 @@ describe('record integration tests', function (this: ISuite) { // there is nothing explicit in rrweb which enforces this, but this test may protect against // a future change where a mutation on a textarea incorrectly updates the .value }); - await page.waitForTimeout(5); + await waitForRAF(page); await page.type('textarea', '2'); // cursor is at index 1 const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); // check after each mutation and text input const replayTextareaValues = await page.evaluate(` @@ -153,12 +162,18 @@ describe('record integration tests', function (this: ISuite) { replayer.pause((e.timestamp - window.snapshots[0].timestamp)+1); let ts = replayer.iframe.contentDocument.querySelector('textarea'); vals.push((e.data.source === 0 ? 'Mutation' : 'User') + ':' + ts.value); + let ts2 = replayer.iframe.contentDocument.getElementById('ta2'); + if (ts2) { + vals.push('ta2:' + ts2.value); + } }); vals; `); expect(replayTextareaValues).toEqual([ 'Mutation:pre value', + 'ta2:', 'Mutation:ok', + 'ta2:added', 'Mutation:ok3', 'User:1ok3', 'Mutation:1ok3', // if this gets set to 'ignore', it's an error, as the 'user' has modified the textarea @@ -166,6 +181,131 @@ describe('record integration tests', function (this: ISuite) { ]); }); + it('can record and replay style mutations', async () => { + // This test shows that the `isStyle` attribute on textContent is not needed in a mutation + // TODO: we could get a lot more elaborate here with mixed textContent and insertRule mutations + const page: puppeteer.Page = await browser.newPage(); + await page.goto(`${serverURL}/html`); + await page.setContent(getHtml.call(this, 'style.html')); + + await waitForRAF(page); // ensure mutations aren't included in fullsnapshot + + await page.evaluate(() => { + let styleEl = document.querySelector('style#dual-textContent'); + if (styleEl) { + styleEl.append( + document.createTextNode('body { background-color: darkgreen; }'), + ); + styleEl.append( + document.createTextNode( + '.absolutify { background-image: url("./rel"); }', + ), + ); + } + }); + await waitForRAF(page); + await page.evaluate(() => { + let styleEl = document.querySelector('style#dual-textContent'); + if (styleEl) { + styleEl.childNodes.forEach((cn) => { + if (cn.textContent) { + cn.textContent = cn.textContent.replace('darkgreen', 'purple'); + cn.textContent = cn.textContent.replace( + 'orange !important', + 'yellow', + ); + } + }); + } + }); + await waitForRAF(page); + await page.evaluate(() => { + let styleEl = document.querySelector('style#dual-textContent'); + if (styleEl) { + styleEl.childNodes.forEach((cn) => { + if (cn.textContent) { + cn.textContent = cn.textContent.replace( + 'black', + 'black !important', + ); + } + }); + } + let hoverMutationStyleEl = document.querySelector('style#hover-mutation'); + if (hoverMutationStyleEl) { + hoverMutationStyleEl.childNodes.forEach((cn) => { + if (cn.textContent) { + cn.textContent = 'a:hover { outline: cyan solid 1px; }'; + } + }); + } + let st = document.createElement('style'); + st.id = 'goldilocks'; + st.innerText = 'body { color: brown }'; + document.body.append(st); + }); + + await waitForRAF(page); + await page.evaluate(() => { + let styleEl = document.querySelector('style#goldilocks'); + if (styleEl) { + styleEl.childNodes.forEach((cn) => { + if (cn.textContent) { + cn.textContent = cn.textContent.replace('brown', 'gold'); + } + }); + } + }); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + + // following ensures that the ./rel url has been absolutized (in a mutation) + await assertSnapshot(snapshots); + + // check after each mutation and text input + const replayStyleValues = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(window.snapshots); + const vals = []; + window.snapshots.filter((e)=>e.data.attributes || e.data.source === 5).forEach((e)=>{ + replayer.pause((e.timestamp - window.snapshots[0].timestamp)+1); + let bodyStyle = getComputedStyle(replayer.iframe.contentDocument.querySelector('body')) + vals.push({ + 'background-color': bodyStyle['background-color'], + 'color': bodyStyle['color'], + }); + }); + vals.push(replayer.iframe.contentDocument.getElementById('single-textContent').innerText); + vals.push(replayer.iframe.contentDocument.getElementById('empty').innerText); + vals.push(replayer.iframe.contentDocument.getElementById('hover-mutation').innerText); + vals; +`); + + expect(replayStyleValues).toEqual([ + { + 'background-color': 'rgb(0, 100, 0)', // darkgreen + color: 'rgb(255, 165, 0)', // orange (from style.html) + }, + { + 'background-color': 'rgb(128, 0, 128)', // purple + color: 'rgb(255, 255, 0)', // yellow + }, + { + 'background-color': 'rgb(0, 0, 0)', // black !important + color: 'rgb(165, 42, 42)', // brown + }, + { + 'background-color': 'rgb(0, 0, 0)', + color: 'rgb(255, 215, 0)', // gold + }, + 'a:hover,\na.\\:hover { outline: red solid 1px; }', // has run adaptCssForReplay + 'a:hover,\na.\\:hover { outline: blue solid 1px; }', // has run adaptCssForReplay + 'a:hover,\na.\\:hover { outline: cyan solid 1px; }', // has run adaptCssForReplay after text mutation + ]); + }); + it('can record childList mutations', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); @@ -183,7 +323,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('can record character data muatations', async () => { @@ -205,7 +345,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('can record attribute mutation', async () => { @@ -225,7 +365,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('handles null attribute values', async () => { @@ -253,7 +393,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('can record node mutations', async () => { @@ -272,7 +412,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('can record style changes compactly and preserve css var() functions', async () => { @@ -322,7 +462,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('can freeze mutations', async () => { @@ -358,7 +498,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should not record input events on ignored elements', async () => { @@ -394,7 +534,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('can use maskInputOptions to configure which type of inputs should be masked', async () => { @@ -420,7 +560,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should mask password value attribute with maskInputOptions', async () => { @@ -444,7 +584,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should mask inputs via function call', async () => { @@ -474,7 +614,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record input userTriggered values if userTriggeredOnInput is enabled', async () => { @@ -494,7 +634,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should not record blocked elements and its child nodes', async () => { @@ -509,7 +649,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should not record blocked elements dynamically added', async () => { @@ -531,7 +671,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('mutations should work when blocked class is unblocked', async () => { @@ -552,7 +692,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record DOM node movement 1', async () => { @@ -572,7 +712,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record DOM node movement 2', async () => { @@ -589,7 +729,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record dynamic CSS changes', async () => { @@ -600,7 +740,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record canvas mutations', async () => { @@ -625,7 +765,7 @@ describe('record integration tests', function (this: ISuite) { }); } } - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should not record input values if dynamically added and maskAllInputs is true', async () => { @@ -662,7 +802,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('can correctly serialize a shader and multiple webgl contexts', async () => { @@ -677,7 +817,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('will serialize node before record', async () => { @@ -698,7 +838,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('will defer missing next node mutation', async () => { @@ -735,7 +875,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record images with blob url', async () => { @@ -752,7 +892,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record images inside iframe with blob url', async () => { @@ -769,7 +909,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record images inside iframe with blob url after iframe was reloaded', async () => { @@ -792,7 +932,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record shadow DOM', async () => { @@ -842,7 +982,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record shadow DOM 2', async () => { @@ -867,7 +1007,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record shadow DOM 3', async () => { @@ -888,7 +1028,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record moved shadow DOM', async () => { @@ -917,7 +1057,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record moved shadow DOM 2', async () => { @@ -955,7 +1095,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record nested iframes and shadow doms', async () => { @@ -1000,7 +1140,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record mutations in iframes accross pages', async () => { @@ -1030,7 +1170,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); // https://github.com/webcomponents/polyfills/tree/master/packages/shadydom @@ -1064,7 +1204,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); // https://github.com/salesforce/lwc/tree/master/packages/%40lwc/synthetic-shadow @@ -1106,7 +1246,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should mask texts', async () => { @@ -1121,7 +1261,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should mask texts using maskTextFn', async () => { @@ -1137,7 +1277,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should unmask texts using maskTextFn', async () => { @@ -1157,7 +1297,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('can mask character data mutations', async () => { @@ -1186,7 +1326,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('can mask character data mutations with regexp', async () => { @@ -1219,7 +1359,7 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record after DOMContentLoaded event', async () => { @@ -1234,6 +1374,175 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); + }); + + /** + * the regression part of the following is now handled by replayer.test.ts::'can deal with duplicate/conflicting values on style elements' + * so this test could be dropped if we add more robust mixing of `insertRule` into 'can record and replay style mutations' + */ + it('should record style mutations and replay them correctly', async () => { + const page: puppeteer.Page = await browser.newPage(); + const OldColor = 'rgb(255, 0, 0)'; // red color + const NewColor = 'rgb(255, 255, 0)'; // yellow color + + await page.setContent( + ` + + + + + +
+
+ + + `, + ); + // Start rrweb recording + await page.evaluate( + (code, recordSnippet) => { + const script = document.createElement('script'); + script.textContent = `${code}window.Date.now = () => new Date(Date.UTC(2018, 10, 15, 8)).valueOf();${recordSnippet}`; + document.head.appendChild(script); + }, + code, + generateRecordSnippet({}), + ); + + await page.evaluate( + async (OldColor, NewColor) => { + // Create a new style element with the same content as the existing style element and apply it to the #two div element + const incrementalStyle = document.createElement( + 'style', + ) as HTMLStyleElement; + incrementalStyle.textContent = ` \n`; + document.head.appendChild(incrementalStyle); + incrementalStyle.sheet!.insertRule(`#two { color: ${OldColor}; }`, 0); + + await new Promise((resolve) => + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }), + ); + + // Change the color of the #one div element to yellow as an incremental style mutation + const styleElement = document.querySelector('style')!; + (styleElement.sheet!.cssRules[0] as any).style.setProperty( + 'color', + NewColor, + ); + // Change the color of the #two div element to yellow as an incremental style mutation + (incrementalStyle.sheet!.cssRules[0] as any).style.setProperty( + 'color', + NewColor, + ); + }, + OldColor, + NewColor, + ); + await waitForRAF(page); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + await assertSnapshot(snapshots); + + /** + * Replay the recorded events and check if the style mutation is applied correctly + */ + const changedColors = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(window.snapshots); + replayer.pause(1000); + + // Get the color of the element after applying the style mutation event + [ + window.getComputedStyle( + replayer.iframe.contentDocument.querySelector('#one'), + ).color, + window.getComputedStyle( + replayer.iframe.contentDocument.querySelector('#two'), + ).color, + ]; + `); + expect(changedColors).toEqual([NewColor, NewColor]); + await page.close(); + }); + + it('should record style mutations with multiple child nodes and replay them correctly', async () => { + // ensure that presence of multiple text nodes doesn't interfere with programmatic insertRule operations + + const page: puppeteer.Page = await browser.newPage(); + const Color = 'rgb(255, 0, 0)'; // red color + + await page.setContent( + ` + + + + + +
+
+ + + `, + ); + // Start rrweb recording + await page.evaluate( + (code, recordSnippet) => { + const script = document.createElement('script'); + script.textContent = `${code};${recordSnippet}`; + document.head.appendChild(script); + }, + code, + generateRecordSnippet({}), + ); + + await page.evaluate(async (Color) => { + // Create a new style element with the same content as the existing style element and apply it to the #two div element + const incrementalStyle = document.createElement( + 'style', + ) as HTMLStyleElement; + incrementalStyle.append(document.createTextNode('/* hello */')); + incrementalStyle.append(document.createTextNode('/* world */')); + document.head.appendChild(incrementalStyle); + incrementalStyle.sheet!.insertRule(`#two { color: ${Color}; }`, 0); + }, Color); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + await assertSnapshot(snapshots); + + /** + * Replay the recorded events and check if the style mutation is applied correctly + */ + const changedColors = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(window.snapshots); + replayer.pause(1000); + + // Get the color of the element after applying the style mutation event + [ + window.getComputedStyle( + replayer.iframe.contentDocument.querySelector('#one'), + ).color, + window.getComputedStyle( + replayer.iframe.contentDocument.querySelector('#two'), + ).color, + ]; + `); + expect(changedColors).toEqual([Color, Color]); + await page.close(); }); }); diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index d35dd6f6a2..72a8a7fbe8 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -206,7 +206,7 @@ describe('record', function (this: ISuite) { }, 10); }); await ctx.page.waitForTimeout(100); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('should record scroll position', async () => { @@ -223,7 +223,7 @@ describe('record', function (this: ISuite) { p.scrollLeft = 10; }); await waitForRAF(ctx.page); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('should record selection event', async () => { @@ -279,7 +279,7 @@ describe('record', function (this: ISuite) { }); }); await ctx.page.waitForTimeout(50); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('captures stylesheet rules', async () => { @@ -297,6 +297,7 @@ describe('record', function (this: ISuite) { // begin: pre-serialization const ruleIdx0 = styleSheet.insertRule('body { background: #000; }'); const ruleIdx1 = styleSheet.insertRule('body { background: #111; }'); + styleSheet.deleteRule(ruleIdx1); // end: pre-serialization setTimeout(() => { @@ -328,8 +329,71 @@ describe('record', function (this: ISuite) { rule: 'body { color: #fff; }', }, ]); + expect((addRules[1].data as styleSheetRuleData).adds).toEqual([ + { + rule: 'body { color: #ccc; }', + }, + ]); + expect(removeRuleCount).toEqual(1); + await assertSnapshot(ctx.events); + }); + + it('captures stylesheet rules with deprecated addRule & removeRule properties', async () => { + await ctx.page.evaluate(() => { + const { record } = (window as unknown as IWindow).rrweb; + + record({ + emit: (window as unknown as IWindow).emit, + }); + + const styleElement = document.createElement('style'); + document.head.appendChild(styleElement); + + const styleSheet = styleElement.sheet; + // begin: pre-serialization + const ruleIdx0 = styleSheet.addRule('body', 'background: #000;'); + const ruleIdx1 = styleSheet.addRule('body', 'background: #111;'); + + styleSheet.removeRule(ruleIdx1); + // end: pre-serialization + setTimeout(() => { + styleSheet.addRule('body', 'color: #fff;'); + }, 0); + setTimeout(() => { + styleSheet.removeRule(ruleIdx0); + }, 5); + setTimeout(() => { + styleSheet.addRule('body', 'color: #ccc;'); + }, 10); + }); + await ctx.page.waitForTimeout(50); + const styleSheetRuleEvents = ctx.events.filter( + (e) => + e.type === EventType.IncrementalSnapshot && + e.data.source === IncrementalSource.StyleSheetRule, + ); + const addRules = styleSheetRuleEvents.filter((e) => + Boolean((e.data as styleSheetRuleData).adds), + ); + const removeRuleCount = styleSheetRuleEvents.filter((e) => + Boolean((e.data as styleSheetRuleData).removes), + ).length; + // pre-serialization insert/delete should be ignored + expect(addRules.length).toEqual(2); + expect((addRules[0].data as styleSheetRuleData).adds).toEqual([ + { + index: 1, + rule: 'body { color: #fff; }', + }, + ]); + expect((addRules[1].data as styleSheetRuleData).adds).toEqual([ + { + index: 1, + rule: 'body { color: #ccc; }', + }, + ]); expect(removeRuleCount).toEqual(1); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); const captureNestedStylesheetRulesTest = async () => { @@ -375,7 +439,7 @@ describe('record', function (this: ISuite) { // sync insert/delete should be ignored expect(addRuleCount).toEqual(2); expect(removeRuleCount).toEqual(1); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }; it('captures nested stylesheet rules', captureNestedStylesheetRulesTest); @@ -426,7 +490,7 @@ describe('record', function (this: ISuite) { }, 0); }); await ctx.page.waitForTimeout(50); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('captures inserted style text nodes correctly', async () => { @@ -446,7 +510,7 @@ describe('record', function (this: ISuite) { styleEl.append(document.createTextNode('h1 { color: pink; }')); }); await waitForRAF(ctx.page); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('captures stylesheets with `blob:` url', async () => { @@ -473,7 +537,7 @@ describe('record', function (this: ISuite) { }); }); await waitForRAF(ctx.page); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('captures mutations on adopted stylesheets', async () => { @@ -538,7 +602,7 @@ describe('record', function (this: ISuite) { }); }); await waitForRAF(ctx.page); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('captures adopted stylesheets in nested shadow doms and iframes', async () => { @@ -590,7 +654,7 @@ describe('record', function (this: ISuite) { }, 150); }); await ctx.page.waitForTimeout(200); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('captures adopted stylesheets of shadow doms in checkout full snapshot', async () => { @@ -619,7 +683,7 @@ describe('record', function (this: ISuite) { }); }); await waitForRAF(ctx.page); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('captures stylesheets in iframes with `blob:` url', async () => { @@ -651,7 +715,7 @@ describe('record', function (this: ISuite) { }); }); await waitForRAF(ctx.page); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('aggregates mutations', async () => { @@ -698,7 +762,7 @@ describe('record', function (this: ISuite) { ); expect(mutationEvents.length).toEqual(0); // there was no aggregate effect - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('no need for attribute mutations on adds', async () => { @@ -737,7 +801,7 @@ describe('record', function (this: ISuite) { ); expect(mutationEvents.length).toEqual(1); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); describe('loading stylesheets', () => { @@ -789,7 +853,7 @@ describe('record', function (this: ISuite) { await ctx.page.waitForResponse(`${serverURL}/html/assets/style.css`); await waitForRAF(ctx.page); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('captures stylesheets in iframes that are still loading', async () => { @@ -823,7 +887,7 @@ describe('record', function (this: ISuite) { await waitForRAF(ctx.page); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); }); @@ -849,7 +913,7 @@ describe('record', function (this: ISuite) { await ctx.page.waitForResponse(corsStylesheetURL); // wait for stylesheet to be loaded await waitForRAF(ctx.page); // wait for rrweb to emit events - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('captures adopted stylesheets in shadow doms and iframe', async () => { @@ -924,7 +988,7 @@ describe('record', function (this: ISuite) { }); await waitForRAF(ctx.page); // wait till events get sent - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); }); @@ -1025,6 +1089,6 @@ describe('record iframes', function (this: ISuite) { expect(styleRelatedEvents.length).toEqual(5); expect(addRuleCount).toEqual(2); expect(removeRuleCount).toEqual(2); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); }); diff --git a/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap new file mode 100644 index 0000000000..03526f8c0f --- /dev/null +++ b/packages/rrweb/test/record/__snapshots__/dialog.test.ts.snap @@ -0,0 +1,487 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`dialog > add dialog and show 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"text/javascript\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 5 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"I'm a dialog\\", + \\"id\\": 9 + } + ], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 10 + } + ], + \\"id\\": 6 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 6, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": { + \\"open\\": \\"\\", + \\"rr_open_mode\\": \\"non-modal\\" + }, + \\"childNodes\\": [], + \\"id\\": 11 + } + } + ] + } + } +]" +`; + +exports[`dialog > add dialog and showModal 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"text/javascript\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 5 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"I'm a dialog\\", + \\"id\\": 9 + } + ], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 10 + } + ], + \\"id\\": 6 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 6, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": { + \\"open\\": \\"\\", + \\"rr_open_mode\\": \\"modal\\" + }, + \\"childNodes\\": [], + \\"id\\": 11 + } + } + ] + } + } +]" +`; + +exports[`dialog > switch to show dialog 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"text/javascript\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 5 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"I'm a dialog\\", + \\"id\\": 9 + } + ], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 10 + } + ], + \\"id\\": 6 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 8, + \\"attributes\\": { + \\"open\\": \\"\\", + \\"rr_open_mode\\": \\"modal\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 8, + \\"attributes\\": { + \\"open\\": \\"\\", + \\"rr_open_mode\\": \\"non-modal\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`dialog > switch to showModal dialog 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"text/javascript\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 5 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"dialog\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"I'm a dialog\\", + \\"id\\": 9 + } + ], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 10 + } + ], + \\"id\\": 6 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 8, + \\"attributes\\": { + \\"open\\": \\"\\", + \\"rr_open_mode\\": \\"non-modal\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 8, + \\"attributes\\": { + \\"open\\": \\"\\", + \\"rr_open_mode\\": \\"modal\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index 2cdf69f006..32aa4644eb 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -53,7 +53,11 @@ async function injectRecordScript( } catch (e) { // we get this error: `Protocol error (DOM.resolveNode): Node with given id does not belong to the document` // then the page wasn't loaded yet and we try again - if (!e.message.includes('DOM.resolveNode')) throw e; + if ( + !e.message.includes('DOM.resolveNode') || + !e.message.includes('DOM.describeNode') + ) + throw e; await injectRecordScript(frame, options); return; } @@ -238,7 +242,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should map scroll events correctly', async () => { @@ -261,7 +265,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); }); @@ -289,7 +293,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record DOM node removal', async () => { @@ -301,7 +305,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record DOM attribute changes', async () => { @@ -313,7 +317,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record DOM text changes', async () => { @@ -325,7 +329,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record canvas elements', async () => { @@ -342,7 +346,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should record custom events', async () => { @@ -358,7 +362,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('captures mutations on adopted stylesheets', async () => { @@ -421,7 +425,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('captures mutations on stylesheets', async () => { @@ -476,7 +480,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); }); @@ -505,7 +509,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); }); @@ -537,7 +541,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); it('should filter out forwarded cross origin rrweb messages', async () => { @@ -564,7 +568,7 @@ describe('cross origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); }); }); @@ -592,7 +596,7 @@ describe('same origin iframes', function (this: ISuite) { // two events (full snapshot + meta) from main frame, // and two (full snapshot + mutation) from iframe expect(events.length).toBe(4); - assertSnapshot(events); + await assertSnapshot(events); }); it('should record cross-origin iframe in same-origin iframe', async () => { @@ -616,6 +620,6 @@ describe('same origin iframes', function (this: ISuite) { const snapshots = (await ctx.page.evaluate( 'window.snapshots', )) as eventWithTime[]; - assertSnapshot(snapshots); + await assertSnapshot(snapshots); }); }); diff --git a/packages/rrweb/test/record/dialog.test.ts b/packages/rrweb/test/record/dialog.test.ts new file mode 100644 index 0000000000..ab6542b547 --- /dev/null +++ b/packages/rrweb/test/record/dialog.test.ts @@ -0,0 +1,229 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { vi } from 'vitest'; + +import { + assertSnapshot, + getServerURL, + ISuite, + launchPuppeteer, + startServer, + waitForRAF, +} from '../utils'; +import { + attributeMutation, + EventType, + eventWithTime, + listenerHandler, +} from '@rrweb/types'; +import { recordOptions } from '../../src/types'; + +interface IWindow extends Window { + rrweb: { + record: ( + options: recordOptions, + ) => listenerHandler | undefined; + addCustomEvent(tag: string, payload: T): void; + }; + emit: (e: eventWithTime) => undefined; +} + +const attributeMutationFactory = ( + mutation: attributeMutation['attributes'], +) => { + return { + data: { + attributes: [ + { + attributes: mutation, + }, + ], + }, + }; +}; + +describe('dialog', () => { + vi.setConfig({ testTimeout: 100_000 }); + let code: ISuite['code']; + let page: ISuite['page']; + let browser: ISuite['browser']; + let server: ISuite['server']; + let serverURL: ISuite['serverURL']; + let events: ISuite['events']; + + beforeAll(async () => { + server = await startServer(); + serverURL = getServerURL(server); + browser = await launchPuppeteer(); + + const bundlePath = path.resolve(__dirname, '../../dist/rrweb.umd.cjs'); + code = fs.readFileSync(bundlePath, 'utf8'); + }); + + afterEach(async () => { + await page.close(); + }); + + afterAll(async () => { + await server.close(); + await browser.close(); + }); + + beforeEach(async () => { + page = await browser.newPage(); + page.on('console', (msg) => { + console.log(msg.text()); + }); + + await page.goto(`${serverURL}/html/dialog.html`); + await page.addScriptTag({ + path: path.resolve(__dirname, '../../dist/rrweb.umd.cjs'), + }); + await waitForRAF(page); + events = []; + + await page.exposeFunction('emit', (e: eventWithTime) => { + if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) { + return; + } + events.push(e); + }); + + page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + + await page.evaluate(() => { + const { record } = (window as unknown as IWindow).rrweb; + record({ + emit: (window as unknown as IWindow).emit, + }); + }); + + await waitForRAF(page); + }); + + it('show dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.show(); + }); + + const lastEvent = events[events.length - 1]; + + expect(lastEvent).toMatchObject(attributeMutationFactory({ open: '' })); + // assertSnapshot(events); + }); + + it('showModal dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.showModal(); + }); + + const lastEvent = events[events.length - 1]; + + expect(lastEvent).toMatchObject( + attributeMutationFactory({ rr_open_mode: 'modal' }), + ); + }); + + it('showModal & close dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.showModal(); + }); + await waitForRAF(page); + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.close(); + }); + + const lastEvent = events[events.length - 1]; + + expect(lastEvent).toMatchObject(attributeMutationFactory({ open: null })); + }); + + it('show & close dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.show(); + }); + await waitForRAF(page); + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.close(); + }); + + const lastEvent = events[events.length - 1]; + + expect(lastEvent).toMatchObject(attributeMutationFactory({ open: null })); + }); + + it('switch to showModal dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.show(); + }); + await waitForRAF(page); + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.close(); + dialog.showModal(); + }); + + await assertSnapshot(events); + }); + + it('switch to show dialog', async () => { + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.showModal(); + }); + await waitForRAF(page); + await page.evaluate(() => { + const dialog = document.querySelector('dialog') as HTMLDialogElement; + dialog.close(); + dialog.show(); + }); + + await assertSnapshot(events); + }); + + it('add dialog and showModal', async () => { + await page.evaluate(() => { + const dialog = document.createElement('dialog') as HTMLDialogElement; + document.body.appendChild(dialog); + dialog.showModal(); + }); + await waitForRAF(page); + + await assertSnapshot(events); + }); + + it('add dialog and show', async () => { + await page.evaluate(() => { + const dialog = document.createElement('dialog') as HTMLDialogElement; + document.body.appendChild(dialog); + dialog.show(); + }); + await waitForRAF(page); + + await assertSnapshot(events); + }); + + // TODO: implement me in the future + it.skip('should record playback order with multiple dialogs opening', async () => { + await page.evaluate(() => { + const dialog1 = document.createElement('dialog') as HTMLDialogElement; + dialog1.className = 'dialog1'; + document.body.appendChild(dialog1); + const dialog2 = document.createElement('dialog') as HTMLDialogElement; + dialog1.className = 'dialog2'; + document.body.appendChild(dialog2); + dialog2.showModal(); // <== Note that dialog TWO is being triggered first + dialog1.showModal(); + }); + + await waitForRAF(page); + await assertSnapshot(events); // <== This should trigger showModal() on dialog2 first, then dialog1 + }); +}); diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts index c19023732a..87999c458f 100644 --- a/packages/rrweb/test/record/webgl.test.ts +++ b/packages/rrweb/test/record/webgl.test.ts @@ -124,7 +124,7 @@ describe('record webgl', function (this: ISuite) { ], }, }); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('will record changes to a webgl2 canvas element', async () => { @@ -150,7 +150,7 @@ describe('record webgl', function (this: ISuite) { ], }, }); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('will record changes to a canvas element before the canvas gets added', async () => { @@ -165,7 +165,7 @@ describe('record webgl', function (this: ISuite) { await waitForRAF(ctx.page); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('will record changes to a canvas element before the canvas gets added (webgl2)', async () => { @@ -188,7 +188,7 @@ describe('record webgl', function (this: ISuite) { // we need to change this await waitForRAF(ctx.page); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('will record webgl variables', async () => { @@ -203,7 +203,7 @@ describe('record webgl', function (this: ISuite) { await ctx.page.waitForTimeout(50); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('will record webgl variables in reverse order', async () => { @@ -219,7 +219,7 @@ describe('record webgl', function (this: ISuite) { await ctx.page.waitForTimeout(50); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); }); it('sets _context on canvas.getContext()', async () => { @@ -264,7 +264,7 @@ describe('record webgl', function (this: ISuite) { await ctx.page.waitForTimeout(50); - assertSnapshot(ctx.events); + await assertSnapshot(ctx.events); expect(ctx.events.length).toEqual(5); }); @@ -312,7 +312,7 @@ describe('record webgl', function (this: ISuite) { await waitForRAF(ctx.page); // should yield a frame for each change at a max of 60fps - assertSnapshot(stripBase64(ctx.events)); + await assertSnapshot(stripBase64(ctx.events)); }); }); }); diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-closed-dialogs-show-nothing-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-closed-dialogs-show-nothing-1-snap.png new file mode 100644 index 0000000000..9fb34401f4 Binary files /dev/null and b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-closed-dialogs-show-nothing-1-snap.png differ diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-alternative.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-alternative.png new file mode 100644 index 0000000000..f328c34b5b Binary files /dev/null and b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot-alternative.png differ diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot.png new file mode 100644 index 0000000000..f328c34b5b Binary files /dev/null and b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-add-an-opened-dialog-with-show-modal-in-incremental-snapshot.png differ diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed.png new file mode 100644 index 0000000000..9fb34401f4 Binary files /dev/null and b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-close-dialog-again-when-open-attribute-gets-removed.png differ diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot.png new file mode 100644 index 0000000000..790b97b6a4 Binary files /dev/null and b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-in-full-snapshot.png differ diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot.png new file mode 100644 index 0000000000..679ab53ab6 Binary files /dev/null and b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal-in-full-snapshot.png differ diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal.png new file mode 100644 index 0000000000..5fe6c04ee5 Binary files /dev/null and b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-open-dialog-with-show-modal.png differ diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal.png new file mode 100644 index 0000000000..5fe6c04ee5 Binary files /dev/null and b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-and-show-modal.png differ diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show.png new file mode 100644 index 0000000000..a3faffd85a Binary files /dev/null and b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-should-switch-between-show-modal-and-show.png differ diff --git a/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-the-dialog-when-open-attribute-gets-added.png b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-the-dialog-when-open-attribute-gets-added.png new file mode 100644 index 0000000000..a3faffd85a Binary files /dev/null and b/packages/rrweb/test/replay/__image_snapshots__/dialog-test-ts-test-replay-dialog-test-ts-dialog-show-the-dialog-when-open-attribute-gets-added.png differ diff --git a/packages/rrweb/test/replay/dialog.test.ts b/packages/rrweb/test/replay/dialog.test.ts new file mode 100644 index 0000000000..7fc1ab99f2 --- /dev/null +++ b/packages/rrweb/test/replay/dialog.test.ts @@ -0,0 +1,159 @@ +import * as fs from 'fs'; +import { toMatchImageSnapshot } from 'jest-image-snapshot'; +import * as path from 'path'; +import { vi } from 'vitest'; + +import dialogPlaybackEvents, { + closedFullSnapshotTime, + showIncrementalAttributeTime, + closeIncrementalAttributeTime, + showModalIncrementalAttributeTime, + showFullSnapshotTime, + showModalFullSnapshotTime, + showModalIncrementalAddTime, + switchBetweenShowModalAndShowIncrementalAttributeTime, + switchBetweenShowAndShowModalIncrementalAttributeTime, +} from '../events/dialog-playback'; +import { + fakeGoto, + getServerURL, + hideMouseAnimation, + ISuite, + launchPuppeteer, + startServer, + waitForRAF, +} from '../utils'; + +expect.extend({ toMatchImageSnapshot }); + +describe('dialog', () => { + vi.setConfig({ testTimeout: 100_000 }); + let code: ISuite['code']; + let page: ISuite['page']; + let browser: ISuite['browser']; + let server: ISuite['server']; + let serverURL: ISuite['serverURL']; + + beforeAll(async () => { + server = await startServer(); + serverURL = getServerURL(server); + browser = await launchPuppeteer(); + + const bundlePath = path.resolve(__dirname, '../../dist/rrweb.umd.cjs'); + code = fs.readFileSync(bundlePath, 'utf8'); + }); + + afterEach(async () => { + await page.close(); + }); + + afterAll(async () => { + await server.close(); + await browser.close(); + }); + + beforeEach(async () => { + page = await browser.newPage(); + page.on('console', (msg) => { + console.log(msg.text()); + }); + + await fakeGoto(page, `${serverURL}/html/dialog.html`); + await page.evaluate(code); + await waitForRAF(page); + await hideMouseAnimation(page); + }); + + [ + { + name: 'show the dialog when open attribute gets added', + time: showIncrementalAttributeTime, + }, + { + name: 'should close dialog again when open attribute gets removed', + time: closeIncrementalAttributeTime, + }, + { + name: 'should open dialog with showModal', + time: showModalIncrementalAttributeTime, + }, + { + name: 'should switch between showModal and show', + time: switchBetweenShowModalAndShowIncrementalAttributeTime, + }, + { + name: 'should switch between show and showModal', + time: switchBetweenShowAndShowModalIncrementalAttributeTime, + }, + { + name: 'should open dialog with show in full snapshot', + time: showFullSnapshotTime, + }, + { + name: 'should open dialog with showModal in full snapshot', + time: showModalFullSnapshotTime, + }, + { + name: 'should add an opened dialog with showModal in incremental snapshot', + time: showModalIncrementalAddTime, + }, + { + name: 'should add an opened dialog with showModal in incremental snapshot alternative', + time: [showModalFullSnapshotTime, showModalIncrementalAddTime], + }, + ].forEach(({ name, time }) => { + [true, false].forEach((useVirtualDom) => { + it(`${name} (virtual dom: ${useVirtualDom})`, async () => { + await page.evaluate( + `let events = ${JSON.stringify(dialogPlaybackEvents)}`, + ); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events, { useVirtualDom: ${useVirtualDom} }); + `); + const timeArray = Array.isArray(time) ? time : [time]; + for (let i = 0; i < timeArray.length; i++) { + await page.evaluate(` + window.replayer.pause(${timeArray[i]}); + `); + await waitForRAF(page); + } + + const frameImage = await page!.screenshot({ + fullPage: false, + }); + const defaultImageFilePrefix = + 'dialog-test-ts-test-replay-dialog-test-ts-dialog'; + const kebabCaseName = name + .replace(/ /g, '-') + .replace(/showModal/g, 'show-modal'); + const imageFileName = `${defaultImageFilePrefix}-${kebabCaseName}`; + expect(frameImage).toMatchImageSnapshot({ + customSnapshotIdentifier: imageFileName, + failureThreshold: 0.05, + failureThresholdType: 'percent', + dumpDiffToConsole: true, + storeReceivedOnFailure: true, + }); + }); + }); + }); + + it('closed dialogs show nothing', async () => { + await page.evaluate(`let events = ${JSON.stringify(dialogPlaybackEvents)}`); + await page.evaluate(` + const { Replayer } = rrweb; + window.replayer = new Replayer(events); + `); + await waitForRAF(page); + + const frameImage = await page!.screenshot(); + expect(frameImage).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); + + // TODO: implement me in the future + it.skip('should trigger showModal on multiple dialogs in a specific order'); +}); diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index 404357a799..3cdc05abd5 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -18,7 +18,8 @@ import inputEvents from './events/input'; import iframeEvents from './events/iframe'; import selectionEvents from './events/selection'; import shadowDomEvents from './events/shadow-dom'; -import textareaEvents from './events/bad-textarea'; +import badTextareaEvents from './events/bad-textarea'; +import badStyleEvents from './events/bad-style'; import StyleSheetTextMutation from './events/style-sheet-text-mutation'; import canvasInIframe from './events/canvas-in-iframe'; import adoptedStyleSheet from './events/adopted-style-sheet'; @@ -1142,7 +1143,7 @@ describe('replayer', function () { }); it('can deal with legacy duplicate/conflicting values on textareas', async () => { - await page.evaluate(`events = ${JSON.stringify(textareaEvents)}`); + await page.evaluate(`events = ${JSON.stringify(badTextareaEvents)}`); const displayValue = await page.evaluate(` const { Replayer } = rrweb; @@ -1155,4 +1156,25 @@ describe('replayer', function () { // If the custom element is defined, the display value will be 'block'. expect(displayValue).toEqual('this value is used for replay'); }); + + it('can deal with duplicate/conflicting values on style elements', async () => { + await page.evaluate(`events = ${JSON.stringify(badStyleEvents)}`); + + const changedColors = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); + replayer.pause(1000); + // Get the color of the elements after applying the style mutation event + [ + replayer.iframe.contentWindow.getComputedStyle( + replayer.iframe.contentDocument.querySelector('#one'), + ).color, + replayer.iframe.contentWindow.getComputedStyle( + replayer.iframe.contentDocument.querySelector('#two'), + ).color, + ]; +`); + const newColor = 'rgb(255, 255, 0)'; // yellow + expect(changedColors).toEqual([newColor, newColor]); + }); }); diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index c7347d89c3..42c8961304 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -110,9 +110,17 @@ export function stringifySnapshots(snapshots: eventWithTime[]): string { snapshots .filter((s) => { if ( - s.type === EventType.IncrementalSnapshot && - (s.data.source === IncrementalSource.MouseMove || - s.data.source === IncrementalSource.ViewportResize) + // mouse move or viewport resize can happen on accidental user interference + // so we ignore them + (s.type === EventType.IncrementalSnapshot && + (s.data.source === IncrementalSource.MouseMove || + s.data.source === IncrementalSource.ViewportResize)) || + // ignore '[vite] connected' messages from vite + (s.type === EventType.Plugin && + s.data.plugin === 'rrweb/console@1' && + (s.data.payload as { payload: string[] })?.payload?.find((msg) => + msg.includes('[vite] connected'), + )) ) { return false; } @@ -242,18 +250,18 @@ export function stringifySnapshots(snapshots: eventWithTime[]): string { function stripBlobURLsFromAttributes(node: { attributes: { - src?: string; + [key: string]: any; }; }) { - if ( - 'src' in node.attributes && - node.attributes.src && - typeof node.attributes.src === 'string' && - node.attributes.src.startsWith('blob:') - ) { - node.attributes.src = node.attributes.src - .replace(/[\w-]+$/, '...') - .replace(/:[0-9]+\//, ':xxxx/'); + for (const attr in node.attributes) { + if ( + typeof node.attributes[attr] === 'string' && + node.attributes[attr].startsWith('blob:') + ) { + node.attributes[attr] = node.attributes[attr] + .replace(/[\w-]+$/, '...') + .replace(/:[0-9]+\//, ':xxxx/'); + } } } diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json index 6a8a5ffb3d..d27cd53179 100644 --- a/packages/rrweb/tsconfig.json +++ b/packages/rrweb/tsconfig.json @@ -9,7 +9,6 @@ "vite/client", "@types/dom-mediacapture-transform", "@types/offscreencanvas", - // rrweb specific: /* * @see https://vitest.dev/config/#globals @@ -18,7 +17,6 @@ */ "vitest/globals" ], - // TODO: enable me in the future, this is quite a large project // at time of writing (April 2024) there are over 100 errors in rrweb "strict": false @@ -27,6 +25,9 @@ { "path": "../types" }, + { + "path": "../utils" + }, { "path": "../rrdom" }, diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 18210906af..b0e111d212 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,32 @@ # @rrweb/types +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb-snapshot@2.0.13 + +## 2.0.0-alpha.17 + +### Minor Changes + +- [#1503](https://github.com/rrweb-io/rrweb/pull/1503) [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158) Thanks [@Juice10](https://github.com/Juice10)! - Support top-layer components. Fixes #1381. + +### Patch Changes + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`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 + +## 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 + ## 2.0.12 ### Patch Changes diff --git a/packages/types/package.json b/packages/types/package.json index 0019d485e0..f95bff061d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@saola.ai/rrweb-types", - "version": "2.0.12", + "version": "2.0.13", "publishConfig": { "access": "public" }, @@ -10,9 +10,9 @@ ], "scripts": { "dev": "vite build --watch", - "build": "tsc -noEmit && vite build", + "build": "yarn turbo run prepublish", "check-types": "tsc -noEmit", - "prepublish": "npm run build", + "prepublish": "tsc -noEmit && vite build", "lint": "yarn eslint src/**/*.ts" }, "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/types#readme", @@ -46,19 +46,11 @@ "package.json" ], "devDependencies": { - "vite": "^5.2.8", - "vite-plugin-dts": "^3.8.1" + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1" }, "dependencies": { - "@changesets/cli": "^2.27.1", - "@monorepo-utils/workspaces-to-typescript-project-references": "^2.8.2", - "@saola.ai/rrweb-snapshot": "^2.0.12", - "browserslist": "^4.22.1", - "concurrently": "^7.1.0", - "cssom": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "eslint": "^8.53.0", - "eslint-plugin-jest": "^27.6.0", - "turbo": "^2.0.3" + "@saola.ai/rrweb-snapshot": "^2.0.13" }, "browserslist": [ "supports es6-class" diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md new file mode 100644 index 0000000000..6fd00c059f --- /dev/null +++ b/packages/utils/CHANGELOG.md @@ -0,0 +1,13 @@ +# @rrweb/utils + +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +## 2.0.0-alpha.17 + +### Patch Changes + +- [#1509](https://github.com/rrweb-io/rrweb/pull/1509) [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414) Thanks [@Juice10](https://github.com/Juice10)! - Reverse monkey patch built in methods to support LWC (and other frameworks like angular which monkey patch built in methods). diff --git a/packages/utils/Readme.md b/packages/utils/Readme.md new file mode 100644 index 0000000000..2107a2f228 --- /dev/null +++ b/packages/utils/Readme.md @@ -0,0 +1,178 @@ +# @rrweb/utils + +This package contains the shared utility functions used across rrweb packages. +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 🥇 + +
+ +sponsor +sponsor +sponsor +sponsor +sponsor +sponsor +sponsor + +
+ +### Silver Sponsors 🥈 + +
+ +sponsor +sponsor +sponsor +sponsor +sponsor +sponsor +sponsor + +
+ +### Bronze Sponsors 🥉 + +
+ +sponsor +sponsor +sponsor +sponsor +sponsor +sponsor +sponsor +sponsor +sponsor + +
+ +### Backers + + + +## Core Team Members + + + + + + + + +
+ + +
Yuyz0112 +

+
+
+ + +
Yun Feng +

+
+
+ + +
eoghanmurray +

+
+
+ + +
Juice10 +
open for rrweb consulting +
+
+ +## Who's using rrweb? + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + Smart screen recording for SaaS + +
+ + The first ever UX automation tool + + + + Remote Access & Co-Browsing + + + + The open source, fullstack Monitoring Platform. + + + + Comprehensive data analytics platform that empowers businesses to gain valuable insights and make data-driven decisions. + +
+ + Intercept, Modify, Record & Replay HTTP Requests. + + + + In-app bug reporting & customer feedback platform. + + + + Self-hosted website analytics with heatmaps and session recordings. + + + + Interactive product demos for small marketing teams + +
diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 0000000000..4df05440ad --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,53 @@ +{ + "name": "@saola.ai/rrweb-utils", + "version": "2.0.13", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "rrweb", + "@rrweb/utils" + ], + "scripts": { + "dev": "vite build --watch", + "build": "tsc -noEmit && vite build", + "check-types": "tsc -noEmit", + "prepublish": "npm run build", + "lint": "yarn eslint src/**/*.ts" + }, + "homepage": "https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/utils#readme", + "bugs": { + "url": "https://github.com/rrweb-io/rrweb/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rrweb-io/rrweb.git" + }, + "license": "MIT", + "type": "module", + "main": "./dist/rrweb-utils.umd.cjs", + "module": "./dist/rrweb-utils.js", + "unpkg": "./dist/rrweb-utils.umd.cjs", + "typings": "dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/rrweb-utils.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/rrweb-utils.umd.cjs" + } + } + }, + "files": [ + "dist", + "package.json" + ], + "devDependencies": { + "vite": "^5.2.8", + "vite-plugin-dts": "^3.8.1" + }, + "dependencies": {} +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 0000000000..b88d7f452e --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1,221 @@ +type PrototypeOwner = Node | ShadowRoot | MutationObserver | Element; +type TypeofPrototypeOwner = + | typeof Node + | typeof ShadowRoot + | typeof MutationObserver + | typeof Element; + +type BasePrototypeCache = { + Node: typeof Node.prototype; + ShadowRoot: typeof ShadowRoot.prototype; + MutationObserver: typeof MutationObserver.prototype; + Element: typeof Element.prototype; +}; + +const testableAccessors = { + Node: ['childNodes', 'parentNode', 'parentElement', 'textContent'] as const, + ShadowRoot: ['host', 'styleSheets'] as const, + Element: ['shadowRoot', 'querySelector', 'querySelectorAll'] as const, + MutationObserver: [] as const, +} as const; + +const testableMethods = { + Node: ['contains', 'getRootNode'] as const, + ShadowRoot: ['getSelection'], + Element: [], + MutationObserver: ['constructor'], +} as const; + +const untaintedBasePrototype: Partial = {}; + +export function getUntaintedPrototype( + key: T, +): BasePrototypeCache[T] { + if (untaintedBasePrototype[key]) + return untaintedBasePrototype[key] as BasePrototypeCache[T]; + + const defaultObj = globalThis[key] as TypeofPrototypeOwner; + const defaultPrototype = defaultObj.prototype as BasePrototypeCache[T]; + + // use list of testable accessors to check if the prototype is tainted + const accessorNames = + key in testableAccessors ? testableAccessors[key] : undefined; + const isUntaintedAccessors = Boolean( + accessorNames && + // @ts-expect-error 2345 + accessorNames.every((accessor: keyof typeof defaultPrototype) => + Boolean( + Object.getOwnPropertyDescriptor(defaultPrototype, accessor) + ?.get?.toString() + .includes('[native code]'), + ), + ), + ); + + const methodNames = key in testableMethods ? testableMethods[key] : undefined; + const isUntaintedMethods = Boolean( + methodNames && + methodNames.every( + // @ts-expect-error 2345 + (method: keyof typeof defaultPrototype) => + typeof defaultPrototype[method] === 'function' && + defaultPrototype[method]?.toString().includes('[native code]'), + ), + ); + + if (isUntaintedAccessors && isUntaintedMethods) { + untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T]; + return defaultObj.prototype as BasePrototypeCache[T]; + } + + try { + const iframeEl = document.createElement('iframe'); + document.body.appendChild(iframeEl); + const win = iframeEl.contentWindow; + if (!win) return defaultObj.prototype as BasePrototypeCache[T]; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + const untaintedObject = (win as any)[key] + .prototype as BasePrototypeCache[T]; + // cleanup + document.body.removeChild(iframeEl); + + if (!untaintedObject) return defaultPrototype; + + return (untaintedBasePrototype[key] = untaintedObject); + } catch { + return defaultPrototype; + } +} + +const untaintedAccessorCache: Record< + string, + (this: PrototypeOwner, ...args: unknown[]) => unknown +> = {}; + +export function getUntaintedAccessor< + K extends keyof BasePrototypeCache, + T extends keyof BasePrototypeCache[K], +>( + key: K, + instance: BasePrototypeCache[K], + accessor: T, +): BasePrototypeCache[K][T] { + const cacheKey = `${key}.${String(accessor)}`; + if (untaintedAccessorCache[cacheKey]) + return untaintedAccessorCache[cacheKey].call( + instance, + ) as BasePrototypeCache[K][T]; + + const untaintedPrototype = getUntaintedPrototype(key); + // eslint-disable-next-line @typescript-eslint/unbound-method + const untaintedAccessor = Object.getOwnPropertyDescriptor( + untaintedPrototype, + accessor, + )?.get; + + if (!untaintedAccessor) return instance[accessor]; + + untaintedAccessorCache[cacheKey] = untaintedAccessor; + + return untaintedAccessor.call(instance) as BasePrototypeCache[K][T]; +} + +type BaseMethod = ( + this: BasePrototypeCache[K], + ...args: unknown[] +) => unknown; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const untaintedMethodCache: Record> = {}; +export function getUntaintedMethod< + K extends keyof BasePrototypeCache, + T extends keyof BasePrototypeCache[K], +>( + key: K, + instance: BasePrototypeCache[K], + method: T, +): BasePrototypeCache[K][T] { + const cacheKey = `${key}.${String(method)}`; + if (untaintedMethodCache[cacheKey]) + return untaintedMethodCache[cacheKey].bind( + instance, + ) as BasePrototypeCache[K][T]; + + const untaintedPrototype = getUntaintedPrototype(key); + const untaintedMethod = untaintedPrototype[method]; + + if (typeof untaintedMethod !== 'function') return instance[method]; + + untaintedMethodCache[cacheKey] = untaintedMethod as BaseMethod; + + return untaintedMethod.bind(instance) as BasePrototypeCache[K][T]; +} + +export function childNodes(n: Node): NodeListOf { + return getUntaintedAccessor('Node', n, 'childNodes'); +} + +export function parentNode(n: Node): ParentNode | null { + return getUntaintedAccessor('Node', n, 'parentNode'); +} + +export function parentElement(n: Node): HTMLElement | null { + return getUntaintedAccessor('Node', n, 'parentElement'); +} + +export function textContent(n: Node): string | null { + return getUntaintedAccessor('Node', n, 'textContent'); +} + +export function contains(n: Node, other: Node): boolean { + return getUntaintedMethod('Node', n, 'contains')(other); +} + +export function getRootNode(n: Node): Node { + return getUntaintedMethod('Node', n, 'getRootNode')(); +} + +export function host(n: ShadowRoot): Element | null { + if (!n || !('host' in n)) return null; + return getUntaintedAccessor('ShadowRoot', n, 'host'); +} + +export function styleSheets(n: ShadowRoot): StyleSheetList { + return n.styleSheets; +} + +export function shadowRoot(n: Node): ShadowRoot | null { + if (!n || !('shadowRoot' in n)) return null; + return getUntaintedAccessor('Element', n as Element, 'shadowRoot'); +} + +export function querySelector(n: Element, selectors: string): Element | null { + return getUntaintedAccessor('Element', n, 'querySelector')(selectors); +} + +export function querySelectorAll( + n: Element, + selectors: string, +): NodeListOf { + return getUntaintedAccessor('Element', n, 'querySelectorAll')(selectors); +} + +export function mutationObserverCtor(): (typeof MutationObserver)['prototype']['constructor'] { + return getUntaintedPrototype('MutationObserver').constructor; +} + +export default { + childNodes, + parentNode, + parentElement, + textContent, + contains, + getRootNode, + host, + styleSheets, + shadowRoot, + querySelector, + querySelectorAll, + mutationObserver: mutationObserverCtor, +}; diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 0000000000..1902007d56 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "exclude": ["vite.config.ts"], + "compilerOptions": { + "rootDir": "src", + "tsBuildInfoFile": "./tsconfig.tsbuildinfo" + }, + "references": [] +} diff --git a/packages/utils/vite.config.js b/packages/utils/vite.config.js new file mode 100644 index 0000000000..854f2b9ef0 --- /dev/null +++ b/packages/utils/vite.config.js @@ -0,0 +1,4 @@ +import path from 'path'; +import config from '../../vite.config.default'; + +export default config(path.resolve(__dirname, 'src/index.ts'), 'rrwebUtils'); diff --git a/packages/web-extension/CHANGELOG.md b/packages/web-extension/CHANGELOG.md index 65ef125f1a..d35d11aedb 100644 --- a/packages/web-extension/CHANGELOG.md +++ b/packages/web-extension/CHANGELOG.md @@ -1,5 +1,35 @@ # @rrweb/web-extension +## 2.0.13 + +### Patch Changes + +- Merge from rrweb remote upstream + +- Updated dependencies []: + - @saola.ai/rrweb-player@2.0.13 + - @saola.ai/rrweb@2.0.13 + +## 2.0.0-alpha.17 + +### Minor Changes + +- [#1522](https://github.com/rrweb-io/rrweb/pull/1522) [`b1f9daa`](https://github.com/rrweb-io/rrweb/commit/b1f9daaa42f007a641104613bb07f141585f9e77) Thanks [@kirankunigiri](https://github.com/kirankunigiri)! - Added session downloader for chrome extension + +### Patch Changes + +- Updated dependencies [[`40bbc25`](https://github.com/rrweb-io/rrweb/commit/40bbc25fc287badc317a53f2d3f21b1c9f2b211b), [`68076b7`](https://github.com/rrweb-io/rrweb/commit/68076b724ff19d198d4f351a05063b85e1705a8c), [`8059d96`](https://github.com/rrweb-io/rrweb/commit/8059d9695146626b102b2059a3a9b932d5f598f6), [`335639a`](https://github.com/rrweb-io/rrweb/commit/335639af9b0ce7f70eb0f38ce113d877c7325158), [`be6bf52`](https://github.com/rrweb-io/rrweb/commit/be6bf52c248c35de1b3491e3a3440ff61f876414)]: + - rrweb@2.0.0-alpha.17 + - rrweb-player@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@2.0.0-alpha.16 + - rrweb-player@2.0.0-alpha.16 + ## 2.0.12 ### Patch Changes diff --git a/packages/web-extension/package.json b/packages/web-extension/package.json index 251ff17dec..9179a294be 100644 --- a/packages/web-extension/package.json +++ b/packages/web-extension/package.json @@ -1,7 +1,7 @@ { "name": "@saola.ai/rrweb-web-extension", "private": true, - "version": "2.0.12", + "version": "2.0.13", "description": "The web extension of rrweb which helps to run rrweb on any website out of box", "author": "rrweb-io", "license": "MIT", @@ -18,13 +18,12 @@ "prepublish": "yarn build" }, "devDependencies": { - "@saola.ai/rrweb-types": "^2.0.12", + "@saola.ai/rrweb-types": "^2.0.13", "@types/react-dom": "^18.0.6", "@types/webextension-polyfill": "^0.9.1", "@vitejs/plugin-react": "^4.2.1", - "cross-env": "^7.0.3", "type-fest": "^2.19.0", - "vite": "^5.2.8", + "vite": "^5.3.1", "vite-plugin-web-extension": "^4.1.3", "vite-plugin-zip-pack": "^1.2.2", "webextension-polyfill": "^0.10.0" @@ -42,7 +41,7 @@ "react-dom": "^18.2.0", "react-icons": "^4.4.0", "react-router-dom": "^6.4.1", - "@saola.ai/rrweb": "^2.0.12", - "@saola.ai/rrweb-player": "^2.0.12" + "@saola.ai/rrweb": "^2.0.13", + "@saola.ai/rrweb-player": "^2.0.13" } } diff --git a/packages/web-extension/src/background/index.ts b/packages/web-extension/src/background/index.ts index 9a0a7c58b3..4ffda7827c 100644 --- a/packages/web-extension/src/background/index.ts +++ b/packages/web-extension/src/background/index.ts @@ -2,11 +2,11 @@ import Browser from 'webextension-polyfill'; import type { eventWithTime } from '@saola.ai/rrweb-types'; import Channel from '~/utils/channel'; import { - LocalData, + type LocalData, LocalDataKey, RecorderStatus, - Settings, - SyncData, + type Settings, + type SyncData, SyncDataKey, } from '~/types'; import { pauseRecording, resumeRecording } from '~/utils/recording'; diff --git a/packages/web-extension/src/components/CircleButton.tsx b/packages/web-extension/src/components/CircleButton.tsx index abbb68e576..f114a1cb66 100644 --- a/packages/web-extension/src/components/CircleButton.tsx +++ b/packages/web-extension/src/components/CircleButton.tsx @@ -1,4 +1,4 @@ -import { Button, ButtonProps } from '@chakra-ui/react'; +import { Button, type ButtonProps } from '@chakra-ui/react'; interface CircleButtonProps extends ButtonProps { diameter: number; diff --git a/packages/web-extension/src/components/SidebarWithHeader.tsx b/packages/web-extension/src/components/SidebarWithHeader.tsx index bcc07147ff..7895efe613 100644 --- a/packages/web-extension/src/components/SidebarWithHeader.tsx +++ b/packages/web-extension/src/components/SidebarWithHeader.tsx @@ -12,8 +12,8 @@ import { Drawer, DrawerContent, useDisclosure, - BoxProps, - FlexProps, + type BoxProps, + type FlexProps, Heading, Stack, Text, diff --git a/packages/web-extension/src/content/index.ts b/packages/web-extension/src/content/index.ts index 18b35b2a31..673f8ad577 100644 --- a/packages/web-extension/src/content/index.ts +++ b/packages/web-extension/src/content/index.ts @@ -1,16 +1,16 @@ -import Browser, { Storage } from 'webextension-polyfill'; +import Browser, { type Storage } from 'webextension-polyfill'; import { nanoid } from 'nanoid'; import type { eventWithTime } from '@saola.ai/rrweb-types'; import { - LocalData, + type LocalData, LocalDataKey, RecorderStatus, ServiceName, - Session, - RecordStartedMessage, - RecordStoppedMessage, + type Session, + type RecordStartedMessage, + type RecordStoppedMessage, MessageName, - EmitEventMessage, + type EmitEventMessage, } from '~/types'; import Channel from '~/utils/channel'; import { isInCrossOriginIFrame } from '~/utils'; diff --git a/packages/web-extension/src/content/inject.ts b/packages/web-extension/src/content/inject.ts index 3ac619e642..ded4a76422 100644 --- a/packages/web-extension/src/content/inject.ts +++ b/packages/web-extension/src/content/inject.ts @@ -1,7 +1,7 @@ import { record } from '@saola.ai/rrweb'; import type { recordOptions } from '@saola.ai/rrweb'; import type { eventWithTime } from '@saola.ai/rrweb-types'; -import { MessageName, RecordStartedMessage } from '~/types'; +import { MessageName, type RecordStartedMessage } from '~/types'; import { isInCrossOriginIFrame } from '~/utils'; /** diff --git a/packages/web-extension/src/pages/SessionList.tsx b/packages/web-extension/src/pages/SessionList.tsx index 8aead1834f..d285932029 100644 --- a/packages/web-extension/src/pages/SessionList.tsx +++ b/packages/web-extension/src/pages/SessionList.tsx @@ -23,15 +23,15 @@ import { useReactTable, flexRender, getCoreRowModel, - SortingState, + type SortingState, getSortedRowModel, - PaginationState, + type PaginationState, } from '@tanstack/react-table'; import { VscTriangleDown, VscTriangleUp } from 'react-icons/vsc'; import { useNavigate } from 'react-router-dom'; -import { Session, EventName } from '~/types'; +import { type Session, EventName } from '~/types'; import Channel from '~/utils/channel'; -import { deleteSessions, getAllSessions } from '~/utils/storage'; +import { deleteSessions, getAllSessions, downloadSessions } from '~/utils/storage'; import { FiChevronLeft, FiChevronRight, @@ -292,24 +292,38 @@ export function SessionList() { ))} {Object.keys(rowSelection).length > 0 && ( - + + + + )} diff --git a/packages/web-extension/src/popup/App.tsx b/packages/web-extension/src/popup/App.tsx index a6bed7b3f2..c83a03ced4 100644 --- a/packages/web-extension/src/popup/App.tsx +++ b/packages/web-extension/src/popup/App.tsx @@ -10,16 +10,13 @@ import { } from '@chakra-ui/react'; import { FiSettings, FiList, FiPause, FiPlay } from 'react-icons/fi'; import Channel from '~/utils/channel'; -import { +import type { LocalData, - LocalDataKey, - RecorderStatus, - ServiceName, RecordStartedMessage, RecordStoppedMessage, Session, - EventName, } from '~/types'; +import { LocalDataKey, RecorderStatus, ServiceName, EventName } from '~/types'; import Browser from 'webextension-polyfill'; import { CircleButton } from '~/components/CircleButton'; import { Timer } from './Timer'; @@ -39,9 +36,8 @@ export function App() { void Browser.storage.local.get(LocalDataKey.recorderStatus).then((data) => { const localData = data as LocalData; if (!localData || !localData[LocalDataKey.recorderStatus]) return; - const { status, startTimestamp, pausedTimestamp } = localData[ - LocalDataKey.recorderStatus - ]; + const { status, startTimestamp, pausedTimestamp } = + localData[LocalDataKey.recorderStatus]; setStatus(status); if (startTimestamp && pausedTimestamp) setStartTime(Date.now() - pausedTimestamp + startTimestamp || 0); diff --git a/packages/web-extension/src/utils/channel.ts b/packages/web-extension/src/utils/channel.ts index 1a8e9b2a82..fc090bcc7f 100644 --- a/packages/web-extension/src/utils/channel.ts +++ b/packages/web-extension/src/utils/channel.ts @@ -1,5 +1,5 @@ import mitt from 'mitt'; -import Browser, { Runtime } from 'webextension-polyfill'; +import Browser, { type Runtime } from 'webextension-polyfill'; export type Message = EventType | ServiceType; export type EventType = { diff --git a/packages/web-extension/src/utils/recording.ts b/packages/web-extension/src/utils/recording.ts index 96f77c5da7..832c5a58fd 100644 --- a/packages/web-extension/src/utils/recording.ts +++ b/packages/web-extension/src/utils/recording.ts @@ -2,11 +2,11 @@ import Browser from 'webextension-polyfill'; import type { eventWithTime } from '@saola.ai/rrweb-types'; import { - LocalData, + type LocalData, LocalDataKey, RecorderStatus, - RecordStartedMessage, - RecordStoppedMessage, + type RecordStartedMessage, + type RecordStoppedMessage, ServiceName, } from '~/types'; import type Channel from './channel'; diff --git a/packages/web-extension/src/utils/storage.ts b/packages/web-extension/src/utils/storage.ts index 6c4c460ac8..e5fc05ab26 100644 --- a/packages/web-extension/src/utils/storage.ts +++ b/packages/web-extension/src/utils/storage.ts @@ -88,3 +88,22 @@ export async function deleteSessions(ids: string[]) { return Promise.all([eventTransition.done, sessionTransition.done]); }); } + +export async function downloadSessions(ids: string[]) { + for (const sessionId of ids) { + const events = await getEvents(sessionId); + const session = await getSession(sessionId); + const blob = new Blob([JSON.stringify({ session, events }, null, 2)], { + type: 'application/json', + }); + + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${session.name}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } +} diff --git a/packages/web-extension/vite.config.ts b/packages/web-extension/vite.config.ts index 0fd0602706..2ca58feab9 100644 --- a/packages/web-extension/vite.config.ts +++ b/packages/web-extension/vite.config.ts @@ -5,6 +5,8 @@ import * as path from 'path'; import type { PackageJson } from 'type-fest'; import react from '@vitejs/plugin-react'; +const emptyOutDir = !process.argv.includes('--watch'); + function useSpecialFormat( entriesToUse: string[], format: LibraryFormats, @@ -46,7 +48,7 @@ export default defineConfig({ 'dist', process.env.TARGET_BROWSER as string, ), - emptyOutDir: true, + emptyOutDir, }, // Add the webExtension plugin plugins: [ diff --git a/tsconfig.base.json b/tsconfig.base.json index 6ca3030761..d01ad45552 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,7 +14,7 @@ "sourceMap": true, "skipLibCheck": true, "declaration": true, - "importsNotUsedAsValues": "error", + "verbatimModuleSyntax": true, "strict": true, "removeComments": true, "noImplicitAny": true, diff --git a/turbo.json b/turbo.json index a48aa0dfc4..54bd1b278f 100644 --- a/turbo.json +++ b/turbo.json @@ -7,9 +7,10 @@ "vite.config.defaults.ts", "tsconfig.json" ], + "globalPassThroughEnv": ["PUPPETEER_HEADLESS"], "tasks": { "prepublish": { - "dependsOn": ["^prepublish"], + "dependsOn": ["^prepublish", "//#references:update"], "outputs": [ "lib/**", "es/**", @@ -20,24 +21,30 @@ ] }, "test": { - "dependsOn": ["^prepublish"], - "passThroughEnv": ["PUPPETEER_HEADLESS"] + "dependsOn": ["^prepublish"] }, "test:watch": { "persistent": true, - "passThroughEnv": ["PUPPETEER_HEADLESS"] + "cache": false }, "test:update": { - "dependsOn": ["^prepublish"], - "passThroughEnv": ["PUPPETEER_HEADLESS"] + "dependsOn": ["^prepublish"] }, "dev": { - // "dependsOn": ["^prepublish"], + "dependsOn": ["prepublish", "^prepublish"], "persistent": true, - "cache": false, - "passThroughEnv": ["CLEAR_DIST_DIR"] + "cache": false }, "lint": {}, - "check-types": {} + "check-types": { + "dependsOn": ["^prepublish"] + }, + "//#references:update": { + "inputs": ["packages/*/package.json", "packages/plugins/*/package.json"], + "outputs": [ + "packages/*/tsconfig.json", + "packages/plugins/*/tsconfig.json" + ] + } } } diff --git a/vite.config.default.ts b/vite.config.default.ts index 9bfb2fd017..4dd3cc496e 100644 --- a/vite.config.default.ts +++ b/vite.config.default.ts @@ -7,7 +7,8 @@ import { build, Format } from 'esbuild'; import { resolve } from 'path'; import { umdWrapper } from 'esbuild-plugin-umd-wrapper'; -const emptyOutDir = process.env.CLEAR_DIST_DIR !== 'false'; +// don't empty out dir if --watch flag is passed +const emptyOutDir = !process.argv.includes('--watch'); function minifyAndUMDPlugin({ name, diff --git a/yarn.lock b/yarn.lock index 56d58b1c1a..f68b2f59ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1956,11 +1956,16 @@ globby "^11.0.0" read-yaml-file "^1.1.0" -"@mdn/browser-compat-data@^5.2.34", "@mdn/browser-compat-data@^5.3.13": +"@mdn/browser-compat-data@^5.2.34": version "5.5.33" resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.5.33.tgz#c1177469bc4d39fa24c2cd3df317039e2b465b4c" integrity sha512-uO4uIBFn9D4UNyUmaueIWnE/IJhBlSJ7W1rANvDdaawhTX8CSgqUX8tj9/6a+1WjpL9Bgirf67d//S2VwDsfig== +"@mdn/browser-compat-data@^5.5.19": + version "5.6.12" + resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.6.12.tgz#ac3e1855c2387334bbfdb2b6249dd95c9d9c2b70" + integrity sha512-W/Km+GFczwpoimaXbtHYdjK26VHGszOEZ9EnIyLS2E65x6LEZs7r0FovR/XSkzgNau95sTxI3JfFKQFLIJE7EQ== + "@microsoft/api-extractor-model@7.28.13": version "7.28.13" resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.28.13.tgz#96fbc52155e0d07e0eabbd9699065b77702fe33a" @@ -2255,6 +2260,11 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== +"@rrweb/utils@^2.0.0-alpha.17": + version "2.0.0-alpha.17" + resolved "https://registry.yarnpkg.com/@rrweb/utils/-/utils-2.0.0-alpha.17.tgz#d13a7326af0311e0f54551e223ace987608eaed5" + integrity sha512-HCsasPERBwOS9/LQeOytO2ETKTCqRj1wORBuxiy3t41hKhmi225DdrUPiWnyDdTQm1GdVbOymMRknJVPnZaSXw== + "@rushstack/node-core-library@4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz#e26854a3314b279d57e8abdb4acce7797d02f554" @@ -3552,7 +3562,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.21.10, browserslist@^4.22.1, browserslist@^4.22.2: +browserslist@^4.22.1, browserslist@^4.22.2: version "4.23.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96" integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw== @@ -3562,6 +3572,16 @@ browserslist@^4.21.10, browserslist@^4.22.1, browserslist@^4.22.2: node-releases "^2.0.14" update-browserslist-db "^1.0.16" +browserslist@^4.23.0: + version "4.24.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" + integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + dependencies: + caniuse-lite "^1.0.30001669" + electron-to-chromium "^1.5.41" + node-releases "^2.0.18" + update-browserslist-db "^1.1.1" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -3677,7 +3697,12 @@ camelcase@^7.0.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048" integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw== -caniuse-lite@^1.0.30001524, caniuse-lite@^1.0.30001629: +caniuse-lite@^1.0.30001605, caniuse-lite@^1.0.30001669: + version "1.0.30001677" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz#27c2e2c637e007cfa864a16f7dfe7cde66b38b5f" + integrity sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog== + +caniuse-lite@^1.0.30001629: version "1.0.30001636" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78" integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg== @@ -4098,13 +4123,6 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-env@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.1.tgz#b2c76c1ca7add66dc874d11798466094f551b34d" - integrity sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ== - dependencies: - cross-spawn "^6.0.5" - cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -4135,17 +4153,6 @@ cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -4592,6 +4599,11 @@ electron-to-chromium@^1.4.796: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz#cf55808a5ee12e2a2778bbe8cdc941ef87c2093b" integrity sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g== +electron-to-chromium@^1.5.41: + version "1.5.50" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.50.tgz#d9ba818da7b2b5ef1f3dd32bce7046feb7e93234" + integrity sha512-eMVObiUQ2LdgeO1F/ySTXsvqvxb6ZH2zPGaMYsWzRDdOddUa77tdmI0ltg+L16UpbWdhPmuF3wIQYyQq65WfZw== + emittery@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" @@ -4789,6 +4801,11 @@ escalade@^3.1.1, escalade@^3.1.2: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-goat@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081" @@ -4832,18 +4849,19 @@ eslint-compat-utils@^0.5.1: dependencies: semver "^7.5.4" -eslint-plugin-compat@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-4.2.0.tgz#eeaf80daa1afe495c88a47e9281295acae45c0aa" - integrity sha512-RDKSYD0maWy5r7zb5cWQS+uSPc26mgOzdORJ8hxILmWM7S/Ncwky7BcAtXVY5iRbKjBdHsWU8Yg7hfoZjtkv7w== +eslint-plugin-compat@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-5.0.0.tgz#d09dff02397d81c9f5b1ac740ef45b39538aa21d" + integrity sha512-29KNWyFkUbNVf6TIKVe9SVCGCtHjML3HnUg9C8LG2GsXf7miAeBOgdMc1n2B5n0sHUzg1/A4IFly7Jyf1gSbgQ== dependencies: - "@mdn/browser-compat-data" "^5.3.13" + "@mdn/browser-compat-data" "^5.5.19" ast-metadata-inferer "^0.8.0" - browserslist "^4.21.10" - caniuse-lite "^1.0.30001524" + browserslist "^4.23.0" + caniuse-lite "^1.0.30001605" find-up "^5.0.0" + globals "^13.24.0" lodash.memoize "^4.1.2" - semver "^7.5.4" + semver "^7.6.0" eslint-plugin-jest@^27.6.0: version "27.9.0" @@ -5578,7 +5596,7 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.19.0: +globals@^13.19.0, globals@^13.24.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== @@ -5674,6 +5692,15 @@ growly@^1.3.0: resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw== +happy-dom@^14.12.0: + version "14.12.0" + resolved "https://registry.yarnpkg.com/happy-dom/-/happy-dom-14.12.0.tgz#40c748578c6ebfb707e6ae69179d6c541d8f63b3" + integrity sha512-dHcnlGFY2o2CdxfuYpqwSrBrpj/Kuzv4u4f3TU5yHW1GL24dKij4pv1BRjXnXc3uWo8qsCbToF9weaDsm/He8A== + dependencies: + entities "^4.5.0" + webidl-conversions "^7.0.0" + whatwg-mimetype "^3.0.0" + hard-rejection@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" @@ -7603,11 +7630,6 @@ netmask@^2.0.2: resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -7657,6 +7679,11 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -7968,11 +7995,6 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -8027,6 +8049,11 @@ picocolors@^1.0.0, picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -8841,7 +8868,7 @@ semver-match@0.1.1: dependencies: semver "^5.1.0" -"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.5.0: +"semver@2 || 3 || 4 || 5", semver@^5.1.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== @@ -9761,47 +9788,47 @@ tty-table@^4.1.5: wcwidth "^1.0.1" yargs "^17.7.1" -turbo-darwin-64@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.0.4.tgz#83c7835f8ba1f7a5473487ce73cfc8d5ad523614" - integrity sha512-x9mvmh4wudBstML8Z8IOmokLWglIhSfhQwnh2gBCSqabgVBKYvzl8Y+i+UCNPxheCGTgtsPepTcIaKBIyFIcvw== +turbo-darwin-64@2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.2.3.tgz#f0ced75ed031091e52851cbe8bb05d21a161a22b" + integrity sha512-Rcm10CuMKQGcdIBS3R/9PMeuYnv6beYIHqfZFeKWVYEWH69sauj4INs83zKMTUiZJ3/hWGZ4jet9AOwhsssLyg== -turbo-darwin-arm64@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.0.4.tgz#046e5768e9d6b490b7108d5bef3f4a1594aca0ba" - integrity sha512-/B1Ih8zPRGVw5vw4SlclOf3C/woJ/2T6ieH6u54KT4wypoaVyaiyMqBcziIXycdObIYr7jQ+raHO7q3mhay9/A== +turbo-darwin-arm64@2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.2.3.tgz#0b4741383ab5070d8383891a65861a8869cc7202" + integrity sha512-+EIMHkuLFqUdJYsA3roj66t9+9IciCajgj+DVek+QezEdOJKcRxlvDOS2BUaeN8kEzVSsNiAGnoysFWYw4K0HA== -turbo-linux-64@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.0.4.tgz#eab8c183a11b26ddec251d62778313a495971e4f" - integrity sha512-6aG670e5zOWu6RczEYcB81nEl8EhiGJEvWhUrnAfNEUIMBEH1pR5SsMmG2ol5/m3PgiRM12r13dSqTxCLcHrVg== +turbo-linux-64@2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.2.3.tgz#2b339db50c12bc52ce99139c156d5555717a209d" + integrity sha512-UBhJCYnqtaeOBQLmLo8BAisWbc9v9daL9G8upLR+XGj6vuN/Nz6qUAhverN4Pyej1g4Nt1BhROnj6GLOPYyqxQ== -turbo-linux-arm64@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.0.4.tgz#2dcc3f1d3e56209736b2ce3d849b80e0d7116e42" - integrity sha512-AXfVOjst+mCtPDFT4tCu08Qrfv12Nj7NDd33AjGwV79NYN1Y1rcFY59UQ4nO3ij3rbcvV71Xc+TZJ4csEvRCSg== +turbo-linux-arm64@2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.2.3.tgz#a4daf6e0872a4e2652e2d05d68ad18cee5b10e94" + integrity sha512-hJYT9dN06XCQ3jBka/EWvvAETnHRs3xuO/rb5bESmDfG+d9yQjeTMlhRXKrr4eyIMt6cLDt1LBfyi+6CQ+VAwQ== -turbo-windows-64@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.0.4.tgz#b2440d82892c983088ed386f9126d365594fc1a5" - integrity sha512-QOnUR9hKl0T5gq5h1fAhVEqBSjpcBi/BbaO71YGQNgsr6pAnCQdbG8/r3MYXet53efM0KTdOhieWeO3KLNKybA== +turbo-windows-64@2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.2.3.tgz#d44b3385948bd0f2ef5c2d53391f142bdd467b18" + integrity sha512-NPrjacrZypMBF31b4HE4ROg4P3nhMBPHKS5WTpMwf7wydZ8uvdEHpESVNMOtqhlp857zbnKYgP+yJF30H3N2dQ== -turbo-windows-arm64@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.0.4.tgz#e943709535baf233f5b85ed35cd95dcf86815283" - integrity sha512-3v8WpdZy1AxZw0gha0q3caZmm+0gveBQ40OspD6mxDBIS+oBtO5CkxhIXkFJJW+jDKmDlM7wXDIGfMEq+QyNCQ== +turbo-windows-arm64@2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.2.3.tgz#d0625ec53f467013a6f259f87f7fc4ae8670aaa4" + integrity sha512-fnNrYBCqn6zgKPKLHu4sOkihBI/+0oYFr075duRxqUZ+1aLWTAGfHZLgjVeLh3zR37CVzuerGIPWAEkNhkWEIw== -turbo@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.0.4.tgz#4fb6f0bf3be905953825de0368203e849c91e412" - integrity sha512-Ilme/2Q5kYw0AeRr+aw3s02+WrEYaY7U8vPnqSZU/jaDG/qd6jHVN6nRWyd/9KXvJGYM69vE6JImoGoyNjLwaw== +turbo@^2.0.4: + version "2.2.3" + resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.2.3.tgz#0f45612d62526c98c75da0682aa8c26b902b5e07" + integrity sha512-5lDvSqIxCYJ/BAd6rQGK/AzFRhBkbu4JHVMLmGh/hCb7U3CqSnr5Tjwfy9vc+/5wG2DJ6wttgAaA7MoCgvBKZQ== optionalDependencies: - turbo-darwin-64 "2.0.4" - turbo-darwin-arm64 "2.0.4" - turbo-linux-64 "2.0.4" - turbo-linux-arm64 "2.0.4" - turbo-windows-64 "2.0.4" - turbo-windows-arm64 "2.0.4" + turbo-darwin-64 "2.2.3" + turbo-darwin-arm64 "2.2.3" + turbo-linux-64 "2.2.3" + turbo-linux-arm64 "2.2.3" + turbo-windows-64 "2.2.3" + turbo-windows-arm64 "2.2.3" type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -9919,20 +9946,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@5.4.2: - version "5.4.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" - integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== - -typescript@^4.7.3, typescript@^4.9.0, typescript@^4.9.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== - -typescript@^5.0.3: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@5.4.2, typescript@^5.0.3, typescript@^5.4.5: + version "5.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" + integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" @@ -10017,6 +10034,14 @@ update-browserslist-db@^1.0.16: escalade "^3.1.2" picocolors "^1.0.1" +update-browserslist-db@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.0" + update-notifier@6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60" @@ -10125,7 +10150,7 @@ vite-node@1.6.0: picocolors "^1.0.0" vite "^5.0.0" -vite-plugin-dts@^3.8.1: +vite-plugin-dts@^3.8.1, vite-plugin-dts@^3.9.1: version "3.9.1" resolved "https://registry.yarnpkg.com/vite-plugin-dts/-/vite-plugin-dts-3.9.1.tgz#625ad388ec3956708ccec7960550a7b0a8e8909e" integrity sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg== @@ -10163,7 +10188,7 @@ vite-plugin-zip-pack@^1.2.2: dependencies: jszip "^3.10.1" -vite@^5.0.0, "vite@^5.0.0 || ^4.1.4", vite@^5.2.8: +vite@^5.0.0, "vite@^5.0.0 || ^4.1.4", vite@^5.2.8, vite@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.1.tgz#bb2ca6b5fd7483249d3e86b25026e27ba8a663e6" integrity sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ== @@ -10307,6 +10332,11 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" @@ -10319,6 +10349,11 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"