From 3376b307f87986d2c5eb10dcaf240f3947e97400 Mon Sep 17 00:00:00 2001 From: Jay Khatri Date: Mon, 14 Dec 2020 21:46:53 -0500 Subject: [PATCH] parent 5992f03c844fcb00268fe1b6c49648f701116e59 author Jay Khatri 1608000413 -0500 committer Vadim Korolik 1666033419 -0700 gpgsig -----BEGIN PGP SIGNATURE----- MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iQIyBAABCgAdFiEE5qJt+0KcVgDpzp9NmG2rb3pHHDQFAmNNpwsACgkQmG2rb3pH HDT+Qg/2Lwz671fehqmv/yC4LksqoSVdFH1JG0e61qKEaUqaCGST/c0GqUpHiI7D Y3yFTlhatbWqaDV+bpJ/9KzB3cRtOhV6URbw5xLrhsR3pGEzNqv4c+ZkaWR2U32k pSFidY7MZTJURbEosOR0cXc+WhHfiVEgzJ6/ny6e4r7zY6gBbE66DLVRNelhMwW7 wdpwzh4KcfLroOecA49skgvTj7xO3Bv3B5X2Ok2Rch18WzWiFDDVYFdIiArf7Kua /gIn1AybT6B6nb4kl99H763zATHLXKdWeLS9nFySGG73ILVpaefMS5cHZnmvmw10 6hltrcYpcZTbPJCV9npFUS+1ViSTK8FpegBVtlUZj5+ZhL3I+McBYyzjgrv/67vc fJKkVCgtgkEgbHHwouw4KnsyRZLcb4/0krOhyqdEKHxmEy+nNvIQ6OVyw4+vYW5B DN84bQ9wWC/p5wEdhJoEz7o42o2UYboGW9Gmzpsef7NaK0MUZr/XHMonOvwL9Yd+ STe9WjkVgWs3Ah6WQrXyYHDsxyn/PXCquSQK1qfnj5qSa3Aajrn1MHVYwNN1IdmL 4+Yn+JQe43Ahb3BEUVuD1w0V3cZs15UoHfKbNuc1VGKfmbvztu/48gwvUCE/BgkG E33iWf+6LcSjQJvEM8HHsBmMq64QOP2Tnsklfn/WJMZF41Pvbg== =P0Tg -----END PGP SIGNATURE----- add typings change change more changes to release change change try again try again try again try again change change change v0.9.12 change change added npm ignore change change chagne change change change change change Preprocess inactive segments Cleaned up code Renamed, and removed generated file changes change change change bump version delete old files change add typings change try again change another change change change change change Undef handling for intervals Another check Add custom build command (#21) Switch to version `1.0.4` of `rrweb-snapshot` (#23) Added support for inactive threshold (#20) * Added support for inactive threshold * Updated version num * Prettified * Fixed names * Fixed name * Renamed Missed an edge case where the last part of a session is inactive, and counted that as active. (#24) * Added support for inactive threshold * Updated version num * Prettified * Fixed names * Fixed name * Renamed * Cleaned code and added handling for edge cases * Updated package num Anthony/hig 156 change the color style of the cursor to (#25) * Added support for inactive threshold * Updated version num * Prettified * Fixed names * Fixed name * Renamed * Cleaned code and added handling for edge cases * Updated package num * More skip customize, changed cursor * Update package version * Changed to use webpack instead of rollup Co-authored-by: Jay Khatri * Fixed files * Added missing styles from CDN Co-authored-by: Jay Khatri Anthony/hig 190 move rrweb getactivityinterval logic (#27) * Added support for inactive threshold * Updated version num * Prettified * Fixed names * Fixed name * Renamed * Cleaned code and added handling for edge cases * Updated package num * More skip customize, changed cursor * Update package version * Changed to use webpack instead of rollup Co-authored-by: Jay Khatri * Fixed files * Added missing styles from CDN * Moved activity intervals calculation * Updated version Co-authored-by: Jay Khatri Anthony/hig 190 move rrweb getactivityinterval logic (#28) * Added support for inactive threshold * Updated version num * Prettified * Fixed names * Fixed name * Renamed * Cleaned code and added handling for edge cases * Updated package num * More skip customize, changed cursor * Update package version * Changed to use webpack instead of rollup Co-authored-by: Jay Khatri * Fixed files * Added missing styles from CDN * Moved activity intervals calculation * Updated version * Only calculate intervals once * Updated version Co-authored-by: Jay Khatri john/hig-231-block-recording-on-all-text-nodes (#30) * Adds text randomization * Update blocked styles * Add privacy mode * Don't record images * v0.10.0 * Rename flag to enableStrictPrivacy * Set redacted style on replayer side Consider allevents for inactivity (#31) * Consider allevents for inactivity * Remove log * skip inactive Don't record value attribute on password inputs (#32) * Don't record password * v0.10.2 Redact the value attribute on mutations (#33) * Redact the value attribute on mutations * v0.10.3 Added 2s delay (#34) * Added 2s delay * updated version * Changed back to fast_forwarding Update rrweb (#35) * fix: sometimes currentTime is smaller than the totalTime when player is finished (#445) plus: fix the problem that sometimes return value of getCurrentTime() is negative * Fix broken link to design docs * Update to fflate (#448) * Update to fflate * Update docs, bundler config * Scroll replayer iframe on firstFullsnapshot (#451) * upgrade snapshot * Release 0.9.12 * Protect against generation of no-change viewport resize events. (#454) I noticed 8 or 10 of these events being generated in a multi-tab browsing session on Chrome 87.0 on Win10. I'm speculating they were generated as a side effect of changing tabs but I can't recreate * fix #452 check isBlocked on add mutation's target * Release 0.9.13 * let mouse tail duration respect timer speed * clean addList when meet a corner case * fix #460 ignore added node that are not in document anymore * upgrade 0.9.14 * Release 0.9.14 * Tweaks to timings to get tests passing on my dev laptop (#466) * Tweaks to timings to get tests passing on my dev laptop - hopefully this makes tests more deterministic * Okay understand what's going on now that the test has run in the travis environment * fix #469 try to get original MutationObserver We found Angular's zone module will patch MutationObserver which make the browser hang in some scenarios. Reference: angular/angular#26948 * Discovered that the common case of mouse movement or scrolling happening during `takeFullSnapshot` was causing mutations to be immediately emitted, contrary to the goal of https://github.com/rrweb-io/rrweb/pull/385 (#470) * Don't remove the style attributes altogether from tests; they are an important part of the mutations (#468) These were removed in https://github.com/rrweb-io/rrweb/commit/8ed1c999cff657c84cdcb88a14ff977c573485a6 in order to smooth over differences in test environments so have maintained that by converting pixel values to 'Npx' (could also try rounding, but didn't attempt that) * read __rrMutationObserver from window * update guide (#483) * Fix RangeError: Maximum call stack size exceeded (#479) Saw this line cause issues in production, causing the following error: ``` RangeError Maximum call stack size exceeded ``` I believe this is caused by javascript engine max argument length - see note from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply#using_apply_and_built-in_functions > The consequences of applying a function with too many arguments (that is, more than tens of thousands of arguments) varies across engines. (The JavaScriptCore engine has hard-coded argument limit of 65536. * Impl record iframe (#481) * Impl record iframe * iframe observe * temp: add bundle file to git * update bundle * update with pick * update bundle * fix fragment map remove * feat: add an option to determine whether to pause CSS animation when playback is paused (#428) set pauseAnimation to true by default * fix: elements would lose some states like scroll position because of "virtual parent" optimization (#427) * fix: elements would lose some state like scroll position because of "virtual parent" optimization * refactor: the bugfix code bug: elements would lose some state like scroll position because of "virtual parent" optimization * fix: an error occured at applyMutation(remove nodes part) error message: Uncaught (in promise) DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node * pick fixes * revert ignore file * re-impl iframe record * re-impl iframe replay * code housekeeping * move multi layer dimension calculation to replay side * update test cases * teardown test server * upgrade rrweb-snapshot with iframe load timeout Co-authored-by: Lucky Feng * remove debugging warning (#486) I can't see a reason for the warning here so believe it's a debugging statement that crept in? * Add prettier as a dependency (#487) * start impl rrdom * upgrade rrweb-snapshot to 1.0.7 * Adding prepare npm statement (#490) * added prepare statement * using master rrweb snapshot Co-authored-by: filip slatinac * Added mousemoveCallback threshold option to sampling config. (#492) * Added mousemoveCallback threshold option to sampling config. * Added mousemoveCallback to definitions file. * Add yarn support for installing unreleased rrweb as a dependency (#497) * Use prepack instead of prepare for yarn support * add prepare and prepack for yarn v1 & v2 compatibility * Create .npmignore * update guide * close #501 do not count attach iframe event in checkout * close #491 check whether link node is head * update test snapshot * fix lint errors * add hiring link * impl #507 export takeFullSnapshot as a public API * Update observer.md (#504) Fixed some grammatical errors * add an experiment config to set max speed in fast forward * Handle event undefined in initMoveObserver (#515) * fix: errors of replaying iframe records (#520) * fix: errors of replaying iframe records error1: HierarchyRequestError: Failed to execute 'appendChild' on 'Node': Nodes of type '#document' may not be inserted inside nodes of type '#document-fragment'. code: parent.appendChild(target) error2: Uncaught DOMException: Failed to execute 'appendChild' on 'Node': Only one element on document allowed. code: parent.appendChild(target); * improve the comment for bugfix * rename node_modules in es bundle to ext * fix: inaccurate mouse position (#522) 1. Position of mouse was inaccurate when replaying and this PR will fix it. 2. Fix the bug that if one nested iframe has a scale transform and the position of mouse was inaccurate as well. * impl shadow DOM manager part of #38 1. observe DOM mutations in shadow DOM 2. rebuild DOM mutations in shadow DOM * Fix docs to point to correct event format (#523) * Fix docs to point to correct event attribute * Update customize-replayer.zh_CN.md * correct event object in guide * Update guide.zh_CN.md * Update snapshot to Release 1.1.1 Co-authored-by: Lucky Feng Co-authored-by: Fanis Katsimpas Co-authored-by: Lucky Feng Co-authored-by: 101arrowz Co-authored-by: Jarosław Salwa Co-authored-by: Yanzhen Yu Co-authored-by: Eoghan Murray Co-authored-by: zzq0826 <770166635@qq.com> Co-authored-by: Karl-Aksel Puulmann Co-authored-by: Moji Izadmehr Co-authored-by: Filip Slatinac Co-authored-by: filip slatinac Co-authored-by: Province Innovation <69924001+provinceinnovation@users.noreply.github.com> Co-authored-by: Justin Halsall Co-authored-by: Season Co-authored-by: arshabh-copods <77658085+arshabh-copods@users.noreply.github.com> Co-authored-by: Yakko Majuri <38760734+yakkomajuri@users.noreply.github.com> Disable onAddHoverClass and duplicate full snapshot (#36) * Don't rebuild full snapshot * Add config for hover class * Revert "Update rrweb (#35)" This reverts commit 5dc4ca2fe74c5c781707ed6e6d7416d1835214cd. * Bump version fix-rrweb-patch (#37) * Make check conditional * Update to 0.11.0 Bump (#38) Skip the first fullSnapshotBuild (#39) User events for inactivity calculation (#40) Make sure we publish a production build (#41) Pull in fix for #528 from rrweb (#42) Buffer modifications to virtual stylesheets (#43) Test rrweb 1.0.1 Co-authored-by: Lucky Feng Co-authored-by: filip slatinac Co-authored-by: zhaoziqiu Co-authored-by: yz-yu Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yash Kumar Co-authored-by: Lucky Feng <294889365@qq.com> Co-authored-by: Vladimir Milenko Co-authored-by: Eoghan Murray Co-authored-by: Fanis Katsimpas Co-authored-by: Lucky Feng Co-authored-by: 101arrowz Co-authored-by: Jarosław Salwa Co-authored-by: Yanzhen Yu Co-authored-by: zzq0826 <770166635@qq.com> Co-authored-by: Karl-Aksel Puulmann Co-authored-by: Moji Izadmehr Co-authored-by: Filip Slatinac Co-authored-by: Province Innovation <69924001+provinceinnovation@users.noreply.github.com> Co-authored-by: Justin Halsall Co-authored-by: Season Co-authored-by: arshabh-copods <77658085+arshabh-copods@users.noreply.github.com> Co-authored-by: Yakko Majuri <38760734+yakkomajuri@users.noreply.github.com> Co-authored-by: re-fort Co-authored-by: Ziqiu Zhao <39512431+ZzqiZQute@users.noreply.github.com> Co-authored-by: yashkumar18 Co-authored-by: bachmanity1 <81428651+bachmanity1@users.noreply.github.com> Co-authored-by: Omair Nabiel Copy https://github.com/rrweb-io/rrweb/pull/630 for DragEvent errors (#45) Record and replay nested stylesheet rules (#46) john/hig-1014-2nd-attempt-at-stitches-fix-in-rrweb (#47) Apply nested styles refactor https://github.com/rrweb-io/rrweb/pull/667/files john/hig-1046-apply-rrweb-css-change (#48) Update Mirror to support null IDs John/hig-1121-add-rrweb-patch-for-element-blocking (#49) Set the blockClass background color to the same as the color (#51) John/hig-1053-strictprivacy-mode-is-injecting-random (#52) gc virtual style map when DOM has been removed (#53) Monkeypatch each iframe (#54) Update rrweb to 1.0.7 (#55) Pause animations on pseudo elements (#56) Add webgl recording and playback (#57) Webgl-patches (#58) * Add diffs * Add missing changes * Bump version Patch webgl support for older Safari browsers Make sure WebGL instance exists before saving More patches for webgl recording Update mouse cursor to make it visible on dark and light backgrounds Bump version Support loading CORS fonts through our proxy (#59) John/hig-1919-update-rrweb (#60) * Port over remaining webgl changes * More updates * https://github.com/rrweb-io/rrweb/pull/810/files * https://github.com/rrweb-io/rrweb/pull/720/files * Update package version Make sure text mutations respect strict privacy mode (#61) John/hig-1930-sync-rrweb-changes (#62) * Sync change * Port fix: an error when I stop the recording process Change click behavior/visual (#63) Silence the "please add custom even after start recording" (#64) John/hig-1984-pull-in-rrweb-sequential-id-pr (#65) * Add sequential IDs * https://github.com/rrweb-io/rrweb/pull/840//files * bump version Export sequential ID plugin Ignore recording rrweb internal click events for canvas elements John/hig-1998-apply-rrweb-commits (#66) [HIG-2114] add replace events function for use by chunking (#67) bump version (#68) https://github.com/rrweb-io/rrweb/pull/866/files (#69) remove promise to resolve immediately (#70) baseline time check should include offset to avoid replaying past events (#71) v1.1.20 fix rollup config (#72) fix the bundle script so that we can use rrweb directly from a + + + + + + diff --git a/packages/rrweb-player/rollup.config.js b/packages/rrweb-player/rollup.config.js new file mode 100644 index 00000000..67aebc80 --- /dev/null +++ b/packages/rrweb-player/rollup.config.js @@ -0,0 +1,116 @@ +import svelte from 'rollup-plugin-svelte'; +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import livereload from 'rollup-plugin-livereload'; +import { terser } from 'rollup-plugin-terser'; +import sveltePreprocess from 'svelte-preprocess'; +import webWorkerLoader from 'rollup-plugin-web-worker-loader'; +import typescript from 'rollup-plugin-typescript2'; +import pkg from './package.json'; +import css from 'rollup-plugin-css-only'; + +// eslint-disable-next-line no-undef +const production = !process.env.ROLLUP_WATCH; + +const entries = (production + ? [ + { file: pkg.module, format: 'es', css: false }, + { file: pkg.main, format: 'cjs', css: false }, + { + file: pkg.unpkg, + format: 'iife', + name: 'rrwebPlayer', + css: 'style.css', + }, + ] + : [] +).concat([ + { + file: 'public/bundle.js', + format: 'iife', + name: 'rrwebPlayer', + css: 'bundle.css', + }, +]); + +export default entries.map((output) => ({ + input: 'src/main.ts', + output: { + file: output.file, + format: output.format, + name: output.name, + sourcemap: true, + exports: 'auto', + }, + plugins: [ + svelte({ + compilerOptions: { + // enable run-time checks when not in production + dev: !production, + }, + preprocess: sveltePreprocess({ + postcss: { + // eslint-disable-next-line no-undef + plugins: [require('postcss-easy-import')], + }, + }), + }), + + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration — + // consult the documentation for details: + // https://github.com/rollup/rollup-plugin-commonjs + resolve({ + browser: true, + dedupe: ['svelte'], + extensions: ['.js', '.ts', '.svelte'], + }), + + commonjs(), + + // supports bundling `web-worker:..filename` from rrweb + webWorkerLoader(), + + typescript(), + + css({ + // we'll extract any component CSS out into + // a separate file — better for performance + output: output.css, + }), + + // In dev mode, call `npm run start` once + // the bundle has been generated + !production && serve(), + + // Watch the `public` directory and refresh the + // browser on changes when not in production + !production && livereload('public'), + + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser(), + ], + watch: { + clearScreen: false, + }, +})); + +function serve() { + let started = false; + + return { + writeBundle() { + if (!started) { + started = true; + + // eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef + require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { + stdio: ['ignore', 'inherit', 'inherit'], + shell: true, + }); + } + }, + }; +} diff --git a/packages/rrweb-player/src/Controller.svelte b/packages/rrweb-player/src/Controller.svelte new file mode 100644 index 00000000..3d599a89 --- /dev/null +++ b/packages/rrweb-player/src/Controller.svelte @@ -0,0 +1,440 @@ + + + + +{#if showController} +
+
+ {formatTime(currentTime)} +
+
+ {#each customEvents as event} +
+ {/each} + +
+
+ {formatTime(meta.totalTime)} +
+
+ + {#each speedOption as s} + + {/each} + + +
+
+{/if} diff --git a/packages/rrweb-player/src/Player.svelte b/packages/rrweb-player/src/Player.svelte new file mode 100644 index 00000000..5c43565b --- /dev/null +++ b/packages/rrweb-player/src/Player.svelte @@ -0,0 +1,223 @@ + + + + +
+
+ {#if replayer} + toggleFullscreen()} /> + {/if} +
diff --git a/packages/rrweb-player/src/components/Switch.svelte b/packages/rrweb-player/src/components/Switch.svelte new file mode 100644 index 00000000..9aa68624 --- /dev/null +++ b/packages/rrweb-player/src/components/Switch.svelte @@ -0,0 +1,79 @@ + + + + +
+ +
diff --git a/packages/rrweb-player/src/main.ts b/packages/rrweb-player/src/main.ts new file mode 100644 index 00000000..23847f5e --- /dev/null +++ b/packages/rrweb-player/src/main.ts @@ -0,0 +1,22 @@ +import type { eventWithTime } from '@highlight-run/rrweb/typings/types'; +import _Player from './Player.svelte'; + +type PlayerProps = { + events: eventWithTime[]; +}; + +class Player extends _Player { + constructor(options: { + target: Element; + props: PlayerProps; + // for compatibility + data?: PlayerProps; + }) { + super({ + target: options.target, + props: options.data || options.props, + }); + } +} + +export default Player; diff --git a/packages/rrweb-player/src/utils.ts b/packages/rrweb-player/src/utils.ts new file mode 100644 index 00000000..32f1faf5 --- /dev/null +++ b/packages/rrweb-player/src/utils.ts @@ -0,0 +1,137 @@ +declare global { + interface Document { + mozExitFullscreen: Document['exitFullscreen']; + webkitExitFullscreen: Document['exitFullscreen']; + msExitFullscreen: Document['exitFullscreen']; + webkitIsFullScreen: Document['fullscreen']; + mozFullScreen: Document['fullscreen']; + msFullscreenElement: Document['fullscreen']; + } + + interface HTMLElement { + mozRequestFullScreen: Element['requestFullscreen']; + webkitRequestFullscreen: Element['requestFullscreen']; + msRequestFullscreen: Element['requestFullscreen']; + } +} + +export function inlineCss(cssObj: Record): string { + let style = ''; + Object.keys(cssObj).forEach((key) => { + style += `${key}: ${cssObj[key]};`; + }); + return style; +} + +function padZero(num: number, len = 2): string { + let str = String(num); + const threshold = Math.pow(10, len - 1); + if (num < threshold) { + while (String(threshold).length > str.length) { + str = `0${num}`; + } + } + return str; +} + +const SECOND = 1000; +const MINUTE = 60 * SECOND; +const HOUR = 60 * MINUTE; +export function formatTime(ms: number): string { + if (ms <= 0) { + return '00:00'; + } + const hour = Math.floor(ms / HOUR); + ms = ms % HOUR; + const minute = Math.floor(ms / MINUTE); + ms = ms % MINUTE; + const second = Math.floor(ms / SECOND); + if (hour) { + return `${padZero(hour)}:${padZero(minute)}:${padZero(second)}`; + } + return `${padZero(minute)}:${padZero(second)}`; +} + +export function openFullscreen(el: HTMLElement): Promise { + if (el.requestFullscreen) { + return el.requestFullscreen(); + } else if (el.mozRequestFullScreen) { + /* Firefox */ + return el.mozRequestFullScreen(); + } else if (el.webkitRequestFullscreen) { + /* Chrome, Safari and Opera */ + return el.webkitRequestFullscreen(); + } else if (el.msRequestFullscreen) { + /* IE/Edge */ + return el.msRequestFullscreen(); + } +} + +export function exitFullscreen(): Promise { + if (document.exitFullscreen) { + return document.exitFullscreen(); + } else if (document.mozExitFullscreen) { + /* Firefox */ + return document.mozExitFullscreen(); + } else if (document.webkitExitFullscreen) { + /* Chrome, Safari and Opera */ + return document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + /* IE/Edge */ + return document.msExitFullscreen(); + } +} + +export function isFullscreen(): boolean { + return ( + document.fullscreen || + document.webkitIsFullScreen || + document.mozFullScreen || + document.msFullscreenElement + ); +} + +export function onFullscreenChange(handler: () => unknown): () => void { + document.addEventListener('fullscreenchange', handler); + document.addEventListener('webkitfullscreenchange', handler); + document.addEventListener('mozfullscreenchange', handler); + document.addEventListener('MSFullscreenChange', handler); + + return () => { + document.removeEventListener('fullscreenchange', handler); + document.removeEventListener('webkitfullscreenchange', handler); + document.removeEventListener('mozfullscreenchange', handler); + document.removeEventListener('MSFullscreenChange', handler); + }; +} + +export function typeOf( + obj: unknown, +): + | 'boolean' + | 'number' + | 'string' + | 'function' + | 'array' + | 'date' + | 'regExp' + | 'undefined' + | 'null' + | 'object' { + // eslint-disable-next-line @typescript-eslint/unbound-method + const toString = Object.prototype.toString; + const map = { + '[object Boolean]': 'boolean', + '[object Number]': 'number', + '[object String]': 'string', + '[object Function]': 'function', + '[object Array]': 'array', + '[object Date]': 'date', + '[object RegExp]': 'regExp', + '[object Undefined]': 'undefined', + '[object Null]': 'null', + '[object Object]': 'object', + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return map[toString.call(obj)]; +} diff --git a/packages/rrweb-player/tsconfig.json b/packages/rrweb-player/tsconfig.json new file mode 100644 index 00000000..b049c15d --- /dev/null +++ b/packages/rrweb-player/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "include": ["src/**/*"], + "exclude": [ + "node_modules/*", + "__sapper__/*", + "public/*", + "../rrweb/src/record/workers/workers.d.ts" + ] +} diff --git a/packages/rrweb-player/typings/index.d.ts b/packages/rrweb-player/typings/index.d.ts new file mode 100755 index 00000000..17d1512c --- /dev/null +++ b/packages/rrweb-player/typings/index.d.ts @@ -0,0 +1,36 @@ +import { eventWithTime, playerConfig } from '@highlight-run/rrweb/typings/types'; +import { Replayer, mirror } from '@highlight-run/rrweb'; +import { SvelteComponent } from 'svelte'; + +export type RRwebPlayerOptions = { + target: HTMLElement; + props: { + events: eventWithTime[]; + width?: number; + height?: number; + autoPlay?: boolean; + speed?: number; + speedOption?: number[]; + showController?: boolean; + tags?: Record; + } & Partial; +}; + +export default class rrwebPlayer extends SvelteComponent { + constructor(options: RRwebPlayerOptions); + + addEventListener(event: string, handler: (params: any) => unknown): void; + + addEvent(event: eventWithTime): void; + getMetaData: Replayer['getMetaData']; + getReplayer: () => Replayer; + getMirror: () => typeof mirror; + + toggle: () => void; + setSpeed: (speed: number) => void; + toggleSkipInactive: () => void; + triggerResize: () => void; + play: () => void; + pause: () => void; + goto: (timeOffset: number, play?: boolean) => void; +} diff --git a/packages/rrweb-snapshot/.gitignore b/packages/rrweb-snapshot/.gitignore new file mode 100644 index 00000000..639e5dd1 --- /dev/null +++ b/packages/rrweb-snapshot/.gitignore @@ -0,0 +1,9 @@ +.vscode +node_modules +package-lock.json +build +dist +es +lib +temp +typings diff --git a/packages/rrweb-snapshot/.release-it.json b/packages/rrweb-snapshot/.release-it.json new file mode 100644 index 00000000..e76cbd8e --- /dev/null +++ b/packages/rrweb-snapshot/.release-it.json @@ -0,0 +1,9 @@ +{ + "non-interactive": true, + "hooks": { + "before:init": ["npm run bundle", "npm run typings"] + }, + "git": { + "requireCleanWorkingDir": false + } +} diff --git a/packages/rrweb-snapshot/LICENSE b/packages/rrweb-snapshot/LICENSE new file mode 100644 index 00000000..fce28eb8 --- /dev/null +++ b/packages/rrweb-snapshot/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Contributors (https://github.com/rrweb-io/rrweb/graphs/contributors) and SmartX Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/rrweb-snapshot/README.md b/packages/rrweb-snapshot/README.md new file mode 100644 index 00000000..596a52b7 --- /dev/null +++ b/packages/rrweb-snapshot/README.md @@ -0,0 +1,40 @@ +# rrweb-snapshot + +[![Build Status](https://travis-ci.org/rrweb-io/rrweb.svg?branch=master)](https://travis-ci.org/rrweb-io/rrweb) [![Join the chat at slack](https://img.shields.io/badge/slack-@rrweb-teal.svg?logo=slack)](https://join.slack.com/t/rrweb/shared_invite/zt-siwoc6hx-uWay3s2wyG8t5GpZVb8rWg) + +Snapshot the DOM into a stateful and serializable data structure. +Also, provide the ability to rebuild the DOM via snapshot. + +## API + +This module export following methods: + +### snapshot + +`snapshot` will traverse the DOM and return a stateful and serializable data structure which can represent the current DOM **view**. + +There are several things will be done during snapshot: + +1. Inline some DOM states into HTML attributes, e.g, HTMLInputElement's value. +2. Turn script tags into `noscript` tags to avoid scripts being executed. +3. Try to inline stylesheets to make sure local stylesheets can be used. +4. Make relative paths in href, src, CSS to be absolute paths. +5. Give an id to each Node, and return the id node map when snapshot finished. + +#### rebuild + +`rebuild` will build the DOM according to the taken snapshot. + +There are several things will be done during rebuild: + +1. Add data-rrid attribute if the Node is an Element. +2. Create some extra DOM node like text node to place inline CSS and some states. +3. Add data-extra-child-index attribute if Node has some extra child DOM. + +#### serializeNodeWithId + +`serializeNodeWithId` can serialize a node into snapshot format with id. + +#### buildNodeWithSN + +`buildNodeWithSN` will build DOM from serialized node and store serialized information in the `mirror.getMeta(node)`. diff --git a/packages/rrweb-snapshot/jest.config.js b/packages/rrweb-snapshot/jest.config.js new file mode 100644 index 00000000..9749329f --- /dev/null +++ b/packages/rrweb-snapshot/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/**.test.ts'], + globals: { + window: {} + } +}; diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json new file mode 100644 index 00000000..e2e7e4b3 --- /dev/null +++ b/packages/rrweb-snapshot/package.json @@ -0,0 +1,62 @@ +{ + "name": "@highlight-run/rrweb-snapshot", + "version": "1.1.31", + "description": "rrweb's component to take a snapshot of DOM, aka DOM serializer", + "scripts": { + "prepare": "npm run prepack", + "prepack": "npm run bundle && npm run typings", + "test": "jest", + "test:watch": "jest --watch", + "bundle": "rollup --config", + "bundle:es-only": "cross-env ES_ONLY=true rollup --config", + "dev": "yarn bundle:es-only --watch", + "typings": "tsc -d --declarationDir typings", + "prepublish": "npm run typings && npm run bundle", + "lint": "yarn eslint src" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rrweb-io/rrweb.git" + }, + "keywords": [ + "rrweb", + "snapshot", + "DOM" + ], + "main": "lib/rrweb-snapshot.js", + "module": "es/rrweb-snapshot.js", + "unpkg": "dist/rrweb-snapshot.js", + "typings": "typings/index.d.ts", + "files": [ + "dist", + "lib", + "es", + "typings" + ], + "author": "yanzhen@smartx.com", + "license": "MIT", + "bugs": { + "url": "https://github.com/rrweb-io/rrweb/issues" + }, + "homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme", + "devDependencies": { + "@types/chai": "^4.1.4", + "@types/jest": "^27.0.2", + "@types/jsdom": "^20.0.0", + "@types/node": "^10.11.3", + "@types/puppeteer": "^1.12.4", + "cross-env": "^5.2.0", + "jest": "^27.2.4", + "jest-snapshot": "^23.6.0", + "jsdom": "^16.4.0", + "puppeteer": "^1.15.0", + "rollup": "^2.45.2", + "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-typescript2": "^0.31.2", + "ts-jest": "^27.0.5", + "ts-node": "^7.0.1", + "tslib": "^1.9.3", + "typescript": "^4.7.3" + }, + "gitHead": "d5751f9e6c52a7734597c8595caa763d0f4dd4ad" +} diff --git a/packages/rrweb-snapshot/rollup.config.js b/packages/rrweb-snapshot/rollup.config.js new file mode 100644 index 00000000..b13f4478 --- /dev/null +++ b/packages/rrweb-snapshot/rollup.config.js @@ -0,0 +1,76 @@ +import typescript from 'rollup-plugin-typescript2'; +import { terser } from 'rollup-plugin-terser'; +import pkg from './package.json'; + +function toMinPath(path) { + return path.replace(/\.js$/, '.min.js'); +} + +let configs = [ + // ES module - for building rrweb + { + input: './src/index.ts', + plugins: [typescript()], + output: [ + { + format: 'esm', + file: pkg.module, + }, + ], + }, +]; +let extra_configs = [ + // browser + { + input: './src/index.ts', + plugins: [typescript()], + output: [ + { + name: 'rrwebSnapshot', + format: 'iife', + file: pkg.unpkg, + }, + ], + }, + { + input: './src/index.ts', + plugins: [typescript(), terser()], + output: [ + { + name: 'rrwebSnapshot', + format: 'iife', + file: toMinPath(pkg.unpkg), + sourcemap: true, + }, + ], + }, + // CommonJS + { + input: './src/index.ts', + plugins: [typescript()], + output: [ + { + format: 'cjs', + file: pkg.main, + }, + ], + }, + // ES module (packed) + { + input: './src/index.ts', + plugins: [typescript(), terser()], + output: [ + { + format: 'esm', + file: toMinPath(pkg.module), + sourcemap: true, + }, + ], + }, +]; + +if (!process.env.ES_ONLY) { + configs.push(...extra_configs); +} + +export default configs; diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts new file mode 100644 index 00000000..e646f58d --- /dev/null +++ b/packages/rrweb-snapshot/src/css.ts @@ -0,0 +1,911 @@ +/** + * 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 */ + +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 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 Node { + /** The part following @document. */ + document?: string; + /** The vendor prefix in @document, or undefined if there is none. */ + vendor?: string; + /** Array of nodes with the types rule, comment and any of the at-rule types. */ + rules?: Array; +} + +/** + * 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 interface Host extends Node { + /** Array of nodes with the types rule, comment and any of the at-rule types. */ + rules?: Array; +} + +/** + * 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 Node { + /** The part following @media. */ + media?: string; + /** Array of nodes with the types rule, comment and any of the at-rule types. */ + rules?: Array; +} + +/** + * 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 Node { + /** The part following @supports. */ + supports?: string; + /** Array of nodes with the types rule, comment and any of the at-rule types. */ + rules?: Array; +} + +/** All at-rules. */ +export type AtRule = + | Charset + | CustomMedia + | Document + | FontFace + | Host + | Import + | KeyFrames + | Media + | Namespace + | Page + | Supports; + +/** + * A collection of rules + */ +export interface StyleRules { + source?: string; + /** Array of nodes with the types rule, comment and any of the at-rule types. */ + rules: Array; + /** 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 = {}) { + /** + * 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 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; + } + } + + /** + * Non-enumerable source string + */ + + Position.prototype.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(); + + return { + type: 'stylesheet', + stylesheet: { + source: options.source, + rules: rulesList, + parsingErrors: errorsList, + }, + }; + } + + /** + * 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 !== false) { + 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 !== false) { + 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. + */ + + function selector() { + const m = match(/^([^{]+)/); + if (!m) { + return; + } + /* @fix Remove all comments from selectors + * http://ostermiller.org/findcomment.html */ + return trim(m[0]) + .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '') + .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, (m) => { + return m.replace(/,/g, '\u200C'); + }) + .split(/\s*(?![^(]*\)),\s*/) + .map((s) => { + return s.replace(/\u200C/g, ','); + }); + } + + /** + * 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*([^;]+);'); + return () => { + const pos = position(); + const m = match(re); + if (!m) { + return; + } + const ret: Record = { type: name }; + ret[name] = m[1].trim(); + return pos(ret); + }; + } + + /** + * 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) { + 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; +} diff --git a/packages/rrweb-snapshot/src/index.ts b/packages/rrweb-snapshot/src/index.ts new file mode 100644 index 00000000..82dd6a42 --- /dev/null +++ b/packages/rrweb-snapshot/src/index.ts @@ -0,0 +1,31 @@ +import snapshot, { + serializeNodeWithId, + transformAttribute, + visitSnapshot, + cleanupSnapshot, + needMaskingText, + classMatchesRegex, + IGNORED_NODE, +} from './snapshot'; +import rebuild, { + buildNodeWithSN, + addHoverClass, + createCache, +} from './rebuild'; +export * from './types'; +export * from './utils'; + +export { + snapshot, + serializeNodeWithId, + rebuild, + buildNodeWithSN, + addHoverClass, + createCache, + transformAttribute, + visitSnapshot, + cleanupSnapshot, + needMaskingText, + classMatchesRegex, + IGNORED_NODE, +}; diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts new file mode 100644 index 00000000..db2a48e3 --- /dev/null +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -0,0 +1,573 @@ +import { parse } from './css'; +import { + serializedNodeWithId, + NodeType, + tagMap, + elementNode, + BuildCache, + attributes, +} from './types'; +import { isElement, Mirror } from './utils'; + +const tagMap: tagMap = { + script: 'noscript', + // camel case svg element tag names + altglyph: 'altGlyph', + altglyphdef: 'altGlyphDef', + altglyphitem: 'altGlyphItem', + animatecolor: 'animateColor', + animatemotion: 'animateMotion', + animatetransform: 'animateTransform', + clippath: 'clipPath', + feblend: 'feBlend', + fecolormatrix: 'feColorMatrix', + fecomponenttransfer: 'feComponentTransfer', + fecomposite: 'feComposite', + feconvolvematrix: 'feConvolveMatrix', + fediffuselighting: 'feDiffuseLighting', + fedisplacementmap: 'feDisplacementMap', + fedistantlight: 'feDistantLight', + fedropshadow: 'feDropShadow', + feflood: 'feFlood', + fefunca: 'feFuncA', + fefuncb: 'feFuncB', + fefuncg: 'feFuncG', + fefuncr: 'feFuncR', + fegaussianblur: 'feGaussianBlur', + feimage: 'feImage', + femerge: 'feMerge', + femergenode: 'feMergeNode', + femorphology: 'feMorphology', + feoffset: 'feOffset', + fepointlight: 'fePointLight', + fespecularlighting: 'feSpecularLighting', + fespotlight: 'feSpotLight', + fetile: 'feTile', + feturbulence: 'feTurbulence', + foreignobject: 'foreignObject', + glyphref: 'glyphRef', + lineargradient: 'linearGradient', + radialgradient: 'radialGradient', +}; +function getTagName(n: elementNode): string { + let tagName = tagMap[n.tagName] ? tagMap[n.tagName] : n.tagName; + if (tagName === 'link' && n.attributes._cssText) { + tagName = 'style'; + } + 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 +} + +declare global { + interface Window { + HIG_CONFIGURATION?: { + enableOnHoverClass?: boolean; + }; + } +} + +const HOVER_SELECTOR = /([^\\]):hover/; +const HOVER_SELECTOR_GLOBAL = new RegExp(HOVER_SELECTOR.source, 'g'); +export function addHoverClass(cssText: string, cache: BuildCache): string { + if (!window?.HIG_CONFIGURATION?.enableOnHoverClass) { + return cssText; + } + const cachedStyle = cache?.stylesWithHoverClass.get(cssText); + if (cachedStyle) return cachedStyle; + + const ast = parse(cssText, { + silent: true, + }); + + if (!ast.stylesheet) { + return cssText; + } + + const selectors: string[] = []; + ast.stylesheet.rules.forEach((rule) => { + if ('selectors' in rule) { + (rule.selectors || []).forEach((selector: string) => { + if (HOVER_SELECTOR.test(selector)) { + selectors.push(selector); + } + }); + } + }); + + if (selectors.length === 0) { + return cssText; + } + + 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', + ); + + const result = cssText.replace(selectorMatcher, (selector) => { + const newSelector = selector.replace(HOVER_SELECTOR_GLOBAL, '$1.\\:hover'); + return `${selector}, ${newSelector}`; + }); + cache?.stylesWithHoverClass.set(cssText, result); + return result; +} + +export function createCache(): BuildCache { + const stylesWithHoverClass: Map = new Map(); + return { + stylesWithHoverClass, + }; +} + +/** + * `rr_` attributes are magic, they change some of the other attributes on the elements, + * so we need to parse them last so they can overwrite any conflicting attributes. + * + * @param attributes list of html attributes to be added to the element + * @returns attributes with rr_* attributes last in the array + */ +function sortAttributes(attributes: attributes): attributes { + // return attributes with rr_ prefix last + return Object.keys(attributes) + .sort((a, b) => { + if (a.startsWith('rr_') && !b.startsWith('rr_')) { + return 1; + } + if (b.startsWith('rr_') && !a.startsWith('rr_')) { + return -1; + } + return 0; + }) + .reduce((acc, key) => { + acc[key] = attributes[key]; + return acc; + }, {} as attributes); +} + +function buildNode( + n: serializedNodeWithId, + options: { + doc: Document; + hackCss: boolean; + cache: BuildCache; + }, +): Node | null { + const { doc, hackCss, cache } = options; + switch (n.type) { + case NodeType.Document: + return doc.implementation.createDocument(null, '', null); + case NodeType.DocumentType: + return doc.implementation.createDocumentType( + n.name || 'html', + n.publicId, + n.systemId, + ); + case NodeType.Element: { + const tagName = getTagName(n); + let node: Element; + if (n.isSVG) { + node = doc.createElementNS('http://www.w3.org/2000/svg', tagName); + } else { + node = doc.createElement(tagName); + } + for (const name in sortAttributes(n.attributes)) { + if (!Object.prototype.hasOwnProperty.call(n.attributes, name)) { + continue; + } + let value = n.attributes[name]; + if (tagName === 'option' && name === 'selected' && value === false) { + // legacy fix (TODO: if `value === false` can be generated for other attrs, + // should we also omit those other attrs from build ?) + continue; + } + value = + typeof value === 'boolean' || typeof value === 'number' ? '' : value; + // attribute names start with rr_ are internal attributes added by rrweb + if (!name.startsWith('rr_')) { + const isTextarea = tagName === 'textarea' && name === 'value'; + const isRemoteOrDynamicCss = + tagName === 'style' && name === '_cssText'; + if (isRemoteOrDynamicCss && hackCss) { + value = addHoverClass(value, cache); + + /** Start of Highlight */ + /** + * Find all remote fonts in the style tag. + * We need to find and replace the URLs with a proxy URL so we can bypass CORS. + */ + if (typeof value === 'string') { + const regex = /url\(\"https:\/\/\S*(.eot|.woff2|.ttf|.woff)\S*\"\)/gm; + let m; + const fontUrls: { originalUrl: string; proxyUrl: string }[] = []; + + const PROXY_URL = 'https://replay-cors-proxy.highlightrun.workers.dev' as const; + while ((m = regex.exec(value)) !== null) { + // This is necessary to avoid infinite loops with zero-width matches + if (m.index === regex.lastIndex) { + regex.lastIndex++; + } + + // The result can be accessed through the `m`-variable. + m.forEach((match, groupIndex) => { + if (groupIndex === 0) { + // Trim the start and end + // example: url("https://app.boardgent.com/fonts/MaterialIcons-Regular.53354891.woff2") + // gets trimmed to https://app.boardgent.com/fonts/MaterialIcons-Regular.53354891.woff2 + const url = match.slice(5, match.length - 2); + + fontUrls.push({ + originalUrl: url, + proxyUrl: url.replace(url, `${PROXY_URL}?url=${url}`), + }); + } + }); + } + + // Replace all references to the old URL to our proxy URL in the stylesheet. + fontUrls.forEach((urlPair) => { + value = (value as string).replace( + urlPair.originalUrl, + urlPair.proxyUrl, + ); + }); + } + /** End of Highlight */ + } + if (isTextarea || isRemoteOrDynamicCss) { + const child = doc.createTextNode(value); + // https://github.com/rrweb-io/rrweb/issues/112 + for (const c of Array.from(node.childNodes)) { + if (c.nodeType === node.TEXT_NODE) { + node.removeChild(c); + } + } + node.appendChild(child); + continue; + } + + try { + if (n.isSVG && name === 'xlink:href') { + node.setAttributeNS('http://www.w3.org/1999/xlink', name, value); + } else if ( + name === 'onload' || + name === 'onclick' || + name.substring(0, 7) === 'onmouse' + ) { + // Rename some of the more common atttributes from https://www.w3schools.com/tags/ref_eventattributes.asp + // as setting them triggers a console.error (which shows up despite the try/catch) + // Assumption: these attributes are not used to css + node.setAttribute('_' + name, value); + } else if ( + tagName === 'meta' && + n.attributes['http-equiv'] === 'Content-Security-Policy' && + name === 'content' + ) { + // If CSP contains style-src and inline-style is disabled, there will be an error "Refused to apply inline style because it violates the following Content Security Policy directive: style-src '*'". + // And the function insertStyleRules in rrweb replayer will throw an error "Uncaught TypeError: Cannot read property 'insertRule' of null". + node.setAttribute('csp-content', value); + continue; + } else if ( + tagName === 'link' && + n.attributes.rel === 'preload' && + n.attributes.as === 'script' + ) { + // ignore + } else if ( + tagName === 'link' && + n.attributes.rel === 'prefetch' && + typeof n.attributes.href === 'string' && + n.attributes.href.endsWith('.js') + ) { + // ignore + } else if ( + tagName === 'img' && + n.attributes.srcset && + n.attributes.rr_dataURL + ) { + // backup original img srcset + node.setAttribute( + 'rrweb-original-srcset', + n.attributes.srcset as string, + ); + } else { + node.setAttribute(name, value); + } + } catch (error) { + // skip invalid attribute + } + } else { + // handle internal attributes + if (tagName === 'canvas' && name === 'rr_dataURL') { + const image = document.createElement('img'); + image.onload = () => { + const ctx = (node as HTMLCanvasElement).getContext('2d'); + if (ctx) { + ctx.drawImage(image, 0, 0, image.width, image.height); + } + }; + image.src = value; + type RRCanvasElement = { + RRNodeType: NodeType; + rr_dataURL: string; + }; + // If the canvas element is created in RRDom runtime (seeking to a time point), the canvas context isn't supported. So the data has to be stored and not handled until diff process. https://github.com/rrweb-io/rrweb/pull/944 + if (((node as unknown) as RRCanvasElement).RRNodeType) + ((node as unknown) as RRCanvasElement).rr_dataURL = value; + } else if (tagName === 'img' && name === 'rr_dataURL') { + const image = node as HTMLImageElement; + if (!image.currentSrc.startsWith('data:')) { + // Backup original img src. It may not have been set yet. + image.setAttribute( + 'rrweb-original-src', + n.attributes.src as string, + ); + image.src = value; + image.setAttribute('rrweb-inline-src', value); + } + } + + if (name === 'rr_width') { + (node as HTMLElement).style.width = value; + } else if (name === 'rr_height') { + (node as HTMLElement).style.height = value; + } else if (name === 'rr_mediaCurrentTime') { + (node as HTMLMediaElement).currentTime = n.attributes + .rr_mediaCurrentTime as number; + } else if (name === 'rr_mediaState') { + switch (value) { + case 'played': + (node as HTMLMediaElement) + .play() + .catch((e) => console.warn('media playback error', e)); + break; + case 'paused': + (node as HTMLMediaElement).pause(); + break; + default: + } + } + } + } + + if (tagName === 'img') { + const image = node as HTMLImageElement; + if (!image.currentSrc.startsWith('data:')) { + const inlineSrc = image.getAttribute('rrweb-inline-src'); + if (inlineSrc?.startsWith('data:')) { + image.src = inlineSrc; + } + } + } + + if (n.isShadowHost) { + /** + * Since node is newly rebuilt, it should be a normal element + * without shadowRoot. + * But if there are some weird situations that has defined + * custom element in the scope before we rebuild node, it may + * register the shadowRoot earlier. + * The logic in the 'else' block is just a try-my-best solution + * for the corner case, please let we know if it is wrong and + * we can remove it. + */ + if (!node.shadowRoot) { + node.attachShadow({ mode: 'open' }); + } else { + while (node.shadowRoot.firstChild) { + node.shadowRoot.removeChild(node.shadowRoot.firstChild); + } + } + } + return node; + } + case NodeType.Text: + return doc.createTextNode( + n.isStyle && hackCss + ? addHoverClass(n.textContent, cache) + : n.textContent, + ); + case NodeType.CDATA: + return doc.createCDATASection(n.textContent); + case NodeType.Comment: + return doc.createComment(n.textContent); + default: + return null; + } +} + +export function buildNodeWithSN( + n: serializedNodeWithId, + options: { + doc: Document; + mirror: Mirror; + skipChild?: boolean; + hackCss: boolean; + afterAppend?: (n: Node) => unknown; + cache: BuildCache; + }, +): Node | null { + const { + doc, + mirror, + skipChild = false, + hackCss = true, + afterAppend, + cache, + } = options; + let node = buildNode(n, { doc, hackCss, cache }); + if (!node) { + return null; + } + if (n.rootId) { + console.assert( + (mirror.getNode(n.rootId) as Document) === doc, + 'Target document should have the same root id.', + ); + } + // use target document as root document + if (n.type === NodeType.Document) { + // close before open to make sure document was closed + doc.close(); + doc.open(); + if ( + n.compatMode === 'BackCompat' && + n.childNodes && + n.childNodes[0].type !== NodeType.DocumentType // there isn't one already defined + ) { + // Trigger compatMode in the iframe + // this is needed as document.createElement('iframe') otherwise inherits a CSS1Compat mode from the parent replayer environment + if ( + n.childNodes[0].type === NodeType.Element && + 'xmlns' in n.childNodes[0].attributes && + n.childNodes[0].attributes.xmlns === 'http://www.w3.org/1999/xhtml' + ) { + // might as well use an xhtml doctype if we've got an xhtml namespace + doc.write( + '', + ); + } else { + doc.write( + '', + ); + } + } + node = doc; + } + + mirror.add(node, n); + + if ( + (n.type === NodeType.Document || n.type === NodeType.Element) && + !skipChild + ) { + for (const childN of n.childNodes) { + const childNode = buildNodeWithSN(childN, { + doc, + mirror, + skipChild: false, + hackCss, + afterAppend, + cache, + }); + if (!childNode) { + console.warn('Failed to rebuild', childN); + continue; + } + + if (childN.isShadow && isElement(node) && node.shadowRoot) { + node.shadowRoot.appendChild(childNode); + } else { + node.appendChild(childNode); + } + if (afterAppend) { + afterAppend(childNode); + } + } + } + + return node; +} + +function visit(mirror: Mirror, onVisit: (node: Node) => void) { + function walk(node: Node) { + onVisit(node); + } + + for (const id of mirror.getIds()) { + if (mirror.has(id)) { + walk(mirror.getNode(id)!); + } + } +} + +function handleScroll(node: Node, mirror: Mirror) { + const n = mirror.getMeta(node); + if (n?.type !== NodeType.Element) { + return; + } + const el = node as HTMLElement; + for (const name in n.attributes) { + if ( + !( + Object.prototype.hasOwnProperty.call(n.attributes, name) && + name.startsWith('rr_') + ) + ) { + continue; + } + const value = n.attributes[name]; + if (name === 'rr_scrollLeft') { + el.scrollLeft = value as number; + } + if (name === 'rr_scrollTop') { + el.scrollTop = value as number; + } + } +} + +function rebuild( + n: serializedNodeWithId, + options: { + doc: Document; + onVisit?: (node: Node) => unknown; + hackCss?: boolean; + afterAppend?: (n: Node) => unknown; + cache: BuildCache; + mirror: Mirror; + }, +): Node | null { + const { + doc, + onVisit, + hackCss = true, + afterAppend, + cache, + mirror = new Mirror(), + } = options; + const node = buildNodeWithSN(n, { + doc, + mirror, + skipChild: false, + hackCss, + afterAppend, + cache, + }); + visit(mirror, (visitedNode) => { + if (onVisit) { + onVisit(visitedNode); + } + handleScroll(visitedNode, mirror); + }); + return node; +} + +export default rebuild; diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts new file mode 100644 index 00000000..95ebd45e --- /dev/null +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -0,0 +1,1472 @@ +import { + serializedNode, + serializedNodeWithId, + NodeType, + attributes, + MaskInputOptions, + SlimDOMOptions, + DataURLOptions, + MaskTextFn, + MaskInputFn, + KeepIframeSrcFn, + ICanvas, + serializedElementNodeWithId, +} from './types'; +import { + Mirror, + is2DCanvasBlank, + isElement, + isShadowRoot, + maskInputValue, + obfuscateText, + isNativeShadowDom, +} from './utils'; + +let _id = 1; +const tagNameRegex = new RegExp('[^a-z0-9-_:]'); + +export const IGNORED_NODE = -2; + +function genId(): number { + return _id++; +} + +function getValidTagName(element: HTMLElement): string { + if (element instanceof HTMLFormElement) { + return 'form'; + } + + const processedTagName = element.tagName.toLowerCase().trim(); + + if (tagNameRegex.test(processedTagName)) { + // if the tag name is odd and we cannot extract + // anything from the string, then we return a + // generic div + return 'div'; + } + + return processedTagName; +} + +function getCssRulesString(s: CSSStyleSheet): string | null { + try { + const rules = s.rules || s.cssRules; + return rules ? Array.from(rules).map(getCssRuleString).join('') : null; + } catch (error) { + return null; + } +} + +function getCssRuleString(rule: CSSRule): string { + let cssStringified = rule.cssText; + if (isCSSImportRule(rule)) { + try { + cssStringified = getCssRulesString(rule.styleSheet) || cssStringified; + } catch { + // ignore + } + } + return cssStringified; +} + +function isCSSImportRule(rule: CSSRule): rule is CSSImportRule { + return 'styleSheet' in rule; +} + +function stringifyStyleSheet(sheet: CSSStyleSheet): string { + return sheet.cssRules + ? Array.from(sheet.cssRules) + .map((rule) => rule.cssText || '') + .join('') + : ''; +} + +function extractOrigin(url: string): string { + let origin = ''; + if (url.indexOf('//') > -1) { + origin = url.split('/').slice(0, 3).join('/'); + } else { + origin = url.split('/')[0]; + } + origin = origin.split('?')[0]; + return origin; +} + +let canvasService: HTMLCanvasElement | null; +let canvasCtx: CanvasRenderingContext2D | null; + +const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; +const RELATIVE_PATH = /^(?!www\.|(?:http|ftp)s?:\/\/|[A-Za-z]:\\|\/\/|#).*/; +const DATA_URI = /^(data:)([^,]*),(.*)/i; +export function absoluteToStylesheet( + cssText: string | null, + href: string, +): string { + return (cssText || '').replace( + URL_IN_CSS_REF, + ( + origin: string, + quote1: string, + path1: string, + quote2: string, + path2: string, + path3: string, + ) => { + const filePath = path1 || path2 || path3; + const maybeQuote = quote1 || quote2 || ''; + if (!filePath) { + return origin; + } + if (!RELATIVE_PATH.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (DATA_URI.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (filePath[0] === '/') { + return `url(${maybeQuote}${ + extractOrigin(href) + filePath + }${maybeQuote})`; + } + const stack = href.split('/'); + const parts = filePath.split('/'); + stack.pop(); + for (const part of parts) { + if (part === '.') { + continue; + } else if (part === '..') { + stack.pop(); + } else { + stack.push(part); + } + } + return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; + }, + ); +} + +// eslint-disable-next-line no-control-regex +const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; // Don't use \s, to avoid matching non-breaking space +// eslint-disable-next-line no-control-regex +const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/; +function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { + /* + run absoluteToDoc over every url in the srcset + + this is adapted from https://github.com/albell/parse-srcset/ + without the parsing of the descriptors (we return these as-is) + parce-srcset is in turn based on + https://html.spec.whatwg.org/multipage/embedded-content.html#parse-a-srcset-attribute + */ + if (attributeValue.trim() === '') { + return attributeValue; + } + + let pos = 0; + + function collectCharacters(regEx: RegExp) { + let chars: string; + const match = regEx.exec(attributeValue.substring(pos)); + if (match) { + chars = match[0]; + pos += chars.length; + return chars; + } + return ''; + } + + const output = []; + // eslint-disable-next-line no-constant-condition + while (true) { + collectCharacters(SRCSET_COMMAS_OR_SPACES); + if (pos >= attributeValue.length) { + break; + } + // don't split on commas within urls + let url = collectCharacters(SRCSET_NOT_SPACES); + if (url.slice(-1) === ',') { + // aside: according to spec more than one comma at the end is a parse error, but we ignore that + url = absoluteToDoc(doc, url.substring(0, url.length - 1)); + // the trailing comma splits the srcset, so the interpretion is that + // another url will follow, and the descriptor is empty + output.push(url); + } else { + let descriptorsStr = ''; + url = absoluteToDoc(doc, url); + let inParens = false; + // eslint-disable-next-line no-constant-condition + while (true) { + const c = attributeValue.charAt(pos); + if (c === '') { + output.push((url + descriptorsStr).trim()); + break; + } else if (!inParens) { + if (c === ',') { + pos += 1; + output.push((url + descriptorsStr).trim()); + break; // parse the next url + } else if (c === '(') { + inParens = true; + } + } else { + // in parenthesis; ignore commas + // (parenthesis may be supported by future additions to spec) + if (c === ')') { + inParens = false; + } + } + descriptorsStr += c; + pos += 1; + } + } + } + return output.join(', '); +} + +export function absoluteToDoc(doc: Document, attributeValue: string): string { + if (!attributeValue || attributeValue.trim() === '') { + return attributeValue; + } + const a: HTMLAnchorElement = doc.createElement('a'); + a.href = attributeValue; + return a.href; +} + +function isSVGElement(el: Element): boolean { + return Boolean(el.tagName === 'svg' || (el as SVGElement).ownerSVGElement); +} + +function getHref() { + // return a href without hash + const a = document.createElement('a'); + a.href = ''; + return a.href; +} + +export function transformAttribute( + doc: Document, + tagName: string, + name: string, + value: string, +): string { + // relative path in attribute + if ( + name === 'src' || + (name === 'href' && value && !(tagName === 'use' && value[0] === '#')) + ) { + // href starts with a # is an id pointer for svg + return absoluteToDoc(doc, value); + } else if (name === 'xlink:href' && value && value[0] !== '#') { + // xlink:href starts with # is an id pointer + return absoluteToDoc(doc, value); + } else if ( + name === 'background' && + value && + (tagName === 'table' || tagName === 'td' || tagName === 'th') + ) { + return absoluteToDoc(doc, value); + } else if (name === 'srcset' && value) { + return getAbsoluteSrcsetString(doc, value); + } else if (name === 'style' && value) { + return absoluteToStylesheet(value, getHref()); + } else if (tagName === 'object' && name === 'data' && value) { + return absoluteToDoc(doc, value); + } else { + return value; + } +} + +export function _isBlockedElement( + element: HTMLElement, + blockClass: string | RegExp, + blockSelector: string | null, +): boolean { + if (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; + } + } else { + for (let eIndex = element.classList.length; eIndex--; ) { + const className = element.classList[eIndex]; + if (blockClass.test(className)) { + return true; + } + } + } + if (blockSelector) { + return element.matches(blockSelector); + } + + return false; +} + +export function classMatchesRegex( + node: Node | null, + regex: RegExp, + checkAncestors: boolean, +): boolean { + if (!node) return false; + if (node.nodeType !== node.ELEMENT_NODE) { + if (!checkAncestors) return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); + } + + for (let eIndex = (node as HTMLElement).classList.length; eIndex--; ) { + const className = (node as HTMLElement).classList[eIndex]; + if (regex.test(className)) { + return true; + } + } + if (!checkAncestors) return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); +} + +export function needMaskingText( + node: Node, + maskTextClass: string | RegExp, + maskTextSelector: string | null, +): boolean { + const el: HTMLElement | null = + node.nodeType === node.ELEMENT_NODE + ? (node as HTMLElement) + : node.parentElement; + if (el === null) return false; + + if (typeof maskTextClass === 'string') { + if (el.classList.contains(maskTextClass)) return true; + if (el.closest(`.${maskTextClass}`)) return true; + } else { + if (classMatchesRegex(el, maskTextClass, true)) return true; + } + + if (maskTextSelector) { + if (el.matches(maskTextSelector)) return true; + if (el.closest(maskTextSelector)) return true; + } + return false; +} + +// https://stackoverflow.com/a/36155560 +function onceIframeLoaded( + iframeEl: HTMLIFrameElement, + listener: () => unknown, + iframeLoadTimeout: number, +) { + const win = iframeEl.contentWindow; + if (!win) { + return; + } + // document is loading + let fired = false; + + let readyState: DocumentReadyState; + try { + readyState = win.document.readyState; + } catch (error) { + return; + } + if (readyState !== 'complete') { + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, iframeLoadTimeout); + iframeEl.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + return; + } + // check blank frame for Chrome + const blankUrl = 'about:blank'; + if ( + win.location.href !== blankUrl || + iframeEl.src === blankUrl || + iframeEl.src === '' + ) { + // iframe was already loaded, make sure we wait to trigger the listener + // till _after_ the mutation that found this iframe has had time to process + setTimeout(listener, 0); + + return iframeEl.addEventListener('load', listener); // keep listing for future loads + } + // use default listener + iframeEl.addEventListener('load', listener); +} + +function isStylesheetLoaded(link: HTMLLinkElement) { + if (!link.getAttribute('href')) return true; // nothing to load + return link.sheet !== null; +} + +function onceStylesheetLoaded( + link: HTMLLinkElement, + listener: () => unknown, + styleSheetLoadTimeout: number, +) { + let fired = false; + let styleSheetLoaded: StyleSheet | null; + try { + styleSheetLoaded = link.sheet; + } catch (error) { + return; + } + + if (styleSheetLoaded) return; + + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, styleSheetLoadTimeout); + + link.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); +} + +function serializeNode( + n: Node, + options: { + doc: Document; + mirror: Mirror; + blockClass: string | RegExp; + blockSelector: string | null; + maskTextClass: string | RegExp; + maskTextSelector: string | null; + inlineStylesheet: boolean; + maskInputOptions: MaskInputOptions; + maskTextFn: MaskTextFn | undefined; + maskInputFn: MaskInputFn | undefined; + dataURLOptions?: DataURLOptions; + inlineImages: boolean; + recordCanvas: boolean; + keepIframeSrcFn: KeepIframeSrcFn; + /** + * `newlyAddedElement: true` skips scrollTop and scrollLeft check + */ + newlyAddedElement?: boolean; + /** Highlight Options Start */ + enableStrictPrivacy: boolean; + /** Highlight Options End */ + }, +): serializedNode | false { + const { + doc, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskInputOptions = {}, + maskTextFn, + maskInputFn, + dataURLOptions = {}, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement = false, + enableStrictPrivacy, + } = options; + // Only record root id when document object is not the base document + const rootId = getRootId(doc, mirror); + switch (n.nodeType) { + case n.DOCUMENT_NODE: + if ((n as Document).compatMode !== 'CSS1Compat') { + return { + type: NodeType.Document, + childNodes: [], + compatMode: (n as Document).compatMode, // probably "BackCompat" + rootId, + }; + } else { + return { + type: NodeType.Document, + childNodes: [], + rootId, + }; + } + case n.DOCUMENT_TYPE_NODE: + return { + type: NodeType.DocumentType, + name: (n as DocumentType).name, + publicId: (n as DocumentType).publicId, + systemId: (n as DocumentType).systemId, + rootId, + }; + case n.ELEMENT_NODE: + return serializeElementNode(n as HTMLElement, { + doc, + blockClass, + blockSelector, + inlineStylesheet, + maskInputOptions, + maskInputFn, + maskTextClass, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + enableStrictPrivacy, + rootId, + }); + case n.TEXT_NODE: + return serializeTextNode(n as Text, { + maskTextClass, + maskTextSelector, + maskTextFn, + enableStrictPrivacy, + rootId, + }); + case n.CDATA_SECTION_NODE: + return { + type: NodeType.CDATA, + textContent: '', + rootId, + }; + case n.COMMENT_NODE: + return { + type: NodeType.Comment, + textContent: (n as Comment).textContent || '', + rootId, + }; + default: + return false; + } +} + +function getRootId(doc: Document, mirror: Mirror): number | undefined { + if (!mirror.hasNode(doc)) return undefined; + const docId = mirror.getId(doc); + return docId === 1 ? undefined : docId; +} + +function serializeTextNode( + n: Text, + options: { + maskTextClass: string | RegExp; + maskTextSelector: string | null; + maskTextFn: MaskTextFn | undefined; + enableStrictPrivacy: boolean; + rootId: number | undefined; + }, +): serializedNode { + const { + maskTextClass, + maskTextSelector, + maskTextFn, + enableStrictPrivacy, + rootId, + } = options; + // The parent node may not be a html element which has a tagName attribute. + // So just let it be undefined which is ok in this use case. + const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName; + let textContent = n.textContent; + const isStyle = parentTagName === 'STYLE' ? true : undefined; + const isScript = parentTagName === 'SCRIPT' ? true : undefined; + /** Determines if this node has been handled already. */ + let textContentHandled = false; + if (isStyle && textContent) { + try { + // try to read style sheet + if (n.nextSibling || n.previousSibling) { + // This is not the only child of the stylesheet. + // We can't read all of the sheet's .cssRules and expect them + // to _only_ include the current rule(s) added by the text node. + // So we'll be conservative and keep textContent as-is. + } else if ((n.parentNode as HTMLStyleElement).sheet?.cssRules) { + textContent = stringifyStyleSheet( + (n.parentNode as HTMLStyleElement).sheet!, + ); + } + } catch (err) { + console.warn( + `Cannot get CSS styles from text's parentNode. Error: ${err as string}`, + n, + ); + } + textContent = absoluteToStylesheet(textContent, getHref()); + textContentHandled = true; + } + if (isScript) { + textContent = 'SCRIPT_PLACEHOLDER'; + textContentHandled = true; + } else if (parentTagName === 'NOSCRIPT') { + textContent = ''; + textContentHandled = true; + } + if ( + !isStyle && + !isScript && + textContent && + needMaskingText(n, maskTextClass, maskTextSelector) + ) { + textContent = maskTextFn + ? maskTextFn(textContent) + : textContent.replace(/[\S]/g, '*'); + } + + /* Start of Highlight */ + // Randomizes the text content to a string of the same length. + if (enableStrictPrivacy && !textContentHandled && parentTagName) { + const IGNORE_TAG_NAMES = new Set([ + 'HEAD', + 'TITLE', + 'STYLE', + 'SCRIPT', + 'HTML', + 'BODY', + 'NOSCRIPT', + ]); + if (!IGNORE_TAG_NAMES.has(parentTagName) && textContent) { + textContent = obfuscateText(textContent); + } + } + /* End of Highlight */ + + return { + type: NodeType.Text, + textContent: textContent || '', + isStyle, + rootId, + }; +} + +function serializeElementNode( + n: HTMLElement, + options: { + doc: Document; + blockClass: string | RegExp; + blockSelector: string | null; + inlineStylesheet: boolean; + maskInputOptions: MaskInputOptions; + maskInputFn: MaskInputFn | undefined; + maskTextClass: string | RegExp; + dataURLOptions?: DataURLOptions; + inlineImages: boolean; + recordCanvas: boolean; + keepIframeSrcFn: KeepIframeSrcFn; + /** + * `newlyAddedElement: true` skips scrollTop and scrollLeft check + */ + newlyAddedElement?: boolean; + enableStrictPrivacy: boolean; + rootId: number | undefined; + }, +): serializedNode | false { + const { + doc, + blockClass, + blockSelector, + inlineStylesheet, + maskInputOptions = {}, + maskInputFn, + maskTextClass, + dataURLOptions = {}, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement = false, + enableStrictPrivacy, + rootId, + } = options; + let needBlock = _isBlockedElement(n, blockClass, blockSelector); + const needMask = _isBlockedElement(n, maskTextClass, null); + const tagName = getValidTagName(n); + let attributes: attributes = {}; + const len = n.attributes.length; + for (let i = 0; i < len; i++) { + const attr = n.attributes[i]; + attributes[attr.name] = transformAttribute( + doc, + tagName, + attr.name, + attr.value, + ); + } + // remote css + if (tagName === 'link' && inlineStylesheet) { + const stylesheet = Array.from(doc.styleSheets).find((s) => { + return s.href === (n as HTMLLinkElement).href; + }); + let cssText: string | null = null; + if (stylesheet) { + cssText = getCssRulesString(stylesheet); + } + if (cssText) { + delete attributes.rel; + delete attributes.href; + attributes._cssText = absoluteToStylesheet(cssText, stylesheet!.href!); + } + } + // dynamic stylesheet + if ( + tagName === 'style' && + (n as HTMLStyleElement).sheet && + // TODO: Currently we only try to get dynamic stylesheet when it is an empty style element + !(n.innerText || n.textContent || '').trim().length + ) { + const cssText = getCssRulesString( + (n as HTMLStyleElement).sheet as CSSStyleSheet, + ); + if (cssText) { + attributes._cssText = absoluteToStylesheet(cssText, getHref()); + } + } + // form fields + if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') { + const value = (n as HTMLInputElement | HTMLTextAreaElement).value; + if ( + attributes.type !== 'radio' && + attributes.type !== 'checkbox' && + attributes.type !== 'submit' && + attributes.type !== 'button' && + value + ) { + attributes.value = maskInputValue({ + type: attributes.type, + tagName, + value, + maskInputOptions, + maskInputFn, + }); + } else if ((n as HTMLInputElement).checked) { + attributes.checked = (n as HTMLInputElement).checked; + } + } + if (tagName === 'option') { + if ((n as HTMLOptionElement).selected && !maskInputOptions['select']) { + attributes.selected = true; + } else { + // ignore the html attribute (which corresponds to DOM (n as HTMLOptionElement).defaultSelected) + // if it's already been changed + delete attributes.selected; + } + } + // canvas image data + if (tagName === 'canvas' && recordCanvas) { + if ((n as ICanvas).__context === '2d') { + // only record this on 2d canvas + if (!is2DCanvasBlank(n as HTMLCanvasElement)) { + attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL( + dataURLOptions.type, + dataURLOptions.quality, + ); + } + } else if (!('__context' in n)) { + // context is unknown, better not call getContext to trigger it + const canvasDataURL = (n as HTMLCanvasElement).toDataURL( + dataURLOptions.type, + dataURLOptions.quality, + ); + + // create blank canvas of same dimensions + const blankCanvas = document.createElement('canvas'); + blankCanvas.width = (n as HTMLCanvasElement).width; + blankCanvas.height = (n as HTMLCanvasElement).height; + const blankCanvasDataURL = blankCanvas.toDataURL( + dataURLOptions.type, + dataURLOptions.quality, + ); + + // no need to save dataURL if it's the same as blank canvas + if (canvasDataURL !== blankCanvasDataURL) { + attributes.rr_dataURL = canvasDataURL; + } + } + } + // save image offline + if ( + tagName === 'img' && + inlineImages && + !needBlock && + !needMask && + !enableStrictPrivacy + ) { + if (!canvasService) { + canvasService = doc.createElement('canvas'); + canvasCtx = canvasService.getContext('2d'); + } + const image = n as HTMLImageElement; + const oldValue = image.crossOrigin; + image.crossOrigin = 'anonymous'; + const recordInlineImage = () => { + try { + canvasService!.width = image.naturalWidth; + canvasService!.height = image.naturalHeight; + canvasCtx!.drawImage(image, 0, 0); + attributes.rr_dataURL = canvasService!.toDataURL( + dataURLOptions.type, + dataURLOptions.quality, + ); + } catch (err) { + console.warn( + `Cannot inline img src=${image.currentSrc}! Error: ${err as string}`, + ); + } + oldValue + ? (attributes.crossOrigin = oldValue) + : image.removeAttribute('crossorigin'); + }; + // The image content may not have finished loading yet. + if (image.complete && image.naturalWidth !== 0) recordInlineImage(); + else image.onload = recordInlineImage; + } + // media elements + if (tagName === 'audio' || tagName === 'video') { + attributes.rr_mediaState = (n as HTMLMediaElement).paused + ? 'paused' + : 'played'; + attributes.rr_mediaCurrentTime = (n as HTMLMediaElement).currentTime; + } + // Scroll + if (!newlyAddedElement) { + // `scrollTop` and `scrollLeft` are expensive calls because they trigger reflow. + // Since `scrollTop` & `scrollLeft` are always 0 when an element is added to the DOM. + // And scrolls also get picked up by rrweb's ScrollObserver + // So we can safely skip the `scrollTop/Left` calls for newly added elements + if (n.scrollLeft) { + attributes.rr_scrollLeft = n.scrollLeft; + } + if (n.scrollTop) { + attributes.rr_scrollTop = n.scrollTop; + } + } + // block element + if (needBlock || needMask || (tagName === 'img' && enableStrictPrivacy)) { + const { width, height } = n.getBoundingClientRect(); + attributes = { + class: attributes.class, + rr_width: `${width}px`, + rr_height: `${height}px`, + }; + if (enableStrictPrivacy) { + needBlock = true; + } + } + // iframe + if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src as string)) { + if (!(n as HTMLIFrameElement).contentDocument) { + // we can't record it directly as we can't see into it + // preserve the src attribute so a decision can be taken at replay time + attributes.rr_src = attributes.src; + } + delete attributes.src; // prevent auto loading + } + + return { + type: NodeType.Element, + tagName, + attributes, + childNodes: [], + isSVG: isSVGElement(n as Element) || undefined, + needBlock, + needMask, + rootId, + }; +} + +function lowerIfExists(maybeAttr: string | number | boolean): string { + if (maybeAttr === undefined) { + return ''; + } else { + return (maybeAttr as string).toLowerCase(); + } +} + +function slimDOMExcluded( + sn: serializedNode, + slimDOMOptions: SlimDOMOptions, +): boolean { + if (slimDOMOptions.comment && sn.type === NodeType.Comment) { + // TODO: convert IE conditional comments to real nodes + return true; + } else if (sn.type === NodeType.Element) { + if ( + slimDOMOptions.script && + // script tag + (sn.tagName === 'script' || + // preload link + (sn.tagName === 'link' && + sn.attributes.rel === 'preload' && + sn.attributes.as === 'script') || + // prefetch link + (sn.tagName === 'link' && + sn.attributes.rel === 'prefetch' && + typeof sn.attributes.href === 'string' && + sn.attributes.href.endsWith('.js'))) + ) { + return true; + } else if ( + slimDOMOptions.headFavicon && + ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') || + (sn.tagName === 'meta' && + (lowerIfExists(sn.attributes.name).match( + /^msapplication-tile(image|color)$/, + ) || + lowerIfExists(sn.attributes.name) === 'application-name' || + lowerIfExists(sn.attributes.rel) === 'icon' || + lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' || + lowerIfExists(sn.attributes.rel) === 'shortcut icon'))) + ) { + return true; + } else if (sn.tagName === 'meta') { + if ( + slimDOMOptions.headMetaDescKeywords && + lowerIfExists(sn.attributes.name).match(/^description|keywords$/) + ) { + return true; + } else if ( + slimDOMOptions.headMetaSocial && + (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || // og = opengraph (facebook) + lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) || + lowerIfExists(sn.attributes.name) === 'pinterest') + ) { + return true; + } else if ( + slimDOMOptions.headMetaRobots && + (lowerIfExists(sn.attributes.name) === 'robots' || + lowerIfExists(sn.attributes.name) === 'googlebot' || + lowerIfExists(sn.attributes.name) === 'bingbot') + ) { + return true; + } else if ( + slimDOMOptions.headMetaHttpEquiv && + sn.attributes['http-equiv'] !== undefined + ) { + // e.g. X-UA-Compatible, Content-Type, Content-Language, + // cache-control, X-Translated-By + return true; + } else if ( + slimDOMOptions.headMetaAuthorship && + (lowerIfExists(sn.attributes.name) === 'author' || + lowerIfExists(sn.attributes.name) === 'generator' || + lowerIfExists(sn.attributes.name) === 'framework' || + lowerIfExists(sn.attributes.name) === 'publisher' || + lowerIfExists(sn.attributes.name) === 'progid' || + lowerIfExists(sn.attributes.property).match(/^article:/) || + lowerIfExists(sn.attributes.property).match(/^product:/)) + ) { + return true; + } else if ( + slimDOMOptions.headMetaVerification && + (lowerIfExists(sn.attributes.name) === 'google-site-verification' || + lowerIfExists(sn.attributes.name) === 'yandex-verification' || + lowerIfExists(sn.attributes.name) === 'csrf-token' || + lowerIfExists(sn.attributes.name) === 'p:domain_verify' || + lowerIfExists(sn.attributes.name) === 'verify-v1' || + lowerIfExists(sn.attributes.name) === 'verification' || + lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token') + ) { + return true; + } + } + } + return false; +} + +export function serializeNodeWithId( + n: Node, + options: { + doc: Document; + mirror: Mirror; + blockClass: string | RegExp; + blockSelector: string | null; + maskTextClass: string | RegExp; + maskTextSelector: string | null; + skipChild: boolean; + inlineStylesheet: boolean; + newlyAddedElement?: boolean; + maskInputOptions?: MaskInputOptions; + maskTextFn: MaskTextFn | undefined; + maskInputFn: MaskInputFn | undefined; + slimDOMOptions: SlimDOMOptions; + dataURLOptions?: DataURLOptions; + keepIframeSrcFn?: KeepIframeSrcFn; + inlineImages?: boolean; + recordCanvas?: boolean; + preserveWhiteSpace?: boolean; + onSerialize?: (n: Node) => unknown; + onIframeLoad?: ( + iframeNode: HTMLIFrameElement, + node: serializedElementNodeWithId, + ) => unknown; + iframeLoadTimeout?: number; + enableStrictPrivacy: boolean; + onStylesheetLoad?: ( + linkNode: HTMLLinkElement, + node: serializedElementNodeWithId, + ) => unknown; + stylesheetLoadTimeout?: number; + }, +): serializedNodeWithId | null { + const { + doc, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild = false, + inlineStylesheet = true, + maskInputOptions = {}, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions = {}, + inlineImages = false, + recordCanvas = false, + onSerialize, + onIframeLoad, + iframeLoadTimeout = 5000, + onStylesheetLoad, + stylesheetLoadTimeout = 5000, + keepIframeSrcFn = () => false, + newlyAddedElement = false, + enableStrictPrivacy, + } = options; + let { preserveWhiteSpace = true } = options; + const _serializedNode = serializeNode(n, { + doc, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + enableStrictPrivacy, + }); + if (!_serializedNode) { + // TODO: dev only + console.warn(n, 'not serialized'); + return null; + } + + let id: number | undefined; + if (mirror.hasNode(n)) { + // Reuse the previous id + id = mirror.getId(n); + } else if ( + slimDOMExcluded(_serializedNode, slimDOMOptions) || + (!preserveWhiteSpace && + _serializedNode.type === NodeType.Text && + !_serializedNode.isStyle && + !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length) + ) { + id = IGNORED_NODE; + } else { + id = genId(); + } + if (id === IGNORED_NODE) { + return null; // slimDOM + } + + const serializedNode = Object.assign(_serializedNode, { id }); + + mirror.add(n, serializedNode); + + if (onSerialize) { + onSerialize(n); + } + let recordChild = !skipChild; + let strictPrivacy = enableStrictPrivacy; + if (serializedNode.type === NodeType.Element) { + recordChild = recordChild && !serializedNode.needBlock; + strictPrivacy = + enableStrictPrivacy || + !!serializedNode.needBlock || + !!serializedNode.needMask; + + /** Highlight Code Begin */ + // Remove the image's src if enableStrictPrivacy. + if (strictPrivacy && serializedNode.tagName === 'img') { + const clone = n.cloneNode(); + ((clone as unknown) as HTMLImageElement).src = ''; + mirror.add(clone, serializedNode); + } + /** Highlight Code End */ + + // these properties was not needed in replay side + delete serializedNode.needBlock; + delete serializedNode.needMask; + const shadowRoot = (n as HTMLElement).shadowRoot; + if (shadowRoot && isNativeShadowDom(shadowRoot)) + serializedNode.isShadowHost = true; + } + if ( + (serializedNode.type === NodeType.Document || + serializedNode.type === NodeType.Element) && + recordChild + ) { + if ( + slimDOMOptions.headWhitespace && + serializedNode.type === NodeType.Element && + serializedNode.tagName === 'head' + // would impede performance: || getComputedStyle(n)['white-space'] === 'normal' + ) { + preserveWhiteSpace = false; + } + const bypassOptions = { + doc, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + enableStrictPrivacy: strictPrivacy, + }; + for (const childN of Array.from(n.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + serializedNode.childNodes.push(serializedChildNode); + } + } + + if (isElement(n) && n.shadowRoot) { + for (const childN of Array.from(n.shadowRoot.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + isNativeShadowDom(n.shadowRoot) && + (serializedChildNode.isShadow = true); + serializedNode.childNodes.push(serializedChildNode); + } + } + } + } + + if ( + n.parentNode && + isShadowRoot(n.parentNode) && + isNativeShadowDom(n.parentNode) + ) { + serializedNode.isShadow = true; + } + + if ( + serializedNode.type === NodeType.Element && + serializedNode.tagName === 'iframe' + ) { + onceIframeLoaded( + n as HTMLIFrameElement, + () => { + const iframeDoc = (n as HTMLIFrameElement).contentDocument; + if (iframeDoc && onIframeLoad) { + const serializedIframeNode = serializeNodeWithId(iframeDoc, { + doc: iframeDoc, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + enableStrictPrivacy, + }); + + if (serializedIframeNode) { + onIframeLoad( + n as HTMLIFrameElement, + serializedIframeNode as serializedElementNodeWithId, + ); + } + } + }, + iframeLoadTimeout, + ); + } + + // + if ( + serializedNode.type === NodeType.Element && + serializedNode.tagName === 'link' && + serializedNode.attributes.rel === 'stylesheet' + ) { + onceStylesheetLoaded( + n as HTMLLinkElement, + () => { + if (onStylesheetLoad) { + const serializedLinkNode = serializeNodeWithId(n, { + doc, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + enableStrictPrivacy, + }); + + if (serializedLinkNode) { + onStylesheetLoad( + n as HTMLLinkElement, + serializedLinkNode as serializedElementNodeWithId, + ); + } + } + }, + stylesheetLoadTimeout, + ); + if (isStylesheetLoaded(n as HTMLLinkElement) === false) return null; // add stylesheet in later mutation + } + + // + if ( + serializedNode.type === NodeType.Element && + serializedNode.tagName === 'link' && + serializedNode.attributes.rel === 'stylesheet' + ) { + onceStylesheetLoaded( + n as HTMLLinkElement, + () => { + if (onStylesheetLoad) { + const serializedLinkNode = serializeNodeWithId(n, { + doc, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + enableStrictPrivacy, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + + if (serializedLinkNode) { + onStylesheetLoad( + n as HTMLLinkElement, + serializedLinkNode as serializedElementNodeWithId, + ); + } + } + }, + stylesheetLoadTimeout, + ); + if (isStylesheetLoaded(n as HTMLLinkElement) === false) return null; // add stylesheet in later mutation + } + + return serializedNode; +} + +function snapshot( + n: Document, + options?: { + mirror?: Mirror; + blockClass?: string | RegExp; + blockSelector?: string | null; + maskTextClass?: string | RegExp; + maskTextSelector?: string | null; + inlineStylesheet?: boolean; + maskAllInputs?: boolean | MaskInputOptions; + maskTextFn?: MaskTextFn; + maskInputFn?: MaskTextFn; + slimDOM?: boolean | SlimDOMOptions; + dataURLOptions?: DataURLOptions; + inlineImages?: boolean; + recordCanvas?: boolean; + preserveWhiteSpace?: boolean; + onSerialize?: (n: Node) => unknown; + onIframeLoad?: ( + iframeNode: HTMLIFrameElement, + node: serializedElementNodeWithId, + ) => unknown; + iframeLoadTimeout?: number; + onStylesheetLoad?: ( + linkNode: HTMLLinkElement, + node: serializedElementNodeWithId, + ) => unknown; + stylesheetLoadTimeout?: number; + keepIframeSrcFn?: KeepIframeSrcFn; + enableStrictPrivacy: boolean; + }, +): serializedNodeWithId | null { + const { + mirror = new Mirror(), + blockClass = 'highlight-block', + blockSelector = null, + maskTextClass = 'highlight-mask', + maskTextSelector = null, + inlineStylesheet = true, + inlineImages = false, + recordCanvas = false, + maskAllInputs = false, + maskTextFn, + maskInputFn, + slimDOM = false, + dataURLOptions, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn = () => false, + enableStrictPrivacy = false, + } = options || {}; + const maskInputOptions: MaskInputOptions = + maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : maskAllInputs === false + ? { + password: true, + } + : maskAllInputs; + const slimDOMOptions: SlimDOMOptions = + slimDOM === true || slimDOM === 'all' + ? // if true: set of sensible options that should not throw away any information + { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaDescKeywords: slimDOM === 'all', // destructive + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaAuthorship: true, + headMetaVerification: true, + } + : slimDOM === false + ? {} + : slimDOM; + return serializeNodeWithId(n, { + doc: n, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + newlyAddedElement: false, + enableStrictPrivacy, + }); +} + +export function visitSnapshot( + node: serializedNodeWithId, + onVisit: (node: serializedNodeWithId) => unknown, +) { + function walk(current: serializedNodeWithId) { + onVisit(current); + if ( + current.type === NodeType.Document || + current.type === NodeType.Element + ) { + current.childNodes.forEach(walk); + } + } + + walk(node); +} + +export function cleanupSnapshot() { + // allow a new recording to start numbering nodes from scratch + _id = 1; +} + +export default snapshot; diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts new file mode 100644 index 00000000..222efa62 --- /dev/null +++ b/packages/rrweb-snapshot/src/types.ts @@ -0,0 +1,156 @@ +export enum NodeType { + Document, + DocumentType, + Element, + Text, + CDATA, + Comment, +} + +export type documentNode = { + type: NodeType.Document; + childNodes: serializedNodeWithId[]; + compatMode?: string; +}; + +export type documentTypeNode = { + type: NodeType.DocumentType; + name: string; + publicId: string; + systemId: string; +}; + +export type attributes = { + [key: string]: string | number | boolean; +}; +export type elementNode = { + type: NodeType.Element; + tagName: string; + attributes: attributes; + childNodes: serializedNodeWithId[]; + isSVG?: true; + needBlock?: boolean; + needMask?: boolean; +}; + +export type textNode = { + type: NodeType.Text; + textContent: string; + isStyle?: true; +}; + +export type cdataNode = { + type: NodeType.CDATA; + textContent: ''; +}; + +export type commentNode = { + type: NodeType.Comment; + textContent: string; +}; + +export type serializedNode = ( + | documentNode + | documentTypeNode + | elementNode + | textNode + | cdataNode + | commentNode +) & { + rootId?: number; + isShadowHost?: boolean; + isShadow?: boolean; +}; + +export type serializedNodeWithId = serializedNode & { id: number }; + +export type serializedElementNodeWithId = Extract< + serializedNodeWithId, + Record<'type', NodeType.Element> +>; + +export type tagMap = { + [key: string]: string; +}; + +// @deprecated +export interface INode extends Node { + __sn: serializedNodeWithId; +} + +export interface ICanvas extends HTMLCanvasElement { + __context: string; +} + +export interface IMirror { + getId(n: TNode | undefined | null): number; + + getNode(id: number): TNode | null; + + getIds(): number[]; + + getMeta(n: TNode): serializedNodeWithId | null; + + removeNodeFromMap(n: TNode): void; + + has(id: number): boolean; + + hasNode(node: TNode): boolean; + + add(n: TNode, meta: serializedNodeWithId): void; + + replace(id: number, n: TNode): void; + + reset(): void; +} + +export type idNodeMap = Map; + +export type nodeMetaMap = WeakMap; + +export type MaskInputOptions = Partial<{ + color: boolean; + date: boolean; + 'datetime-local': boolean; + email: boolean; + month: boolean; + number: boolean; + range: boolean; + search: boolean; + tel: boolean; + text: boolean; + time: boolean; + url: boolean; + week: boolean; + // unify textarea and select element with text input + textarea: boolean; + select: boolean; + password: boolean; +}>; + +export type SlimDOMOptions = Partial<{ + script: boolean; + comment: boolean; + headFavicon: boolean; + headWhitespace: boolean; + headMetaDescKeywords: boolean; + headMetaSocial: boolean; + headMetaRobots: boolean; + headMetaHttpEquiv: boolean; + headMetaAuthorship: boolean; + headMetaVerification: boolean; +}>; + +export type DataURLOptions = Partial<{ + type: string; + quality: number; +}>; + +export type MaskTextFn = (text: string) => string; +export type MaskInputFn = (text: string) => string; + +export type KeepIframeSrcFn = (src: string) => boolean; + +export type BuildCache = { + stylesWithHoverClass: Map; +}; diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts new file mode 100644 index 00000000..22b1ebc9 --- /dev/null +++ b/packages/rrweb-snapshot/src/utils.ts @@ -0,0 +1,175 @@ +import { + idNodeMap, + MaskInputFn, + MaskInputOptions, + nodeMetaMap, + IMirror, + serializedNodeWithId, +} from './types'; + +export function isElement(n: Node): n is Element { + return n.nodeType === n.ELEMENT_NODE; +} + +export function isShadowRoot(n: Node): n is ShadowRoot { + const host: Element | null = (n as ShadowRoot)?.host; + return Boolean(host?.shadowRoot === n); +} + +/** + * To fix the issue https://github.com/rrweb-io/rrweb/issues/933. + * Some websites use polyfilled shadow dom and this function is used to detect this situation. + */ +export function isNativeShadowDom(shadowRoot: ShadowRoot) { + return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; +} + +export class Mirror implements IMirror { + private idNodeMap: idNodeMap = new Map(); + private nodeMetaMap: nodeMetaMap = new WeakMap(); + + getId(n: Node | undefined | null): number { + if (!n) return -1; + + const id = this.getMeta(n)?.id; + + // if n is not a serialized Node, use -1 as its id. + return id ?? -1; + } + + getNode(id: number): Node | null { + return this.idNodeMap.get(id) || null; + } + + getIds(): number[] { + return Array.from(this.idNodeMap.keys()); + } + + getMeta(n: Node): serializedNodeWithId | null { + return this.nodeMetaMap.get(n) || null; + } + + // removes the node from idNodeMap + // doesn't remove the node from nodeMetaMap + removeNodeFromMap(n: Node) { + const id = this.getId(n); + this.idNodeMap.delete(id); + + if (n.childNodes) { + n.childNodes.forEach((childNode) => + this.removeNodeFromMap((childNode as unknown) as Node), + ); + } + } + has(id: number): boolean { + return this.idNodeMap.has(id); + } + + hasNode(node: Node): boolean { + return this.nodeMetaMap.has(node); + } + + add(n: Node, meta: serializedNodeWithId) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); + } + + replace(id: number, n: Node) { + this.idNodeMap.set(id, n); + } + + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } +} + +export function createMirror(): Mirror { + return new Mirror(); +} + +export function maskInputValue({ + maskInputOptions, + tagName, + type, + value, + maskInputFn, +}: { + maskInputOptions: MaskInputOptions; + tagName: string; + type: string | number | boolean | null; + value: string | null; + maskInputFn?: MaskInputFn; +}): string { + let text = value || ''; + if ( + maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || + maskInputOptions[type as keyof MaskInputOptions] + ) { + if (maskInputFn) { + text = maskInputFn(text); + } else { + text = '*'.repeat(text.length); + } + } + return text; +} + +const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; +type PatchedGetImageData = { + [ORIGINAL_ATTRIBUTE_NAME]: CanvasImageData['getImageData']; +} & CanvasImageData['getImageData']; + +export function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean { + const ctx = canvas.getContext('2d'); + if (!ctx) return true; + + const chunkSize = 50; + + // get chunks of the canvas and check if it is blank + for (let x = 0; x < canvas.width; x += chunkSize) { + for (let y = 0; y < canvas.height; y += chunkSize) { + // eslint-disable-next-line @typescript-eslint/unbound-method + const getImageData = ctx.getImageData as PatchedGetImageData; + const originalGetImageData = + ORIGINAL_ATTRIBUTE_NAME in getImageData + ? getImageData[ORIGINAL_ATTRIBUTE_NAME] + : getImageData; + // by getting the canvas in chunks we avoid an expensive + // `getImageData` call that retrieves everything + // even if we can already tell from the first chunk(s) that + // the canvas isn't blank + const pixelBuffer = new Uint32Array( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access + originalGetImageData.call( + ctx, + x, + y, + Math.min(chunkSize, canvas.width - x), + Math.min(chunkSize, canvas.height - y), + ).data.buffer, + ); + if (pixelBuffer.some((pixel) => pixel !== 0)) return false; + } + } + return true; +} + +/** Start of Highlight + * Returns a string of the same length that has been obfuscated. + */ +export function obfuscateText(text: string): string { + // We remove non-printing characters. + // For example: '‌' is a character that isn't shown visibly or takes up layout space on the screen. However if you take the length of the string, it's counted as 1. + // For example: "‌1"'s length is 2 but visually it's only taking up 1 character width. + // If we don't filter does out, our string obfuscation could have more characters than what was originally presented. + text = text.replace(/[^ -~]+/g, ''); + text = + text + ?.split(' ') + .map((word) => Math.random().toString(20).substr(2, word.length)) + .join(' ') || ''; + return text; +} +/* End of Highlight */ diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap new file mode 100644 index 00000000..64b8f30f --- /dev/null +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -0,0 +1,843 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`iframe integration tests snapshot async iframes 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\\": \\"Main\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"one\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"two\\" + }, + \\"childNodes\\": [], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 19 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 +}" +`; + +exports[`integration tests [html file]: about-mozilla.html 1`] = ` +" + The Book of Mozilla, 11:9 + +

+ Mammon slept. And the beast reborn spread over the earth and its numbers + grew legion. And they proclaimed the times and sacrificed crops unto the + fire, with the cunning of foxes. And they built a new world in their own + image as promised by the + sacred words, and spoke + of the beast with their children. Mammon awoke, and lo! it was + naught but a follower. +

+ from The Book of Mozilla, 11:9
(10th Edition) +

" +`; + +exports[`integration tests [html file]: basic.html 1`] = ` +" + + + + Document + +

Title

" +`; + +exports[`integration tests [html file]: block-element.html 1`] = ` +" + + + + Document + + +
+
record 2
+
+
+ " +`; + +exports[`integration tests [html file]: compat-mode.html 1`] = ` +" + Compat Mode; image resizing + + +
+ + + +
+
+" +`; + +exports[`integration tests [html file]: cors-style-sheet.html 1`] = ` +" + + + + with style sheet + + + + " +`; + +exports[`integration tests [html file]: dynamic-stylesheet.html 1`] = ` +" + + + + dynamic stylesheet + + + + +

p tag

+ " +`; + +exports[`integration tests [html file]: form-fields.html 1`] = ` +" + + + + form fields + +
+ + + + + + +
+ + " +`; + +exports[`integration tests [html file]: hover.html 1`] = ` +" + + + + hover selector + + +
hover me
+" +`; + +exports[`integration tests [html file]: iframe.html 1`] = ` +" + + + + iframe + + + +" +`; + +exports[`integration tests [html file]: iframe-inner.html 1`] = ` +" +" +`; + +exports[`integration tests [html file]: invalid-attribute.html 1`] = ` +" +" +`; + +exports[`integration tests [html file]: invalid-doctype.html 1`] = ` +" + + + Invalid Doctype + + " +`; + +exports[`integration tests [html file]: invalid-tagname.html 1`] = ` +" + + + + Document + + +
Hello
+
Hello
+
+" +`; + +exports[`integration tests [html file]: mask-text.html 1`] = ` +" + + + + Document + +

**** *

+
+ **** * +
+
**** *
+ " +`; + +exports[`integration tests [html file]: picture.html 1`] = ` +" + + + + + \\"This + " +`; + +exports[`integration tests [html file]: preload.html 1`] = ` +" + + + Document + + + + " +`; + +exports[`integration tests [html file]: shadow-dom.html 1`] = ` +" + + + shadow DOM + + + + + + +
content panel 1
+
content panel 2
+
content panel 3
+
+ + " +`; + +exports[`integration tests [html file]: svg.html 1`] = ` +" + IcoMoon - SVG Icons + + + + +
+

Grid Size: 0

+
+
+ + Icon-behance +
+
+
+
+ + Icon-linkedin +
+
+
+ + + + + + + + + " +`; + +exports[`integration tests [html file]: video.html 1`] = ` +" + + + + video + + + + " +`; + +exports[`integration tests [html file]: with-relative-res.html 1`] = ` +" + + + + Document + + + +
Hello
+ Hello +
Hello
+
+ \\"\\" + \\"\\" + \\"\\" + \\"\\" + \\"\\"" +`; + +exports[`integration tests [html file]: with-script.html 1`] = ` +" + + + + with script + + + " +`; + +exports[`integration tests [html file]: with-style-sheet.html 1`] = ` +" + + + + with style sheet + + +" +`; + +exports[`integration tests [html file]: with-style-sheet-with-import.html 1`] = ` +" + + + + with style sheet with import + + +" +`; + +exports[`shadow DOM integration tests snapshot shadow DOM 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\\": \\"shadow DOM\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"fancy-tabs\\", + \\"attributes\\": { + \\"background\\": \\"\\", + \\"role\\": \\"tablist\\", + \\"selected\\": \\"1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": { + \\"slot\\": \\"title\\", + \\"role\\": \\"tab\\", + \\"tabindex\\": \\"-1\\", + \\"aria-selected\\": \\"false\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Tab 1\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": { + \\"slot\\": \\"title\\", + \\"selected\\": \\"\\", + \\"role\\": \\"tab\\", + \\"tabindex\\": \\"0\\", + \\"aria-selected\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Tab 2\\", + \\"id\\": 22 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": { + \\"slot\\": \\"title\\", + \\"role\\": \\"tab\\", + \\"tabindex\\": \\"-1\\", + \\"aria-selected\\": \\"false\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Tab 3\\", + \\"id\\": 25 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"section\\", + \\"attributes\\": { + \\"role\\": \\"tabpanel\\", + \\"tabindex\\": \\"0\\", + \\"aria-hidden\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"content panel 1\\", + \\"id\\": 28 + } + ], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"section\\", + \\"attributes\\": { + \\"role\\": \\"tabpanel\\", + \\"tabindex\\": \\"0\\", + \\"aria-hidden\\": \\"false\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"content panel 2\\", + \\"id\\": 31 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 32 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"section\\", + \\"attributes\\": { + \\"role\\": \\"tabpanel\\", + \\"tabindex\\": \\"0\\", + \\"aria-hidden\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"content panel 3\\", + \\"id\\": 34 + } + ], + \\"id\\": 33 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 35 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36, + \\"isShadow\\": true + }, + { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": {}, + \\"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, + \\"id\\": 38 + } + ], + \\"id\\": 37, + \\"isShadow\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 39, + \\"isShadow\\": true + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"tabs\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"slot\\", + \\"attributes\\": { + \\"id\\": \\"tabsSlot\\", + \\"name\\": \\"title\\" + }, + \\"childNodes\\": [], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 40, + \\"isShadow\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 44, + \\"isShadow\\": true + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"panels\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"slot\\", + \\"attributes\\": { + \\"id\\": \\"panelsSlot\\" + }, + \\"childNodes\\": [], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + } + ], + \\"id\\": 45, + \\"isShadow\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 49, + \\"isShadow\\": true + } + ], + \\"id\\": 16, + \\"isShadowHost\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 50 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 52 + } + ], + \\"id\\": 51 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"id\\": 53 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 +}" +`; diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts new file mode 100644 index 00000000..4461022b --- /dev/null +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -0,0 +1,109 @@ +import { parse, Rule, Media } from '../src/css'; + +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', + }); + + 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('should throw when a selector is missing', () => { + expect(() => { + parse('{size: large}'); + }).toThrow(); + + expect(() => { + parse('b { color: red; }\n{ color: green; }\na { color: blue; }'); + }).toThrow(); + }); + + it('should throw when a broken comment is found', () => { + expect(() => { + parse('thing { color: red; } /* b { color: blue; }'); + }).toThrow(); + + expect(() => { + parse('/*'); + }).toThrow(); + + /* Nested comments should be fine */ + expect(() => { + parse('/* /* */'); + }).not.toThrow(); + }); + + it('should allow empty property value', () => { + expect(() => { + parse('p { color:; }'); + }).not.toThrow(); + }); + + it('should not throw with silent option', () => { + expect(() => { + parse('thing { color: red; } /* b { color: blue; }', { silent: true }); + }).not.toThrow(); + }); + + 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('should set parent property', () => { + const result = parse( + 'thing { test: value; }\n' + + '@media (min-width: 100px) { thing { test: value; } }', + ); + + 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); + }); +}); diff --git a/packages/rrweb-snapshot/test/css/benchmark.css b/packages/rrweb-snapshot/test/css/benchmark.css new file mode 100644 index 00000000..ea7e0585 --- /dev/null +++ b/packages/rrweb-snapshot/test/css/benchmark.css @@ -0,0 +1,6 @@ +/*!----------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.17.0(63d87164d0bc8c6206d9339c195289c93665028e) + * Released under the MIT license + * https://github.com/Microsoft/vscode/blob/master/LICENSE.txt + *-----------------------------------------------------------*/.monaco-action-bar{text-align:right;overflow:hidden;white-space:nowrap}.monaco-action-bar .actions-container{display:flex;margin:0 auto;padding:0;width:100%;justify-content:flex-end}.monaco-action-bar.vertical .actions-container{display:inline-block}.monaco-action-bar.reverse .actions-container{flex-direction:row-reverse}.monaco-action-bar .action-item{cursor:pointer;display:inline-block;transition:transform 50ms ease;position:relative}.monaco-action-bar .action-item.disabled{cursor:default}.monaco-action-bar.animated .action-item.active{transform:scale(1.272019649)}.monaco-action-bar .action-item .icon{display:inline-block}.monaco-action-bar .action-label{font-size:11px;margin-right:4px}.monaco-action-bar .action-label.octicon{font-size:15px;line-height:35px;text-align:center}.monaco-action-bar .action-item.disabled .action-label,.monaco-action-bar .action-item.disabled .action-label:hover{opacity:.4}.monaco-action-bar.vertical{text-align:left}.monaco-action-bar.vertical .action-item{display:block}.monaco-action-bar.vertical .action-label.separator{display:block;border-bottom:1px solid #bbb;padding-top:1px;margin-left:.8em;margin-right:.8em}.monaco-action-bar.animated.vertical .action-item.active{transform:translate(5px)}.secondary-actions .monaco-action-bar .action-label{margin-left:6px}.monaco-action-bar .action-item.select-container{overflow:hidden;flex:1;max-width:170px;min-width:60px;display:flex;align-items:center;justify-content:center}.monaco-aria-container{position:absolute;left:-999em}.monaco-custom-checkbox{margin-left:2px;float:left;cursor:pointer;overflow:hidden;opacity:.7;width:20px;height:20px;border:1px solid transparent;padding:1px;-o-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;-o-user-select:none;-ms-user-select:none;user-select:none}.monaco-custom-checkbox.checked,.monaco-custom-checkbox:hover{opacity:1}.hc-black .monaco-custom-checkbox,.hc-black .monaco-custom-checkbox:hover{background:none}.context-view{position:absolute;z-index:2000}.monaco-count-badge{padding:.3em .5em;border-radius:1em;font-size:85%;min-width:1.6em;line-height:1em;font-weight:400;text-align:center;display:inline-block;box-sizing:border-box}.monaco-findInput{position:relative}.monaco-findInput .monaco-inputbox{font-size:13px;width:100%}.monaco-findInput>.controls{position:absolute;top:3px;right:2px}.vs .monaco-findInput.disabled{background-color:#e1e1e1}.vs-dark .monaco-findInput.disabled{background-color:#333}.monaco-findInput.highlight-0 .controls{animation:monaco-findInput-highlight-0 .1s linear 0s}.monaco-findInput.highlight-1 .controls{animation:monaco-findInput-highlight-1 .1s linear 0s}.hc-black .monaco-findInput.highlight-0 .controls,.vs-dark .monaco-findInput.highlight-0 .controls{animation:monaco-findInput-highlight-dark-0 .1s linear 0s}.hc-black .monaco-findInput.highlight-1 .controls,.vs-dark .monaco-findInput.highlight-1 .controls{animation:monaco-findInput-highlight-dark-1 .1s linear 0s}@keyframes monaco-findInput-highlight-0{0%{background:rgba(253,255,0,.8)}to{background:transparent}}@keyframes monaco-findInput-highlight-1{0%{background:rgba(253,255,0,.8)}99%{background:transparent}}@keyframes monaco-findInput-highlight-dark-0{0%{background:hsla(0,0%,100%,.44)}to{background:transparent}}@keyframes monaco-findInput-highlight-dark-1{0%{background:hsla(0,0%,100%,.44)}99%{background:transparent}}.vs .monaco-custom-checkbox.monaco-case-sensitive{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc3Qwe29wYWNpdHk6MDtmaWxsOiNGNkY2RjY7fSAuc3Qxe2ZpbGw6I0Y2RjZGNjt9IC5zdDJ7ZmlsbDojNDI0MjQyO308L3N0eWxlPjxnIGlkPSJvdXRsaW5lIj48cmVjdCBjbGFzcz0ic3QwIiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTQuMTc2IDUuNTkyYy0uNTU1LS42LTEuMzM2LS45MDQtMi4zMjItLjkwNC0uMjU4IDAtLjUyMS4wMjQtLjc4NC4wNzItLjI0Ni4wNDQtLjQ3OS4xMDEtLjcuMTY5LS4yMjguMDctLjQzMi4xNDctLjYxMy4yMjktLjIyLjA5OS0uMzg5LjE5Ni0uNTEyLjI4NGwtLjQxOS4yOTl2Mi43MDFjLS4wODYuMTA4LS4xNjIuMjIzLS4yMjkuMzQ0bC0yLjQ1LTYuMzU0aC0yLjM5NGwtMy43NTMgOS44MDR2LjU5OGgzLjAyNWwuODM4LTIuMzVoMi4xNjdsLjg5MSAyLjM1aDMuMjM3bC0uMDAxLS4wMDNjLjMwNS4wOTIuNjMzLjE1Ljk5My4xNS4zNDQgMCAuNjcxLS4wNDkuOTc4LS4xNDZoMi44NTN2LTQuOTAzYy0uMDAxLS45NzUtLjI3MS0xLjc2My0uODA1LTIuMzR6Ii8+PC9nPjxnIGlkPSJpY29uX3g1Rl9iZyI+PHBhdGggY2xhc3M9InN0MiIgZD0iTTcuNjExIDExLjgzNGwtLjg5MS0yLjM1aC0zLjU2MmwtLjgzOCAyLjM1aC0xLjA5NWwzLjIxNy04LjQwMmgxLjAybDMuMjQgOC40MDJoLTEuMDkxem0tMi41MzEtNi44MTRsLS4wNDQtLjEzNS0uMDM4LS4xNTYtLjAyOS0uMTUyLS4wMjQtLjEyNmgtLjAyM2wtLjAyMS4xMjYtLjAzMi4xNTItLjAzOC4xNTYtLjA0NC4xMzUtMS4zMDcgMy41NzRoMi45MThsLTEuMzE4LTMuNTc0eiIvPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0xMy4wMiAxMS44MzR2LS45MzhoLS4wMjNjLS4xOTkuMzUyLS40NTYuNjItLjc3MS44MDZzLS42NzMuMjc4LTEuMDc1LjI3OGMtLjMxMyAwLS41ODgtLjA0NS0uODI2LS4xMzVzLS40MzgtLjIxMi0uNTk4LS4zNjYtLjI4MS0uMzM4LS4zNjMtLjU1MS0uMTI0LS40NDItLjEyNC0uNjg4YzAtLjI2Mi4wMzktLjUwMi4xMTctLjcyMXMuMTk4LS40MTIuMzYtLjU4LjM2Ny0uMzA4LjYxNS0uNDE5LjU0NC0uMTkuODg4LS4yMzdsMS44MTEtLjI1MmMwLS4yNzMtLjAyOS0uNTA3LS4wODgtLjdzLS4xNDMtLjM1MS0uMjUyLS40NzItLjI0MS0uMjEtLjM5Ni0uMjY3LS4zMjUtLjA4NS0uNTEzLS4wODVjLS4zNjMgMC0uNzE0LjA2NC0xLjA1Mi4xOTNzLS42MzguMzEtLjkwNC41NHYtLjk4NGMuMDgyLS4wNTkuMTk2LS4xMjEuMzQzLS4xODhzLjMxMi0uMTI4LjQ5NS0uMTg1LjM3OC0uMTA0LjU4My0uMTQxLjQwNy0uMDU2LjYwNi0uMDU2Yy42OTkgMCAxLjIyOS4xOTQgMS41ODguNTgzcy41MzkuOTQyLjUzOSAxLjY2MXYzLjkwMmgtLjk2em0tMS40NTQtMi44M2MtLjI3My4wMzUtLjQ5OC4wODUtLjY3NC4xNDlzLS4zMTMuMTQ0LS40MS4yMzctLjE2NS4yMDUtLjIwMi4zMzQtLjA1NS4yNzYtLjA1NS40NGMwIC4xNDEuMDI1LjI3MS4wNzYuMzkzcy4xMjQuMjI3LjIyLjMxNi4yMTUuMTYuMzU3LjIxMS4zMDguMDc2LjQ5NS4wNzZjLjI0MiAwIC40NjUtLjA0NS42NjgtLjEzNXMuMzc4LS4yMTQuNTI0LS4zNzIuMjYxLS4zNDQuMzQzLS41NTcuMTIzLS40NDIuMTIzLS42ODh2LS42MDlsLTEuNDY1LjIwNXoiLz48L2c+PC9zdmc+) 50% no-repeat}.hc-black .monaco-custom-checkbox.monaco-case-sensitive,.hc-black .monaco-custom-checkbox.monaco-case-sensitive:hover,.vs-dark .monaco-custom-checkbox.monaco-case-sensitive{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc3Qwe29wYWNpdHk6MDtmaWxsOiMyNjI2MjY7fSAuc3Qxe2ZpbGw6IzI2MjYyNjt9IC5zdDJ7ZmlsbDojQzVDNUM1O308L3N0eWxlPjxnIGlkPSJvdXRsaW5lIj48cmVjdCBjbGFzcz0ic3QwIiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTQuMTc2IDUuNTkyYy0uNTU1LS42LTEuMzM2LS45MDQtMi4zMjItLjkwNC0uMjU4IDAtLjUyMS4wMjQtLjc4NC4wNzItLjI0Ni4wNDQtLjQ3OS4xMDEtLjcuMTY5LS4yMjguMDctLjQzMi4xNDctLjYxMy4yMjktLjIyLjA5OS0uMzg5LjE5Ni0uNTEyLjI4NGwtLjQxOS4yOTl2Mi43MDFjLS4wODYuMTA4LS4xNjIuMjIzLS4yMjkuMzQ0bC0yLjQ1LTYuMzU0aC0yLjM5NGwtMy43NTMgOS44MDR2LjU5OGgzLjAyNWwuODM4LTIuMzVoMi4xNjdsLjg5MSAyLjM1aDMuMjM3bC0uMDAxLS4wMDNjLjMwNS4wOTIuNjMzLjE1Ljk5My4xNS4zNDQgMCAuNjcxLS4wNDkuOTc4LS4xNDZoMi44NTN2LTQuOTAzYy0uMDAxLS45NzUtLjI3MS0xLjc2My0uODA1LTIuMzR6Ii8+PC9nPjxnIGlkPSJpY29uX3g1Rl9iZyI+PHBhdGggY2xhc3M9InN0MiIgZD0iTTcuNjExIDExLjgzNGwtLjg5MS0yLjM1aC0zLjU2MmwtLjgzOCAyLjM1aC0xLjA5NWwzLjIxNy04LjQwMmgxLjAybDMuMjQgOC40MDJoLTEuMDkxem0tMi41MzEtNi44MTRsLS4wNDQtLjEzNS0uMDM4LS4xNTYtLjAyOS0uMTUyLS4wMjQtLjEyNmgtLjAyM2wtLjAyMS4xMjYtLjAzMi4xNTItLjAzOC4xNTYtLjA0NC4xMzUtMS4zMDcgMy41NzRoMi45MThsLTEuMzE4LTMuNTc0eiIvPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0xMy4wMiAxMS44MzR2LS45MzhoLS4wMjNjLS4xOTkuMzUyLS40NTYuNjItLjc3MS44MDZzLS42NzMuMjc4LTEuMDc1LjI3OGMtLjMxMyAwLS41ODgtLjA0NS0uODI2LS4xMzVzLS40MzgtLjIxMi0uNTk4LS4zNjYtLjI4MS0uMzM4LS4zNjMtLjU1MS0uMTI0LS40NDItLjEyNC0uNjg4YzAtLjI2Mi4wMzktLjUwMi4xMTctLjcyMXMuMTk4LS40MTIuMzYtLjU4LjM2Ny0uMzA4LjYxNS0uNDE5LjU0NC0uMTkuODg4LS4yMzdsMS44MTEtLjI1MmMwLS4yNzMtLjAyOS0uNTA3LS4wODgtLjdzLS4xNDMtLjM1MS0uMjUyLS40NzItLjI0MS0uMjEtLjM5Ni0uMjY3LS4zMjUtLjA4NS0uNTEzLS4wODVjLS4zNjMgMC0uNzE0LjA2NC0xLjA1Mi4xOTNzLS42MzguMzEtLjkwNC41NHYtLjk4NGMuMDgyLS4wNTkuMTk2LS4xMjEuMzQzLS4xODhzLjMxMi0uMTI4LjQ5NS0uMTg1LjM3OC0uMTA0LjU4My0uMTQxLjQwNy0uMDU2LjYwNi0uMDU2Yy42OTkgMCAxLjIyOS4xOTQgMS41ODguNTgzcy41MzkuOTQyLjUzOSAxLjY2MXYzLjkwMmgtLjk2em0tMS40NTQtMi44M2MtLjI3My4wMzUtLjQ5OC4wODUtLjY3NC4xNDlzLS4zMTMuMTQ0LS40MS4yMzctLjE2NS4yMDUtLjIwMi4zMzQtLjA1NS4yNzYtLjA1NS40NGMwIC4xNDEuMDI1LjI3MS4wNzYuMzkzcy4xMjQuMjI3LjIyLjMxNi4yMTUuMTYuMzU3LjIxMS4zMDguMDc2LjQ5NS4wNzZjLjI0MiAwIC40NjUtLjA0NS42NjgtLjEzNXMuMzc4LS4yMTQuNTI0LS4zNzIuMjYxLS4zNDQuMzQzLS41NTcuMTIzLS40NDIuMTIzLS42ODh2LS42MDlsLTEuNDY1LjIwNXoiLz48L2c+PC9zdmc+) 50% no-repeat}.vs .monaco-custom-checkbox.monaco-whole-word{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc3Qwe29wYWNpdHk6MDtmaWxsOiNGNkY2RjY7fSAuc3Qxe2ZpbGw6I0Y2RjZGNjt9IC5zdDJ7ZmlsbDojNDI0MjQyO308L3N0eWxlPjxnIGlkPSJvdXRsaW5lIj48cmVjdCBjbGFzcz0ic3QwIiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTYgNC4wMjJ2LTMuMDIyaC0xNi4wMTR2My4wMjJoMy4wNDZsLTMuMDQzIDcuOTQ1aC0uMDA0di4wMWwuMDE1IDEuMDIzaC0uMDE0djEuOTkxaDE2LjAxNHYtMy4wMjNoLTF2LTcuOTQ2aDF6bS01LjkxNCA1LjMwMWMwIC4yMzMtLjAyMy40NDEtLjA2Ni41OTUtLjA0Ny4xNjQtLjA5OS4yNDctLjEyNy4yODRsLS4wNzguMDY5LS4xNTEuMDI2LS4xMTUtLjAxNy0uMTM5LS4xMzdjLS4wMzEtLjA3OC0uMTEyLS4zMzItLjExMi0uNTY2IDAtLjI1NC4wOTEtLjU2MS4xMjYtLjY1NmwuMDY5LS4xNDEuMTA5LS4wODIuMTc4LS4wMjdjLjA3NyAwIC4xMTcuMDE0LjE3Ny4wNTZsLjA4Ny4xNzkuMDUxLjIzNy0uMDA5LjE4em0tMy42OTUtNS4zMDF2Mi44OTNsLTEuMTE2LTIuODkzaDEuMTE2em0tMy4wMjYgNy4wMmgxLjU3M2wuMzUxLjkyNmgtMi4yNTRsLjMzLS45MjZ6bTguNjM1LTQuMzU0Yy0uMjA2LS4yLS40MzEtLjM4LS42OTUtLjUxMi0uMzk2LS4xOTgtLjg1My0uMjk4LTEuMzU1LS4yOTgtLjIxNSAwLS40MjMuMDItLjYyMS4wNTh2LTEuOTE0aDIuNjcxdjIuNjY2eiIvPjwvZz48ZyBpZD0iaWNvbl94NUZfYmciPjxyZWN0IHg9IjEzIiB5PSI0IiBjbGFzcz0ic3QyIiB3aWR0aD0iMSIgaGVpZ2h0PSI4Ii8+PHBhdGggY2xhc3M9InN0MiIgZD0iTTExLjIyNSA4LjM4N2MtLjA3OC0uMjk5LS4xOTktLjU2Mi0uMzYtLjc4NnMtLjM2NS0uNDAxLS42MDktLjUzLS41MzQtLjE5My0uODY2LS4xOTNjLS4xOTggMC0uMzguMDI0LS41NDcuMDczLS4xNjUuMDQ5LS4zMTYuMTE3LS40NTMuMjA1LS4xMzYuMDg4LS4yNTcuMTk0LS4zNjUuMzE4bC0uMTc5LjI1OHYtMy4xNTRoLS44OTN2Ny40MjJoLjg5M3YtLjU3NWwuMTI2LjE3NWMuMDg3LjEwMi4xODkuMTkuMzA0LjI2OS4xMTcuMDc4LjI0OS4xNC4zOTguMTg2LjE0OS4wNDYuMzE0LjA2OC40OTguMDY4LjM1MyAwIC42NjYtLjA3MS45MzctLjIxMi4yNzItLjE0My40OTktLjMzOC42ODItLjU4Ni4xODMtLjI1LjMyMS0uNTQzLjQxNC0uODc5LjA5My0uMzM4LjE0LS43MDMuMTQtMS4wOTctLjAwMS0uMzQyLS4wNC0uNjYzLS4xMi0uOTYyem0tMS40NzktLjYwN2MuMTUxLjA3MS4yODIuMTc2LjM5LjMxNC4xMDkuMTQuMTk0LjMxMy4yNTUuNTE3LjA1MS4xNzQuMDgyLjM3MS4wODkuNTg3bC0uMDA3LjEyNWMwIC4zMjctLjAzMy42Mi0uMS44NjktLjA2Ny4yNDYtLjE2MS40NTMtLjI3OC42MTQtLjExNy4xNjItLjI2LjI4NS0uNDIxLjM2Ni0uMzIyLjE2Mi0uNzYuMTY2LTEuMDY5LjAxNS0uMTUzLS4wNzUtLjI4Ni0uMTc1LS4zOTMtLjI5Ni0uMDg1LS4wOTYtLjE1Ni0uMjE2LS4yMTgtLjM2NyAwIDAtLjE3OS0uNDQ3LS4xNzktLjk0NyAwLS41LjE3OS0xLjAwMi4xNzktMS4wMDIuMDYyLS4xNzcuMTM2LS4zMTguMjI0LS40My4xMTQtLjE0My4yNTYtLjI1OS40MjQtLjM0NS4xNjgtLjA4Ni4zNjUtLjEyOS41ODctLjEyOS4xOSAwIC4zNjQuMDM3LjUxNy4xMDl6Ii8+PHJlY3QgeD0iLjk4NyIgeT0iMiIgY2xhc3M9InN0MiIgd2lkdGg9IjE0LjAxMyIgaGVpZ2h0PSIxLjAyMyIvPjxyZWN0IHg9Ii45ODciIHk9IjEyLjk2OCIgY2xhc3M9InN0MiIgd2lkdGg9IjE0LjAxMyIgaGVpZ2h0PSIxLjAyMyIvPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0xLjk5MSAxMi4wMzFsLjcyOC0yLjAzMWgyLjIxOWwuNzc4IDIuMDMxaDEuMDgybC0yLjQ4NS03LjE1OGgtLjk0MWwtMi40NDEgNy4wODYtLjAyNS4wNzJoMS4wODV6bTEuODI3LTUuNjA5aC4wMjJsLjkxNCAyLjc1M2gtMS44NDFsLjkwNS0yLjc1M3oiLz48L2c+PC9zdmc+) 50% no-repeat}.hc-black .monaco-custom-checkbox.monaco-whole-word,.hc-black .monaco-custom-checkbox.monaco-whole-word:hover,.vs-dark .monaco-custom-checkbox.monaco-whole-word{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc3Qwe29wYWNpdHk6MDtmaWxsOiMyNjI2MjY7fSAuc3Qxe2ZpbGw6IzI2MjYyNjt9IC5zdDJ7ZmlsbDojQzVDNUM1O308L3N0eWxlPjxnIGlkPSJvdXRsaW5lIj48cmVjdCBjbGFzcz0ic3QwIiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTYgNC4wMjJ2LTMuMDIyaC0xNi4wMTR2My4wMjJoMy4wNDZsLTMuMDQzIDcuOTQ1aC0uMDA0di4wMWwuMDE1IDEuMDIzaC0uMDE0djEuOTkxaDE2LjAxNHYtMy4wMjNoLTF2LTcuOTQ2aDF6bS01LjkxNCA1LjMwMWMwIC4yMzMtLjAyMy40NDEtLjA2Ni41OTUtLjA0Ny4xNjQtLjA5OS4yNDctLjEyNy4yODRsLS4wNzguMDY5LS4xNTEuMDI2LS4xMTUtLjAxNy0uMTM5LS4xMzdjLS4wMzEtLjA3OC0uMTEyLS4zMzItLjExMi0uNTY2IDAtLjI1NC4wOTEtLjU2MS4xMjYtLjY1NmwuMDY5LS4xNDEuMTA5LS4wODIuMTc4LS4wMjdjLjA3NyAwIC4xMTcuMDE0LjE3Ny4wNTZsLjA4Ny4xNzkuMDUxLjIzNy0uMDA5LjE4em0tMy42OTUtNS4zMDF2Mi44OTNsLTEuMTE2LTIuODkzaDEuMTE2em0tMy4wMjYgNy4wMmgxLjU3M2wuMzUxLjkyNmgtMi4yNTRsLjMzLS45MjZ6bTguNjM1LTQuMzU0Yy0uMjA2LS4yLS40MzEtLjM4LS42OTUtLjUxMi0uMzk2LS4xOTgtLjg1My0uMjk4LTEuMzU1LS4yOTgtLjIxNSAwLS40MjMuMDItLjYyMS4wNTh2LTEuOTE0aDIuNjcxdjIuNjY2eiIvPjwvZz48ZyBpZD0iaWNvbl94NUZfYmciPjxyZWN0IHg9IjEzIiB5PSI0IiBjbGFzcz0ic3QyIiB3aWR0aD0iMSIgaGVpZ2h0PSI4Ii8+PHBhdGggY2xhc3M9InN0MiIgZD0iTTExLjIyNSA4LjM4N2MtLjA3OC0uMjk5LS4xOTktLjU2Mi0uMzYtLjc4NnMtLjM2NS0uNDAxLS42MDktLjUzLS41MzQtLjE5My0uODY2LS4xOTNjLS4xOTggMC0uMzguMDI0LS41NDcuMDczLS4xNjUuMDQ5LS4zMTYuMTE3LS40NTMuMjA1LS4xMzYuMDg4LS4yNTcuMTk0LS4zNjUuMzE4bC0uMTc5LjI1OHYtMy4xNTRoLS44OTN2Ny40MjJoLjg5M3YtLjU3NWwuMTI2LjE3NWMuMDg3LjEwMi4xODkuMTkuMzA0LjI2OS4xMTcuMDc4LjI0OS4xNC4zOTguMTg2LjE0OS4wNDYuMzE0LjA2OC40OTguMDY4LjM1MyAwIC42NjYtLjA3MS45MzctLjIxMi4yNzItLjE0My40OTktLjMzOC42ODItLjU4Ni4xODMtLjI1LjMyMS0uNTQzLjQxNC0uODc5LjA5My0uMzM4LjE0LS43MDMuMTQtMS4wOTctLjAwMS0uMzQyLS4wNC0uNjYzLS4xMi0uOTYyem0tMS40NzktLjYwN2MuMTUxLjA3MS4yODIuMTc2LjM5LjMxNC4xMDkuMTQuMTk0LjMxMy4yNTUuNTE3LjA1MS4xNzQuMDgyLjM3MS4wODkuNTg3bC0uMDA3LjEyNWMwIC4zMjctLjAzMy42Mi0uMS44NjktLjA2Ny4yNDYtLjE2MS40NTMtLjI3OC42MTQtLjExNy4xNjItLjI2LjI4NS0uNDIxLjM2Ni0uMzIyLjE2Mi0uNzYuMTY2LTEuMDY5LjAxNS0uMTUzLS4wNzUtLjI4Ni0uMTc1LS4zOTMtLjI5Ni0uMDg1LS4wOTYtLjE1Ni0uMjE2LS4yMTgtLjM2NyAwIDAtLjE3OS0uNDQ3LS4xNzktLjk0NyAwLS41LjE3OS0xLjAwMi4xNzktMS4wMDIuMDYyLS4xNzcuMTM2LS4zMTguMjI0LS40My4xMTQtLjE0My4yNTYtLjI1OS40MjQtLjM0NS4xNjgtLjA4Ni4zNjUtLjEyOS41ODctLjEyOS4xOSAwIC4zNjQuMDM3LjUxNy4xMDl6Ii8+PHJlY3QgeD0iLjk4NyIgeT0iMiIgY2xhc3M9InN0MiIgd2lkdGg9IjE0LjAxMyIgaGVpZ2h0PSIxLjAyMyIvPjxyZWN0IHg9Ii45ODciIHk9IjEyLjk2OCIgY2xhc3M9InN0MiIgd2lkdGg9IjE0LjAxMyIgaGVpZ2h0PSIxLjAyMyIvPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0xLjk5MSAxMi4wMzFsLjcyOC0yLjAzMWgyLjIxOWwuNzc4IDIuMDMxaDEuMDgybC0yLjQ4NS03LjE1OGgtLjk0MWwtMi40NDEgNy4wODYtLjAyNS4wNzJoMS4wODV6bTEuODI3LTUuNjA5aC4wMjJsLjkxNCAyLjc1M2gtMS44NDFsLjkwNS0yLjc1M3oiLz48L2c+PC9zdmc+) 50% no-repeat}.vs .monaco-custom-checkbox.monaco-regex{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBvbHlnb24gZmlsbD0iI0Y2RjZGNiIgcG9pbnRzPSIxMy42NCw3LjM5NiAxMi4xNjksMi44OTggMTAuNzA2LDMuNzYxIDExLjA4NywyIDYuNTU3LDIgNi45MzYsMy43NjIgNS40NzMsMi44OTggNCw3LjM5NiA1LjY4Miw3LjU1NCA0LjUxMyw4LjU2MSA1LjAxMyw5IDIsOSAyLDE0IDcsMTQgNywxMC43NDcgNy45NzgsMTEuNjA2IDguODIsOS43MjUgOS42NjEsMTEuNjAyIDEzLjE0NCw4LjU2MiAxMS45NjgsNy41NTQiLz48ZyBmaWxsPSIjNDI0MjQyIj48cGF0aCBkPSJNMTIuMzAxIDYuNTE4bC0yLjc3Mi4yNjIgMi4wODYgMS43ODgtMS41OTQgMS4zOTItMS4yMDEtMi42ODItMS4yMDEgMi42ODItMS41ODMtMS4zOTIgMi4wNzUtMS43ODgtMi43NzEtLjI2Mi42OTYtMi4xMjYgMi4zNTggMS4zOTItLjU5OS0yLjc4NGgyLjA1M2wtLjYwMiAyLjc4MyAyLjM1OS0xLjM5Mi42OTYgMi4xMjd6Ii8+PHJlY3QgeD0iMyIgeT0iMTAiIHdpZHRoPSIzIiBoZWlnaHQ9IjMiLz48L2c+PC9zdmc+) 50% no-repeat}.hc-black .monaco-custom-checkbox.monaco-regex,.hc-black .monaco-custom-checkbox.monaco-regex:hover,.vs-dark .monaco-custom-checkbox.monaco-regex{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBvbHlnb24gZmlsbD0iIzJkMmQzMCIgcG9pbnRzPSIxMy42NCw3LjM5NiAxMi4xNjksMi44OTggMTAuNzA2LDMuNzYxIDExLjA4NywyIDYuNTU3LDIgNi45MzYsMy43NjIgNS40NzMsMi44OTggNCw3LjM5NiA1LjY4Miw3LjU1NCA0LjUxMyw4LjU2MSA1LjAxMyw5IDIsOSAyLDE0IDcsMTQgNywxMC43NDcgNy45NzgsMTEuNjA2IDguODIsOS43MjUgOS42NjEsMTEuNjAyIDEzLjE0NCw4LjU2MiAxMS45NjgsNy41NTQiLz48ZyBmaWxsPSIjQzVDNUM1Ij48cGF0aCBkPSJNMTIuMzAxIDYuNTE4bC0yLjc3Mi4yNjIgMi4wODYgMS43ODgtMS41OTQgMS4zOTItMS4yMDEtMi42ODItMS4yMDEgMi42ODItMS41ODMtMS4zOTIgMi4wNzUtMS43ODgtMi43NzEtLjI2Mi42OTYtMi4xMjYgMi4zNTggMS4zOTItLjU5OS0yLjc4NGgyLjA1M2wtLjYwMiAyLjc4MyAyLjM1OS0xLjM5Mi42OTYgMi4xMjd6Ii8+PHJlY3QgeD0iMyIgeT0iMTAiIHdpZHRoPSIzIiBoZWlnaHQ9IjMiLz48L2c+PC9zdmc+) 50% no-repeat}.monaco-icon-label{display:flex;overflow:hidden;text-overflow:ellipsis}.monaco-icon-label:before{background-size:16px;background-position:0;background-repeat:no-repeat;padding-right:6px;width:16px;height:22px;display:inline-block;-webkit-font-smoothing:antialiased;vertical-align:top;flex-shrink:0}.monaco-icon-label>.monaco-icon-label-description-container{overflow:hidden;text-overflow:ellipsis}.monaco-icon-label>.monaco-icon-label-description-container>.label-name{color:inherit;white-space:pre}.monaco-icon-label>.monaco-icon-label-description-container>.label-description{opacity:.7;margin-left:.5em;font-size:.9em;white-space:pre}.monaco-icon-label.italic>.monaco-icon-label-description-container>.label-description,.monaco-icon-label.italic>.monaco-icon-label-description-container>.label-name{font-style:italic}.monaco-icon-label:after{opacity:.75;font-size:90%;font-weight:600;padding:0 12px 0 5px;margin-left:auto;text-align:center}.monaco-list:focus .selected .monaco-icon-label,.monaco-list:focus .selected .monaco-icon-label:after,.monaco-tree.focused .selected .monaco-icon-label,.monaco-tree.focused .selected .monaco-icon-label:after{color:inherit!important}.monaco-list-row.focused.selected .label-description,.monaco-list-row.selected .label-description,.monaco-tree-row.focused.selected .label-description,.monaco-tree-row.selected .label-description{opacity:.8}.monaco-inputbox{position:relative;display:block;padding:0;-o-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;line-height:auto!important;font-size:inherit}.monaco-inputbox.idle{border:1px solid transparent}.monaco-inputbox>.wrapper>.input,.monaco-inputbox>.wrapper>.mirror{padding:4px}.monaco-inputbox>.wrapper{position:relative;width:100%;height:100%}.monaco-inputbox>.wrapper>.input{display:inline-block;-o-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;width:100%;height:100%;line-height:inherit;border:none;font-family:inherit;font-size:inherit;resize:none;color:inherit}.monaco-inputbox>.wrapper>input{text-overflow:ellipsis}.monaco-inputbox>.wrapper>textarea.input{display:block;overflow:hidden}.monaco-inputbox>.wrapper>.mirror{position:absolute;display:inline-block;width:100%;top:0;left:0;-o-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;white-space:pre-wrap;visibility:hidden;word-wrap:break-word}.monaco-inputbox-container{text-align:right}.monaco-inputbox-container .monaco-inputbox-message{display:inline-block;overflow:hidden;text-align:left;width:100%;-o-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;padding:.4em;font-size:12px;line-height:17px;min-height:34px;margin-top:-1px;word-wrap:break-word}.monaco-inputbox .monaco-action-bar{position:absolute;right:2px;top:4px}.monaco-inputbox .monaco-action-bar .action-item{margin-left:2px}.monaco-inputbox .monaco-action-bar .action-item .icon{background-repeat:no-repeat;width:16px;height:16px}.monaco-keybinding{display:flex;align-items:center;line-height:10px}.monaco-keybinding>.monaco-keybinding-key{display:inline-block;border:1px solid hsla(0,0%,80%,.4);border-bottom-color:hsla(0,0%,73%,.4);border-radius:3px;box-shadow:inset 0 -1px 0 hsla(0,0%,73%,.4);background-color:hsla(0,0%,87%,.4);vertical-align:middle;color:#555;font-size:11px;padding:3px 5px;margin:0 2px}.monaco-keybinding>.monaco-keybinding-key:first-child{margin-left:0}.monaco-keybinding>.monaco-keybinding-key:last-child{margin-right:0}.hc-black .monaco-keybinding>.monaco-keybinding-key,.vs-dark .monaco-keybinding>.monaco-keybinding-key{background-color:hsla(0,0%,50%,.17);color:#ccc;border:1px solid rgba(51,51,51,.6);border-bottom-color:rgba(68,68,68,.6);box-shadow:inset 0 -1px 0 rgba(68,68,68,.6)}.monaco-keybinding>.monaco-keybinding-key-separator{display:inline-block}.monaco-keybinding>.monaco-keybinding-key-chord-separator{width:6px}.monaco-list{position:relative;height:100%;width:100%;white-space:nowrap}.monaco-list.mouse-support{-webkit-user-select:none;-moz-user-select:-moz-none;-ms-user-select:none;-o-user-select:none;user-select:none}.monaco-list>.monaco-scrollable-element{height:100%}.monaco-list-rows{position:relative;width:100%;height:100%}.monaco-list.horizontal-scrolling .monaco-list-rows{width:auto;min-width:100%}.monaco-list-row{position:absolute;-o-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;overflow:hidden;width:100%}.monaco-list.mouse-support .monaco-list-row{cursor:pointer;touch-action:none}.monaco-list-row.scrolling{display:none!important}.monaco-list.element-focused,.monaco-list.selection-multiple,.monaco-list.selection-single{outline:0!important}.monaco-drag-image{display:inline-block;padding:1px 7px;border-radius:10px;font-size:12px;position:absolute}.monaco-list-type-filter{display:flex;align-items:center;position:absolute;border-radius:2px;padding:0 3px;max-width:calc(100% - 10px);text-overflow:ellipsis;overflow:hidden;text-align:right;box-sizing:border-box;cursor:all-scroll;font-size:13px;line-height:18px;height:20px;z-index:1;top:4px}.monaco-list-type-filter.dragging{transition:top .2s,left .2s}.monaco-list-type-filter.ne{right:4px}.monaco-list-type-filter.nw{left:4px}.monaco-list-type-filter>.controls{display:flex;align-items:center;box-sizing:border-box;transition:width .2s;width:0}.monaco-list-type-filter.dragging>.controls,.monaco-list-type-filter:hover>.controls{width:36px}.monaco-list-type-filter>.controls>*{box-sizing:border-box;width:16px;height:16px;margin:0 0 0 2px;flex-shrink:0}.monaco-list-type-filter>.controls>.filter{-webkit-appearance:none;width:16px;height:16px;background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYgOS45OTk1MUgyVjExLjk5OTVINlY5Ljk5OTUxWiIgZmlsbD0iIzRCNEI0QiIvPgo8cGF0aCBkPSJNMTAgN0gyVjlIMTBWN1oiIGZpbGw9IiM0QjRCNEIiLz4KPHBhdGggZD0iTTE0IDRIMlY2SDE0VjRaIiBmaWxsPSIjNEI0QjRCIi8+Cjwvc3ZnPgo=);background-position:50% 50%;cursor:pointer}.monaco-list-type-filter>.controls>.filter:checked{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjAwMDIgMTBINi4wMDAyNFYxMkgxMC4wMDAyVjEwWiIgZmlsbD0iIzQyNDI0MiIvPgo8cGF0aCBkPSJNMTEuOTk5OCA3SDMuOTk5NzZWOUgxMS45OTk4VjdaIiBmaWxsPSIjNDI0MjQyIi8+CjxwYXRoIGQ9Ik0xNCA0SDJWNkgxNFY0WiIgZmlsbD0iIzQyNDI0MiIvPgo8L3N2Zz4K)}.vs-dark .monaco-list-type-filter>.controls>.filter{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYgOS45OTk1MUgyVjExLjk5OTVINlY5Ljk5OTUxWiIgZmlsbD0iI0U4RThFOCIvPgo8cGF0aCBkPSJNMTAgN0gyVjlIMTBWN1oiIGZpbGw9IiNFOEU4RTgiLz4KPHBhdGggZD0iTTE0IDRIMlY2SDE0VjRaIiBmaWxsPSIjRThFOEU4Ii8+Cjwvc3ZnPgo=)}.vs-dark .monaco-list-type-filter>.controls>.filter:checked{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjAwMDIgMTBINi4wMDAyNFYxMkgxMC4wMDAyVjEwWiIgZmlsbD0iI0U4RThFOCIvPgo8cGF0aCBkPSJNMTEuOTk5OCA3SDMuOTk5NzZWOUgxMS45OTk4VjdaIiBmaWxsPSIjRThFOEU4Ii8+CjxwYXRoIGQ9Ik0xNCA0SDJWNkgxNFY0WiIgZmlsbD0iI0U4RThFOCIvPgo8L3N2Zz4K)}.hc-black .monaco-list-type-filter>.controls>.filter{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYgOS45OTk1MUgyVjExLjk5OTVINlY5Ljk5OTUxWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEwIDdIMlY5SDEwVjdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTQgNEgyVjZIMTRWNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=)}.hc-black .monaco-list-type-filter>.controls>.filter:checked{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjAwMDIgMTBINi4wMDAyNFYxMkgxMC4wMDAyVjEwWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTExLjk5OTggN0gzLjk5OTc2VjlIMTEuOTk5OFY3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE0IDRIMlY2SDE0VjRaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K)}.monaco-list-type-filter>.controls>.clear{border:none;background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMyAzIDE2IDE2IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDMgMyAxNiAxNiI+PHBvbHlnb24gZmlsbD0iIzQyNDI0MiIgcG9pbnRzPSIxMi41OTcsMTEuMDQyIDE1LjQsMTMuODQ1IDEzLjg0NCwxNS40IDExLjA0MiwxMi41OTggOC4yMzksMTUuNCA2LjY4MywxMy44NDUgOS40ODUsMTEuMDQyIDYuNjgzLDguMjM5IDguMjM4LDYuNjgzIDExLjA0Miw5LjQ4NiAxMy44NDUsNi42ODMgMTUuNCw4LjIzOSIvPjwvc3ZnPg==);cursor:pointer}.vs-dark .monaco-list-type-filter>.controls>.clear{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMyAzIDE2IDE2IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDMgMyAxNiAxNiI+PHBvbHlnb24gZmlsbD0iI2U4ZThlOCIgcG9pbnRzPSIxMi41OTcsMTEuMDQyIDE1LjQsMTMuODQ1IDEzLjg0NCwxNS40IDExLjA0MiwxMi41OTggOC4yMzksMTUuNCA2LjY4MywxMy44NDUgOS40ODUsMTEuMDQyIDYuNjgzLDguMjM5IDguMjM4LDYuNjgzIDExLjA0Miw5LjQ4NiAxMy44NDUsNi42ODMgMTUuNCw4LjIzOSIvPjwvc3ZnPg==)}.hc-black .monaco-list-type-filter>.controls>.clear{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgaWQ9InN2ZzczMjAiCiAgIHZlcnNpb249IjEuMSIKICAgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAzIDMgMTYgMTYiCiAgIHZpZXdCb3g9IjMgMyAxNiAxNiIKICAgaGVpZ2h0PSIxNiIKICAgd2lkdGg9IjE2Ij4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3MzI2Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZGVmcwogICAgIGlkPSJkZWZzNzMyNCIgLz4KICA8cG9seWdvbgogICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjEiCiAgICAgaWQ9InBvbHlnb243MzE4IgogICAgIHBvaW50cz0iMTIuNTk3LDExLjA0MiAxNS40LDEzLjg0NSAxMy44NDQsMTUuNCAxMS4wNDIsMTIuNTk4IDguMjM5LDE1LjQgNi42ODMsMTMuODQ1IDkuNDg1LDExLjA0MiA2LjY4Myw4LjIzOSA4LjIzOCw2LjY4MyAxMS4wNDIsOS40ODYgMTMuODQ1LDYuNjgzIDE1LjQsOC4yMzkiCiAgICAgZmlsbD0iIzQyNDI0MiIgLz4KPC9zdmc+Cg==)}.monaco-list-type-filter-message{position:absolute;box-sizing:border-box;width:100%;height:100%;top:0;left:0;padding:40px 1em 1em;text-align:center;white-space:normal;opacity:.7;pointer-events:none}.monaco-list-type-filter-message:empty{display:none}.monaco-list-type-filter{cursor:-webkit-grab}.monaco-list-type-filter.dragging{cursor:-webkit-grabbing}.monaco-menu .monaco-action-bar.vertical{margin-left:0;overflow:visible}.monaco-menu .monaco-action-bar.vertical .actions-container{display:block}.monaco-menu .monaco-action-bar.vertical .action-item{padding:0;transform:none;display:-ms-flexbox;display:flex}.monaco-menu .monaco-action-bar.vertical .action-item.active{transform:none}.monaco-menu .monaco-action-bar.vertical .action-menu-item{-ms-flex:1 1 auto;flex:1 1 auto;display:-ms-flexbox;display:flex;height:2em;align-items:center;position:relative}.monaco-menu .monaco-action-bar.vertical .action-label{-ms-flex:1 1 auto;flex:1 1 auto;text-decoration:none;padding:0 1em;background:none;font-size:12px;line-height:1}.monaco-menu .monaco-action-bar.vertical .keybinding,.monaco-menu .monaco-action-bar.vertical .submenu-indicator{display:inline-block;-ms-flex:2 1 auto;flex:2 1 auto;padding:0 1em;text-align:right;font-size:12px;line-height:1}.monaco-menu .monaco-action-bar.vertical .submenu-indicator{height:100%;-webkit-mask:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuNTIwNTEgMTIuMzY0M0w5Ljg3NzkzIDdMNC41MjA1MSAxLjYzNTc0Mkw1LjEzNTc0IDEuMDIwNTA3OEwxMS4xMjIxIDdMNS4xMzU3NCAxMi45Nzk1TDQuNTIwNTEgMTIuMzY0M1oiIGZpbGw9ImJsYWNrIi8+Cjwvc3ZnPgo=) no-repeat 90% 50%/13px 13px;mask:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuNTIwNTEgMTIuMzY0M0w5Ljg3NzkzIDdMNC41MjA1MSAxLjYzNTc0Mkw1LjEzNTc0IDEuMDIwNTA3OEwxMS4xMjIxIDdMNS4xMzU3NCAxMi45Nzk1TDQuNTIwNTEgMTIuMzY0M1oiIGZpbGw9ImJsYWNrIi8+Cjwvc3ZnPgo=) no-repeat 90% 50%/13px 13px}.monaco-menu .monaco-action-bar.vertical .action-item.disabled .keybinding,.monaco-menu .monaco-action-bar.vertical .action-item.disabled .submenu-indicator{opacity:.4}.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator){display:inline-block;-o-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;margin:0}.monaco-menu .monaco-action-bar.vertical .action-item{position:static;overflow:visible}.monaco-menu .monaco-action-bar.vertical .action-item .monaco-submenu{position:absolute}.monaco-menu .monaco-action-bar.vertical .action-label.separator{padding:.5em 0 0;margin-bottom:.5em;width:100%}.monaco-menu .monaco-action-bar.vertical .action-label.separator.text{padding:.7em 1em .1em;font-weight:700;opacity:1}.monaco-menu .monaco-action-bar.vertical .action-label:hover{color:inherit}.monaco-menu .monaco-action-bar.vertical .menu-item-check{position:absolute;visibility:hidden;-webkit-mask:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iLTIgLTIgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgLTIgLTIgMTYgMTYiPjxwb2x5Z29uIGZpbGw9IiM0MjQyNDIiIHBvaW50cz0iOSwwIDQuNSw5IDMsNiAwLDYgMywxMiA2LDEyIDEyLDAiLz48L3N2Zz4=) no-repeat 50% 56%/15px 15px;mask:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iLTIgLTIgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgLTIgLTIgMTYgMTYiPjxwb2x5Z29uIGZpbGw9IiM0MjQyNDIiIHBvaW50cz0iOSwwIDQuNSw5IDMsNiAwLDYgMywxMiA2LDEyIDEyLDAiLz48L3N2Zz4=) no-repeat 50% 56%/15px 15px;width:1em;height:100%}.monaco-menu .monaco-action-bar.vertical .action-menu-item.checked .menu-item-check{visibility:visible}.context-view.monaco-menu-container{outline:0;border:none;-webkit-animation:fadeIn 83ms linear;animation:fadeIn 83ms linear}.context-view.monaco-menu-container .monaco-action-bar.vertical:focus,.context-view.monaco-menu-container .monaco-action-bar.vertical :focus,.context-view.monaco-menu-container :focus{outline:0}.monaco-menu .monaco-action-bar.vertical .action-item{border:1px solid transparent}.hc-black .context-view.monaco-menu-container{box-shadow:none}.hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused{background:none}.menubar{display:flex;flex-shrink:1;box-sizing:border-box;height:30px;overflow:hidden;flex-wrap:wrap}.fullscreen .menubar{margin:0;padding:0 5px}.menubar>.menubar-menu-button{align-items:center;box-sizing:border-box;padding:0 8px;cursor:default;-webkit-app-region:no-drag;zoom:1;white-space:nowrap;outline:0}.menubar .menubar-menu-items-holder{position:absolute;left:0;opacity:1;z-index:2000}.menubar .menubar-menu-items-holder.monaco-menu-container{outline:0;border:none}.menubar .menubar-menu-items-holder.monaco-menu-container :focus{outline:0}.menubar .toolbar-toggle-more{background-position:50%;background-repeat:no-repeat;background-size:14px;width:20px;height:100%;display:inline-block;padding:0;-webkit-mask:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PGRlZnM+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudCwuaWNvbi12cy1vdXR7ZmlsbDojZjZmNmY2O30uaWNvbi1jYW52YXMtdHJhbnNwYXJlbnR7b3BhY2l0eTowO30uaWNvbi12cy1iZ3tmaWxsOiM0MjQyNDI7fTwvc3R5bGU+PC9kZWZzPjx0aXRsZT5FbGxpcHNpc19ib2xkXzE2eDwvdGl0bGU+PGcgaWQ9ImNhbnZhcyI+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYsMFYxNkgwVjBaIi8+PC9nPjxnIGlkPSJvdXRsaW5lIiBzdHlsZT0iZGlzcGxheTogbm9uZTsiPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTYsNy41QTIuNSwyLjUsMCwxLDEsMy41LDUsMi41LDIuNSwwLDAsMSw2LDcuNVpNOC41LDVBMi41LDIuNSwwLDEsMCwxMSw3LjUsMi41LDIuNSwwLDAsMCw4LjUsNVptNSwwQTIuNSwyLjUsMCwxLDAsMTYsNy41LDIuNSwyLjUsMCwwLDAsMTMuNSw1WiIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ii8+PC9nPjxnIGlkPSJpY29uQmciPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNNSw3LjVBMS41LDEuNSwwLDEsMSwzLjUsNiwxLjUsMS41LDAsMCwxLDUsNy41Wk04LjUsNkExLjUsMS41LDAsMSwwLDEwLDcuNSwxLjUsMS41LDAsMCwwLDguNSw2Wm01LDBBMS41LDEuNSwwLDEsMCwxNSw3LjUsMS41LDEuNSwwLDAsMCwxMy41LDZaIi8+PC9nPjwvc3ZnPg==) no-repeat 50% 55%/14px 14px;mask:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PGRlZnM+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudCwuaWNvbi12cy1vdXR7ZmlsbDojZjZmNmY2O30uaWNvbi1jYW52YXMtdHJhbnNwYXJlbnR7b3BhY2l0eTowO30uaWNvbi12cy1iZ3tmaWxsOiM0MjQyNDI7fTwvc3R5bGU+PC9kZWZzPjx0aXRsZT5FbGxpcHNpc19ib2xkXzE2eDwvdGl0bGU+PGcgaWQ9ImNhbnZhcyI+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYsMFYxNkgwVjBaIi8+PC9nPjxnIGlkPSJvdXRsaW5lIiBzdHlsZT0iZGlzcGxheTogbm9uZTsiPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTYsNy41QTIuNSwyLjUsMCwxLDEsMy41LDUsMi41LDIuNSwwLDAsMSw2LDcuNVpNOC41LDVBMi41LDIuNSwwLDEsMCwxMSw3LjUsMi41LDIuNSwwLDAsMCw4LjUsNVptNSwwQTIuNSwyLjUsMCwxLDAsMTYsNy41LDIuNSwyLjUsMCwwLDAsMTMuNSw1WiIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ii8+PC9nPjxnIGlkPSJpY29uQmciPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNNSw3LjVBMS41LDEuNSwwLDEsMSwzLjUsNiwxLjUsMS41LDAsMCwxLDUsNy41Wk04LjUsNkExLjUsMS41LDAsMSwwLDEwLDcuNSwxLjUsMS41LDAsMCwwLDguNSw2Wm01LDBBMS41LDEuNSwwLDEsMCwxNSw3LjUsMS41LDEuNSwwLDAsMCwxMy41LDZaIi8+PC9nPjwvc3ZnPg==) no-repeat 50% 55%/14px 14px}.monaco-progress-container{width:100%;height:5px;overflow:hidden}.monaco-progress-container .progress-bit{width:2%;height:5px;position:absolute;left:0;display:none}.monaco-progress-container.active .progress-bit{display:inherit}.monaco-progress-container.discrete .progress-bit{left:0;transition:width .1s linear}.monaco-progress-container.discrete.done .progress-bit{width:100%}.monaco-progress-container.infinite .progress-bit{animation-name:progress;animation-duration:4s;animation-iteration-count:infinite;animation-timing-function:linear;-ms-animation-name:progress;-ms-animation-duration:4s;-ms-animation-iteration-count:infinite;-ms-animation-timing-function:linear;-webkit-animation-name:progress;-webkit-animation-duration:4s;-webkit-animation-iteration-count:infinite;-webkit-animation-timing-function:linear;-moz-animation-name:progress;-moz-animation-duration:4s;-moz-animation-iteration-count:infinite;-moz-animation-timing-function:linear;will-change:transform}@keyframes progress{0%{transform:translateX(0) scaleX(1)}50%{transform:translateX(2500%) scaleX(3)}to{transform:translateX(4950%) scaleX(1)}}@-webkit-keyframes progress{0%{transform:translateX(0) scaleX(1)}50%{transform:translateX(2500%) scaleX(3)}to{transform:translateX(4950%) scaleX(1)}}.monaco-sash{position:absolute;z-index:35;touch-action:none}.monaco-sash.disabled{pointer-events:none}.monaco-sash.vertical{cursor:ew-resize;top:0;width:4px;height:100%}.monaco-sash.mac.vertical{cursor:col-resize}.monaco-sash.vertical.minimum{cursor:e-resize}.monaco-sash.vertical.maximum{cursor:w-resize}.monaco-sash.horizontal{cursor:ns-resize;left:0;width:100%;height:4px}.monaco-sash.mac.horizontal{cursor:row-resize}.monaco-sash.horizontal.minimum{cursor:s-resize}.monaco-sash.horizontal.maximum{cursor:n-resize}.monaco-sash:not(.disabled).orthogonal-end:after,.monaco-sash:not(.disabled).orthogonal-start:before{content:" ";height:8px;width:8px;z-index:100;display:block;cursor:all-scroll;position:absolute}.monaco-sash.orthogonal-start.vertical:before{left:-2px;top:-4px}.monaco-sash.orthogonal-end.vertical:after{left:-2px;bottom:-4px}.monaco-sash.orthogonal-start.horizontal:before{top:-2px;left:-4px}.monaco-sash.orthogonal-end.horizontal:after{top:-2px;right:-4px}.monaco-sash.disabled{cursor:default!important;pointer-events:none!important}.monaco-sash.touch.vertical{width:20px}.monaco-sash.touch.horizontal{height:20px}.monaco-sash.debug{background:cyan}.monaco-sash.debug.disabled{background:rgba(0,255,255,.2)}.monaco-sash.debug:not(.disabled).orthogonal-end:after,.monaco-sash.debug:not(.disabled).orthogonal-start:before{background:red}.monaco-scrollable-element>.scrollbar>.up-arrow{background:url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTEgMTEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTkuNDgwNDYsOC45NjE1bDEuMjYsLTEuMjZsLTUuMDQsLTUuMDRsLTUuNDYsNS4wNGwxLjI2LDEuMjZsNC4yLC0zLjc4bDMuNzgsMy43OHoiIGZpbGw9IiM0MjQyNDIiLz48L3N2Zz4=);cursor:pointer}.monaco-scrollable-element>.scrollbar>.down-arrow{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMSAxMSI+PHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTE4MCA1LjQ5MDQ1OTkxODk3NTgzLDUuODExNTAwMDcyNDc5MjQ4KSIgZmlsbD0iIzQyNDI0MiIgZD0ibTkuNDgwNDYsOC45NjE1bDEuMjYsLTEuMjZsLTUuMDQsLTUuMDRsLTUuNDYsNS4wNGwxLjI2LDEuMjZsNC4yLC0zLjc4bDMuNzgsMy43OHoiLz48L3N2Zz4=);cursor:pointer}.monaco-scrollable-element>.scrollbar>.left-arrow{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMSAxMSI+PHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDUuNDkwNDU5OTE4OTc1ODMxLDUuNDMxMzgyMTc5MjYwMjU0KSIgZmlsbD0iIzQyNDI0MiIgZD0ibTkuNDgwNDYsOC41ODEzOGwxLjI2LC0xLjI2bC01LjA0LC01LjA0bC01LjQ2LDUuMDRsMS4yNiwxLjI2bDQuMiwtMy43OGwzLjc4LDMuNzh6Ii8+PC9zdmc+);cursor:pointer}.monaco-scrollable-element>.scrollbar>.right-arrow{background:url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTEgMTEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoOTAgNS42MTcxNjUwODg2NTM1NjQ1LDUuNTU4MDg5NzMzMTIzNzgpICIgZmlsbD0iIzQyNDI0MiIgZD0ibTkuNjA3MTcsOC43MDgwOWwxLjI2LC0xLjI2bC01LjA0LC01LjA0bC01LjQ2LDUuMDRsMS4yNiwxLjI2bDQuMiwtMy43OGwzLjc4LDMuNzh6Ii8+PC9zdmc+);cursor:pointer}.hc-black .monaco-scrollable-element>.scrollbar>.up-arrow,.vs-dark .monaco-scrollable-element>.scrollbar>.up-arrow{background:url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTEgMTEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTkuNDgwNDYsOC45NjE1bDEuMjYsLTEuMjZsLTUuMDQsLTUuMDRsLTUuNDYsNS4wNGwxLjI2LDEuMjZsNC4yLC0zLjc4bDMuNzgsMy43OHoiIGZpbGw9IiNFOEU4RTgiLz48L3N2Zz4=)}.hc-black .monaco-scrollable-element>.scrollbar>.down-arrow,.vs-dark .monaco-scrollable-element>.scrollbar>.down-arrow{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMSAxMSI+PHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTE4MCA1LjQ5MDQ1OTkxODk3NTgzLDUuODExNTAwMDcyNDc5MjQ4KSIgZmlsbD0iI0U4RThFOCIgZD0ibTkuNDgwNDYsOC45NjE1bDEuMjYsLTEuMjZsLTUuMDQsLTUuMDRsLTUuNDYsNS4wNGwxLjI2LDEuMjZsNC4yLC0zLjc4bDMuNzgsMy43OHoiLz48L3N2Zz4=)}.hc-black .monaco-scrollable-element>.scrollbar>.left-arrow,.vs-dark .monaco-scrollable-element>.scrollbar>.left-arrow{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMSAxMSI+PHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDUuNDkwNDU5OTE4OTc1ODMxLDUuNDMxMzgyMTc5MjYwMjU0KSIgZmlsbD0iI0U4RThFOCIgZD0ibTkuNDgwNDYsOC41ODEzOGwxLjI2LC0xLjI2bC01LjA0LC01LjA0bC01LjQ2LDUuMDRsMS4yNiwxLjI2bDQuMiwtMy43OGwzLjc4LDMuNzh6Ii8+PC9zdmc+)}.hc-black .monaco-scrollable-element>.scrollbar>.right-arrow,.vs-dark .monaco-scrollable-element>.scrollbar>.right-arrow{background:url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTEgMTEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoOTAgNS42MTcxNjUwODg2NTM1NjQ1LDUuNTU4MDg5NzMzMTIzNzgpICIgZmlsbD0iI0U4RThFOCIgZD0ibTkuNjA3MTcsOC43MDgwOWwxLjI2LC0xLjI2bC01LjA0LC01LjA0bC01LjQ2LDUuMDRsMS4yNiwxLjI2bDQuMiwtMy43OGwzLjc4LDMuNzh6Ii8+PC9zdmc+)}.monaco-scrollable-element>.visible{opacity:1;background:transparent;transition:opacity .1s linear}.monaco-scrollable-element>.invisible{opacity:0;pointer-events:none}.monaco-scrollable-element>.invisible.fade{transition:opacity .8s linear}.monaco-scrollable-element>.shadow{position:absolute;display:none}.monaco-scrollable-element>.shadow.top{display:block;top:0;left:3px;height:3px;width:100%;box-shadow:inset 0 6px 6px -6px #ddd}.monaco-scrollable-element>.shadow.left{display:block;top:3px;left:0;height:100%;width:3px;box-shadow:inset 6px 0 6px -6px #ddd}.monaco-scrollable-element>.shadow.top-left-corner{display:block;top:0;left:0;height:3px;width:3px}.monaco-scrollable-element>.shadow.top.left{box-shadow:inset 6px 6px 6px -6px #ddd}.vs .monaco-scrollable-element>.scrollbar>.slider{background:hsla(0,0%,39%,.4)}.vs-dark .monaco-scrollable-element>.scrollbar>.slider{background:hsla(0,0%,47%,.4)}.hc-black .monaco-scrollable-element>.scrollbar>.slider{background:rgba(111,195,223,.6)}.monaco-scrollable-element>.scrollbar>.slider:hover{background:hsla(0,0%,39%,.7)}.hc-black .monaco-scrollable-element>.scrollbar>.slider:hover{background:rgba(111,195,223,.8)}.monaco-scrollable-element>.scrollbar>.slider.active{background:rgba(0,0,0,.6)}.vs-dark .monaco-scrollable-element>.scrollbar>.slider.active{background:hsla(0,0%,75%,.4)}.hc-black .monaco-scrollable-element>.scrollbar>.slider.active{background:#6fc3df}.vs-dark .monaco-scrollable-element .shadow.top{box-shadow:none}.vs-dark .monaco-scrollable-element .shadow.left{box-shadow:inset 6px 0 6px -6px #000}.vs-dark .monaco-scrollable-element .shadow.top.left{box-shadow:inset 6px 6px 6px -6px #000}.hc-black .monaco-scrollable-element .shadow.left,.hc-black .monaco-scrollable-element .shadow.top,.hc-black .monaco-scrollable-element .shadow.top.left{box-shadow:none}.monaco-split-view2{position:relative;width:100%;height:100%}.monaco-split-view2>.sash-container{position:absolute;width:100%;height:100%;pointer-events:none}.monaco-split-view2>.sash-container>.monaco-sash{pointer-events:auto}.monaco-split-view2>.split-view-container{display:flex;width:100%;height:100%;white-space:nowrap}.monaco-split-view2.vertical>.split-view-container{flex-direction:column}.monaco-split-view2.horizontal>.split-view-container{flex-direction:row}.monaco-split-view2>.split-view-container>.split-view-view{white-space:normal;flex:none;position:relative}.monaco-split-view2.vertical>.split-view-container>.split-view-view{width:100%}.monaco-split-view2.horizontal>.split-view-container>.split-view-view{height:100%;display:inline-block}.monaco-split-view2.separator-border>.split-view-container>.split-view-view:not(:first-child):before{content:" ";position:absolute;top:0;left:0;z-index:5;pointer-events:none;background-color:var(--separator-border)}.monaco-split-view2.separator-border.horizontal>.split-view-container>.split-view-view:not(:first-child):before{height:100%;width:1px}.monaco-split-view2.separator-border.vertical>.split-view-container>.split-view-view:not(:first-child):before{height:1px;width:100%}.monaco-tl-row{display:flex;height:100%;align-items:center}.monaco-tl-contents,.monaco-tl-twistie{height:100%}.monaco-tl-twistie{font-size:10px;text-align:right;margin-right:6px;flex-shrink:0;width:16px}.monaco-tl-contents{flex:1;overflow:hidden}.monaco-tl-twistie.collapsible{background-size:16px;background-position:3px 50%;background-repeat:no-repeat;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iIzY0NjQ2NSIgZD0iTTExIDEwSDUuMzQ0TDExIDQuNDE0VjEweiIvPjwvc3ZnPg==)}.monaco-tl-twistie.collapsible.collapsed:not(.loading){display:inline-block;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iIzY0NjQ2NSIgZD0iTTYgNHY4bDQtNC00LTR6bTEgMi40MTRMOC41ODYgOCA3IDkuNTg2VjYuNDE0eiIvPjwvc3ZnPg==)}.vs-dark .monaco-tl-twistie.collapsible:not(.loading){background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iI0U4RThFOCIgZD0iTTExIDEwSDUuMzQ0TDExIDQuNDE0VjEweiIvPjwvc3ZnPg==)}.vs-dark .monaco-tl-twistie.collapsible.collapsed:not(.loading){background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iI0U4RThFOCIgZD0iTTYgNHY4bDQtNC00LTR6bTEgMi40MTRMOC41ODYgOCA3IDkuNTg2VjYuNDE0eiIvPjwvc3ZnPg==)}.hc-black .monaco-tl-twistie.collapsible:not(.loading){background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTExIDEwLjA3aC01LjY1Nmw1LjY1Ni01LjY1NnY1LjY1NnoiLz48L3N2Zz4=)}.hc-black .monaco-tl-twistie.collapsible.collapsed:not(.loading){background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTYgNHY4bDQtNC00LTR6bTEgMi40MTRsMS41ODYgMS41ODYtMS41ODYgMS41ODZ2LTMuMTcyeiIvPjwvc3ZnPg==)}.monaco-tl-twistie.loading{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBzdGFuZGFsb25lPSdubycgPz4KPHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZlcnNpb249JzEuMScgd2lkdGg9JzEwcHgnIGhlaWdodD0nMTBweCc+Cgk8c3R5bGU+CiAgICBjaXJjbGUgewogICAgICBhbmltYXRpb246IGJhbGwgMC42cyBsaW5lYXIgaW5maW5pdGU7CiAgICB9CgogICAgY2lyY2xlOm50aC1jaGlsZCgyKSB7IGFuaW1hdGlvbi1kZWxheTogMC4wNzVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDMpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjE1czsgfQogICAgY2lyY2xlOm50aC1jaGlsZCg0KSB7IGFuaW1hdGlvbi1kZWxheTogMC4yMjVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDUpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjNzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDYpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjM3NXM7IH0KICAgIGNpcmNsZTpudGgtY2hpbGQoNykgeyBhbmltYXRpb24tZGVsYXk6IDAuNDVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDgpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjUyNXM7IH0KCiAgICBAa2V5ZnJhbWVzIGJhbGwgewogICAgICBmcm9tIHsgb3BhY2l0eTogMTsgfQogICAgICB0byB7IG9wYWNpdHk6IDAuMzsgfQogICAgfQoJPC9zdHlsZT4KCTxnPgoJCTxjaXJjbGUgY3g9JzUnIGN5PScxJyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzcuODI4NCcgY3k9JzIuMTcxNicgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PSc5JyBjeT0nNScgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PSc3LjgyODQnIGN5PSc3LjgyODQnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nNScgY3k9JzknIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nMi4xNzE2JyBjeT0nNy44Mjg0JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzEnIGN5PSc1JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzIuMTcxNicgY3k9JzIuMTcxNicgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCTwvZz4KPC9zdmc+Cg==);background-position:0}.vs-dark .monaco-tl-twistie.loading{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBzdGFuZGFsb25lPSdubycgPz4KPHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZlcnNpb249JzEuMScgd2lkdGg9JzEwcHgnIGhlaWdodD0nMTBweCc+Cgk8c3R5bGU+CiAgICBjaXJjbGUgewogICAgICBhbmltYXRpb246IGJhbGwgMC42cyBsaW5lYXIgaW5maW5pdGU7CiAgICB9CgogICAgY2lyY2xlOm50aC1jaGlsZCgyKSB7IGFuaW1hdGlvbi1kZWxheTogMC4wNzVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDMpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjE1czsgfQogICAgY2lyY2xlOm50aC1jaGlsZCg0KSB7IGFuaW1hdGlvbi1kZWxheTogMC4yMjVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDUpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjNzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDYpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjM3NXM7IH0KICAgIGNpcmNsZTpudGgtY2hpbGQoNykgeyBhbmltYXRpb24tZGVsYXk6IDAuNDVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDgpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjUyNXM7IH0KCiAgICBAa2V5ZnJhbWVzIGJhbGwgewogICAgICBmcm9tIHsgb3BhY2l0eTogMTsgfQogICAgICB0byB7IG9wYWNpdHk6IDAuMzsgfQogICAgfQoJPC9zdHlsZT4KCTxnIHN0eWxlPSJmaWxsOmdyZXk7Ij4KCQk8Y2lyY2xlIGN4PSc1JyBjeT0nMScgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PSc3LjgyODQnIGN5PScyLjE3MTYnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nOScgY3k9JzUnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nNy44Mjg0JyBjeT0nNy44Mjg0JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzUnIGN5PSc5JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzIuMTcxNicgY3k9JzcuODI4NCcgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PScxJyBjeT0nNScgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PScyLjE3MTYnIGN5PScyLjE3MTYnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+Cgk8L2c+Cjwvc3ZnPgo=)}.hc-black .monaco-tl-twistie.loading{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBzdGFuZGFsb25lPSdubycgPz4KPHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZlcnNpb249JzEuMScgd2lkdGg9JzEwcHgnIGhlaWdodD0nMTBweCc+Cgk8c3R5bGU+CiAgICBjaXJjbGUgewogICAgICBhbmltYXRpb246IGJhbGwgMC42cyBsaW5lYXIgaW5maW5pdGU7CiAgICB9CgogICAgY2lyY2xlOm50aC1jaGlsZCgyKSB7IGFuaW1hdGlvbi1kZWxheTogMC4wNzVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDMpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjE1czsgfQogICAgY2lyY2xlOm50aC1jaGlsZCg0KSB7IGFuaW1hdGlvbi1kZWxheTogMC4yMjVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDUpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjNzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDYpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjM3NXM7IH0KICAgIGNpcmNsZTpudGgtY2hpbGQoNykgeyBhbmltYXRpb24tZGVsYXk6IDAuNDVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDgpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjUyNXM7IH0KCiAgICBAa2V5ZnJhbWVzIGJhbGwgewogICAgICBmcm9tIHsgb3BhY2l0eTogMTsgfQogICAgICB0byB7IG9wYWNpdHk6IDAuMzsgfQogICAgfQoJPC9zdHlsZT4KCTxnIHN0eWxlPSJmaWxsOndoaXRlOyI+CgkJPGNpcmNsZSBjeD0nNScgY3k9JzEnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nNy44Mjg0JyBjeT0nMi4xNzE2JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzknIGN5PSc1JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzcuODI4NCcgY3k9JzcuODI4NCcgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PSc1JyBjeT0nOScgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PScyLjE3MTYnIGN5PSc3LjgyODQnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nMScgY3k9JzUnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nMi4xNzE2JyBjeT0nMi4xNzE2JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJPC9nPgo8L3N2Zz4K)}.monaco-quick-open-widget{position:absolute;width:600px;z-index:2000;padding-bottom:6px;left:50%;margin-left:-300px}.monaco-quick-open-widget .monaco-progress-container{position:absolute;left:0;top:38px;z-index:1;height:2px}.monaco-quick-open-widget .monaco-progress-container .progress-bit{height:2px}.monaco-quick-open-widget .quick-open-input{width:588px;border:none;margin:6px}.monaco-quick-open-widget .quick-open-input .monaco-inputbox{width:100%;height:25px}.monaco-quick-open-widget .quick-open-result-count{position:absolute;left:-10000px}.monaco-quick-open-widget .quick-open-tree{line-height:22px}.monaco-quick-open-widget .quick-open-tree .monaco-tree-row>.content>.sub-content{overflow:hidden}.monaco-quick-open-widget.content-changing .quick-open-tree .monaco-scrollable-element .slider{display:none}.monaco-quick-open-widget .quick-open-tree .quick-open-entry{overflow:hidden;text-overflow:ellipsis;display:flex;flex-direction:column;height:100%}.monaco-quick-open-widget .quick-open-tree .quick-open-entry>.quick-open-row{display:flex;align-items:center}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon{overflow:hidden;width:16px;height:16px;margin-right:4px;display:inline-block;vertical-align:middle;flex-shrink:0}.monaco-quick-open-widget .quick-open-tree .monaco-icon-label,.monaco-quick-open-widget .quick-open-tree .monaco-icon-label .monaco-icon-label-description-container{flex:1}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .monaco-highlighted-label span{opacity:1}.monaco-quick-open-widget .quick-open-tree .quick-open-entry-meta{opacity:.7;line-height:normal}.monaco-quick-open-widget .quick-open-tree .content.has-group-label .quick-open-entry-keybinding{margin-right:8px}.monaco-quick-open-widget .quick-open-tree .quick-open-entry-keybinding .monaco-keybinding-key{vertical-align:text-bottom}.monaco-quick-open-widget .quick-open-tree .results-group{margin-right:18px}.monaco-quick-open-widget .quick-open-tree .focused .monaco-tree-row.focused>.content.has-actions>.results-group,.monaco-quick-open-widget .quick-open-tree .monaco-tree-row.focused>.content.has-actions>.results-group,.monaco-quick-open-widget .quick-open-tree .monaco-tree-row:hover:not(.highlighted)>.content.has-actions>.results-group{margin-right:0}.monaco-quick-open-widget .quick-open-tree .results-group-separator{border-top-width:1px;border-top-style:solid;box-sizing:border-box;margin-left:-11px;padding-left:11px}.monaco-tree .monaco-tree-row>.content.actions{position:relative;display:flex}.monaco-tree .monaco-tree-row>.content.actions>.sub-content{flex:1}.monaco-tree .monaco-tree-row>.content.actions .action-item{margin:0}.monaco-tree .monaco-tree-row>.content.actions>.primary-action-bar{line-height:22px;display:none;padding:0 .8em 0 .4em}.monaco-tree .monaco-tree-row.focused>.content.has-actions>.primary-action-bar{width:0;display:block}.monaco-tree.focused .monaco-tree-row.focused>.content.has-actions>.primary-action-bar,.monaco-tree .monaco-tree-row:hover:not(.highlighted)>.content.has-actions>.primary-action-bar,.monaco-tree .monaco-tree-row>.content.has-actions.more>.primary-action-bar{width:inherit;display:block}.monaco-tree .monaco-tree-row>.content.actions>.primary-action-bar .action-label{margin-right:.4em;margin-top:4px;background-repeat:no-repeat;width:16px;height:16px}.monaco-quick-open-widget .quick-open-tree .monaco-highlighted-label .highlight{font-weight:700}.monaco-tree{height:100%;width:100%;white-space:nowrap;-webkit-user-select:none;-moz-user-select:-moz-none;-ms-user-select:none;-o-user-select:none;user-select:none;position:relative}.monaco-tree>.monaco-scrollable-element{height:100%}.monaco-tree>.monaco-scrollable-element>.monaco-tree-wrapper{height:100%;width:100%;position:relative}.monaco-tree .monaco-tree-rows{position:absolute;width:100%;height:100%}.monaco-tree .monaco-tree-rows>.monaco-tree-row{-o-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;cursor:pointer;overflow:hidden;width:100%;touch-action:none}.monaco-tree .monaco-tree-rows>.monaco-tree-row>.content{position:relative;height:100%}.monaco-tree-drag-image{display:inline-block;padding:1px 7px;border-radius:10px;font-size:12px;position:absolute}.monaco-tree .monaco-tree-rows>.monaco-tree-row.scrolling{display:none}.monaco-tree .monaco-tree-rows.show-twisties>.monaco-tree-row.has-children>.content:before{content:" ";position:absolute;display:block;background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iIzY0NjQ2NSIgZD0iTTYgNHY4bDQtNC00LTR6bTEgMi40MTRMOC41ODYgOCA3IDkuNTg2VjYuNDE0eiIvPjwvc3ZnPg==) 50% 50% no-repeat;width:16px;height:100%;top:0;left:-16px}.monaco-tree .monaco-tree-rows.show-twisties>.monaco-tree-row.expanded>.content:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iIzY0NjQ2NSIgZD0iTTExIDEwSDUuMzQ0TDExIDQuNDE0VjEweiIvPjwvc3ZnPg==)}.monaco-tree .monaco-tree-rows>.monaco-tree-row.has-children.loading>.content:before{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBzdGFuZGFsb25lPSdubycgPz4KPHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZlcnNpb249JzEuMScgd2lkdGg9JzEwcHgnIGhlaWdodD0nMTBweCc+Cgk8c3R5bGU+CiAgICBjaXJjbGUgewogICAgICBhbmltYXRpb246IGJhbGwgMC42cyBsaW5lYXIgaW5maW5pdGU7CiAgICB9CgogICAgY2lyY2xlOm50aC1jaGlsZCgyKSB7IGFuaW1hdGlvbi1kZWxheTogMC4wNzVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDMpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjE1czsgfQogICAgY2lyY2xlOm50aC1jaGlsZCg0KSB7IGFuaW1hdGlvbi1kZWxheTogMC4yMjVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDUpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjNzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDYpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjM3NXM7IH0KICAgIGNpcmNsZTpudGgtY2hpbGQoNykgeyBhbmltYXRpb24tZGVsYXk6IDAuNDVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDgpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjUyNXM7IH0KCiAgICBAa2V5ZnJhbWVzIGJhbGwgewogICAgICBmcm9tIHsgb3BhY2l0eTogMTsgfQogICAgICB0byB7IG9wYWNpdHk6IDAuMzsgfQogICAgfQoJPC9zdHlsZT4KCTxnPgoJCTxjaXJjbGUgY3g9JzUnIGN5PScxJyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzcuODI4NCcgY3k9JzIuMTcxNicgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PSc5JyBjeT0nNScgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PSc3LjgyODQnIGN5PSc3LjgyODQnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nNScgY3k9JzknIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nMi4xNzE2JyBjeT0nNy44Mjg0JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzEnIGN5PSc1JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzIuMTcxNicgY3k9JzIuMTcxNicgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCTwvZz4KPC9zdmc+Cg==)}.monaco-tree.highlighted .monaco-tree-rows>.monaco-tree-row:not(.highlighted){opacity:.3}.vs-dark .monaco-tree .monaco-tree-rows.show-twisties>.monaco-tree-row.has-children>.content:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iI0U4RThFOCIgZD0iTTYgNHY4bDQtNC00LTR6bTEgMi40MTRMOC41ODYgOCA3IDkuNTg2VjYuNDE0eiIvPjwvc3ZnPg==)}.vs-dark .monaco-tree .monaco-tree-rows.show-twisties>.monaco-tree-row.expanded>.content:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iI0U4RThFOCIgZD0iTTExIDEwSDUuMzQ0TDExIDQuNDE0VjEweiIvPjwvc3ZnPg==)}.vs-dark .monaco-tree .monaco-tree-rows>.monaco-tree-row.has-children.loading>.content:before{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBzdGFuZGFsb25lPSdubycgPz4KPHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZlcnNpb249JzEuMScgd2lkdGg9JzEwcHgnIGhlaWdodD0nMTBweCc+Cgk8c3R5bGU+CiAgICBjaXJjbGUgewogICAgICBhbmltYXRpb246IGJhbGwgMC42cyBsaW5lYXIgaW5maW5pdGU7CiAgICB9CgogICAgY2lyY2xlOm50aC1jaGlsZCgyKSB7IGFuaW1hdGlvbi1kZWxheTogMC4wNzVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDMpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjE1czsgfQogICAgY2lyY2xlOm50aC1jaGlsZCg0KSB7IGFuaW1hdGlvbi1kZWxheTogMC4yMjVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDUpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjNzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDYpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjM3NXM7IH0KICAgIGNpcmNsZTpudGgtY2hpbGQoNykgeyBhbmltYXRpb24tZGVsYXk6IDAuNDVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDgpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjUyNXM7IH0KCiAgICBAa2V5ZnJhbWVzIGJhbGwgewogICAgICBmcm9tIHsgb3BhY2l0eTogMTsgfQogICAgICB0byB7IG9wYWNpdHk6IDAuMzsgfQogICAgfQoJPC9zdHlsZT4KCTxnIHN0eWxlPSJmaWxsOmdyZXk7Ij4KCQk8Y2lyY2xlIGN4PSc1JyBjeT0nMScgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PSc3LjgyODQnIGN5PScyLjE3MTYnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nOScgY3k9JzUnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nNy44Mjg0JyBjeT0nNy44Mjg0JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzUnIGN5PSc5JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzIuMTcxNicgY3k9JzcuODI4NCcgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PScxJyBjeT0nNScgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PScyLjE3MTYnIGN5PScyLjE3MTYnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+Cgk8L2c+Cjwvc3ZnPgo=)}.hc-black .monaco-tree .monaco-tree-rows.show-twisties>.monaco-tree-row.has-children>.content:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTYgNHY4bDQtNC00LTR6bTEgMi40MTRsMS41ODYgMS41ODYtMS41ODYgMS41ODZ2LTMuMTcyeiIvPjwvc3ZnPg==)}.hc-black .monaco-tree .monaco-tree-rows.show-twisties>.monaco-tree-row.expanded>.content:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTExIDEwLjA3aC01LjY1Nmw1LjY1Ni01LjY1NnY1LjY1NnoiLz48L3N2Zz4=)}.hc-black .monaco-tree .monaco-tree-rows>.monaco-tree-row.has-children.loading>.content:before{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBzdGFuZGFsb25lPSdubycgPz4KPHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZlcnNpb249JzEuMScgd2lkdGg9JzEwcHgnIGhlaWdodD0nMTBweCc+Cgk8c3R5bGU+CiAgICBjaXJjbGUgewogICAgICBhbmltYXRpb246IGJhbGwgMC42cyBsaW5lYXIgaW5maW5pdGU7CiAgICB9CgogICAgY2lyY2xlOm50aC1jaGlsZCgyKSB7IGFuaW1hdGlvbi1kZWxheTogMC4wNzVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDMpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjE1czsgfQogICAgY2lyY2xlOm50aC1jaGlsZCg0KSB7IGFuaW1hdGlvbi1kZWxheTogMC4yMjVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDUpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjNzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDYpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjM3NXM7IH0KICAgIGNpcmNsZTpudGgtY2hpbGQoNykgeyBhbmltYXRpb24tZGVsYXk6IDAuNDVzOyB9CiAgICBjaXJjbGU6bnRoLWNoaWxkKDgpIHsgYW5pbWF0aW9uLWRlbGF5OiAwLjUyNXM7IH0KCiAgICBAa2V5ZnJhbWVzIGJhbGwgewogICAgICBmcm9tIHsgb3BhY2l0eTogMTsgfQogICAgICB0byB7IG9wYWNpdHk6IDAuMzsgfQogICAgfQoJPC9zdHlsZT4KCTxnIHN0eWxlPSJmaWxsOndoaXRlOyI+CgkJPGNpcmNsZSBjeD0nNScgY3k9JzEnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nNy44Mjg0JyBjeT0nMi4xNzE2JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzknIGN5PSc1JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJCTxjaXJjbGUgY3g9JzcuODI4NCcgY3k9JzcuODI4NCcgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PSc1JyBjeT0nOScgcj0nMScgc3R5bGU9J29wYWNpdHk6MC4zOycgLz4KCQk8Y2lyY2xlIGN4PScyLjE3MTYnIGN5PSc3LjgyODQnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nMScgY3k9JzUnIHI9JzEnIHN0eWxlPSdvcGFjaXR5OjAuMzsnIC8+CgkJPGNpcmNsZSBjeD0nMi4xNzE2JyBjeT0nMi4xNzE2JyByPScxJyBzdHlsZT0nb3BhY2l0eTowLjM7JyAvPgoJPC9nPgo8L3N2Zz4K)}.monaco-tree-action.collapse-all{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iLTEgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAtMSAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjNDI0MjQyIiBkPSJNMTQgMXY5aC0xdi04aC04di0xaDl6bS0xMSAydjFoOHY4aDF2LTloLTl6bTcgMnY5aC05di05aDl6bS0yIDJoLTV2NWg1di01eiIvPjxyZWN0IHg9IjQiIHk9IjkiIGZpbGw9IiMwMDUzOUMiIHdpZHRoPSIzIiBoZWlnaHQ9IjEiLz48L3N2Zz4=) 50% no-repeat}.hc-black .monaco-tree-action.collapse-all,.vs-dark .monaco-tree-action.collapse-all{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iLTEgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAtMSAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjQzVDNUM1IiBkPSJNMTQgMXY5aC0xdi04aC04di0xaDl6bS0xMSAydjFoOHY4aDF2LTloLTl6bTcgMnY5aC05di05aDl6bS0yIDJoLTV2NWg1di01eiIvPjxyZWN0IHg9IjQiIHk9IjkiIGZpbGw9IiM3NUJFRkYiIHdpZHRoPSIzIiBoZWlnaHQ9IjEiLz48L3N2Zz4=) 50% no-repeat}.monaco-editor .inputarea{min-width:0;min-height:0;margin:0;padding:0;position:absolute;outline:none!important;resize:none;border:none;overflow:hidden;color:transparent;background-color:transparent}.monaco-editor .inputarea.ime-input{z-index:10}.monaco-editor .margin-view-overlays .current-line,.monaco-editor .view-overlays .current-line{display:block;position:absolute;left:0;top:0;box-sizing:border-box}.monaco-editor .margin-view-overlays .current-line.current-line-margin.current-line-margin-both{border-right:0}.monaco-editor .lines-content .cdr{position:absolute}.monaco-editor .glyph-margin{position:absolute;top:0}.monaco-editor .lines-content .cigr,.monaco-editor .lines-content .cigra,.monaco-editor .margin-view-overlays .cgmr{position:absolute}.monaco-editor .margin-view-overlays .line-numbers{position:absolute;text-align:right;display:inline-block;vertical-align:middle;box-sizing:border-box;cursor:default;height:100%}.monaco-editor .relative-current-line-number{text-align:left;display:inline-block;width:100%}.monaco-editor .margin-view-overlays .line-numbers{cursor:-webkit-image-set(url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNSIgaGVpZ2h0PSIyNSIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCAxNSAyNSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTUgMjU7Ij48cG9seWdvbiBzdHlsZT0iZmlsbDojRkZGRkZGO3N0cm9rZTojMDAwMDAwIiBwb2ludHM9IjE0LjUsMS4yIDEuOSwxMy44IDcsMTMuOCAzLjIsMjEuNSA2LjMsMjIuNSAxMC4xLDE0LjkgMTQuNSwxOCIvPjwvc3ZnPg==) 1x,url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMzAiIGhlaWdodD0iNTAiIHZpZXdCb3g9IjAgMCAzMCA1MCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzAgNTA7Ij48cG9seWdvbiBzdHlsZT0iZmlsbDojRkZGRkZGO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyOyIgcG9pbnRzPSIyOSwyLjQgMy44LDI3LjYgMTQsMjcuNiA2LjQsNDMgMTIuNiw0NSAyMC4yLDI5LjggMjksMzYiLz48L3N2Zz4K) 2x) 30 0,default}.monaco-editor.mac .margin-view-overlays .line-numbers{cursor:-webkit-image-set(url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTMgMTkiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEzIDE5OyIgd2lkdGg9IjEzIiBoZWlnaHQ9IjE5Ij48c3R5bGUgdHlwZT0idGV4dC9jc3MiPi5zdDB7c3Ryb2tlOiNGRkZGRkY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7fTwvc3R5bGU+PHRpdGxlPmZsaXBwZWQtY3Vyc29yLW1hYzwvdGl0bGU+PHBhdGggY2xhc3M9InN0MCIgZD0iTTUuMywxNi42bDEuNi00LjdIMi4xTDEyLjUsMS4ydjE0LjRMOS43LDEzbC0xLjYsNC42Yy0wLjIsMC41LTAuOCwwLjgtMS4zLDAuNkw2LDE3LjkgQzUuNCwxNy43LDUuMSwxNy4yLDUuMywxNi42eiIvPjwvc3ZnPgo=) 1x,url(data:image/svg+xml;base64,CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDI2IDM4IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyNiAzODsiIHdpZHRoPSIyNiIgaGVpZ2h0PSIzOCI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc3Qwe3N0cm9rZTojRkZGRkZGO3N0cm9rZS1taXRlcmxpbWl0OjEwO308L3N0eWxlPgk8dGl0bGU+ZmxpcHBlZC1jdXJzb3ItbWFjPC90aXRsZT48cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTAuNiwzMy4ybDMuMi05LjRINC4yTDI1LDIuNHYyOC44TDE5LjQsMjZsLTMuMiw5LjJjLTAuNCwxLTEuNiwxLjYtMi42LDEuMkwxMiwzNS44IEMxMC44LDM1LjQsMTAuMiwzNC40LDEwLjYsMzMuMnoiLz48L3N2Zz4K) 2x) 24 3,default}.monaco-editor .margin-view-overlays .line-numbers.lh-odd{margin-top:1px}.monaco-editor.no-user-select .lines-content,.monaco-editor.no-user-select .view-line,.monaco-editor.no-user-select .view-lines{-webkit-user-select:none;-ms-user-select:none;-moz-user-select:none;-o-user-select:none;user-select:none}.monaco-editor .view-lines{cursor:text;white-space:nowrap}.monaco-editor.hc-black.mac .view-lines,.monaco-editor.vs-dark.mac .view-lines{cursor:-webkit-image-set(url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=) 1x,url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC) 2x) 5 8,text}.monaco-editor .view-line{position:absolute;width:100%}.monaco-editor .lines-decorations{position:absolute;top:0;background:#fff}.monaco-editor .margin-view-overlays .cldr{position:absolute;height:100%}.monaco-editor .margin-view-overlays .cmdr{position:absolute;left:0;width:100%;height:100%}.monaco-editor .minimap.slider-mouseover .minimap-slider{opacity:0;transition:opacity .1s linear}.monaco-editor .minimap.slider-mouseover .minimap-slider.active,.monaco-editor .minimap.slider-mouseover:hover .minimap-slider{opacity:1}.monaco-editor .minimap-shadow-hidden{position:absolute;width:0}.monaco-editor .minimap-shadow-visible{position:absolute;left:-6px;width:6px}.monaco-editor .overlayWidgets{position:absolute;top:0;left:0}.monaco-editor .view-ruler{position:absolute;top:0}.monaco-editor .scroll-decoration{position:absolute;top:0;left:0;height:6px}.monaco-editor .lines-content .cslr{position:absolute}.monaco-editor .top-left-radius{border-top-left-radius:3px}.monaco-editor .bottom-left-radius{border-bottom-left-radius:3px}.monaco-editor .top-right-radius{border-top-right-radius:3px}.monaco-editor .bottom-right-radius{border-bottom-right-radius:3px}.monaco-editor.hc-black .top-left-radius{border-top-left-radius:0}.monaco-editor.hc-black .bottom-left-radius{border-bottom-left-radius:0}.monaco-editor.hc-black .top-right-radius{border-top-right-radius:0}.monaco-editor.hc-black .bottom-right-radius{border-bottom-right-radius:0}.monaco-editor .cursors-layer{position:absolute;top:0}.monaco-editor .cursors-layer>.cursor{position:absolute;cursor:text;overflow:hidden}.monaco-editor .cursors-layer.cursor-smooth-caret-animation>.cursor{transition:80ms}.monaco-editor .cursors-layer.cursor-block-outline-style>.cursor{box-sizing:border-box;background:transparent!important;border-style:solid;border-width:1px}.monaco-editor .cursors-layer.cursor-underline-style>.cursor{border-bottom-width:2px;border-bottom-style:solid;background:transparent!important;box-sizing:border-box}.monaco-editor .cursors-layer.cursor-underline-thin-style>.cursor{border-bottom-width:1px;border-bottom-style:solid;background:transparent!important;box-sizing:border-box}@keyframes monaco-cursor-smooth{0%,20%{opacity:1}60%,to{opacity:0}}@keyframes monaco-cursor-phase{0%,20%{opacity:1}90%,to{opacity:0}}@keyframes monaco-cursor-expand{0%,20%{transform:scaleY(1)}80%,to{transform:scaleY(0)}}.cursor-smooth{animation:monaco-cursor-smooth .5s ease-in-out 0s 20 alternate}.cursor-phase{animation:monaco-cursor-phase .5s ease-in-out 0s 20 alternate}.cursor-expand>.cursor{animation:monaco-cursor-expand .5s ease-in-out 0s 20 alternate}.monaco-diff-editor .diffOverview{z-index:9}.monaco-diff-editor.vs .diffOverview{background:rgba(0,0,0,.03)}.monaco-diff-editor.vs-dark .diffOverview{background:hsla(0,0%,100%,.01)}.monaco-diff-editor .diffViewport{box-shadow:inset 0 0 1px 0 #b9b9b9;background:rgba(0,0,0,.1)}.monaco-diff-editor.hc-black .diffViewport,.monaco-diff-editor.vs-dark .diffViewport{background:hsla(0,0%,100%,.1)}.monaco-scrollable-element.modified-in-monaco-diff-editor.vs-dark .scrollbar,.monaco-scrollable-element.modified-in-monaco-diff-editor.vs .scrollbar{background:transparent}.monaco-scrollable-element.modified-in-monaco-diff-editor.hc-black .scrollbar{background:none}.monaco-scrollable-element.modified-in-monaco-diff-editor .slider{z-index:10}.modified-in-monaco-diff-editor .slider.active{background:hsla(0,0%,67%,.4)}.modified-in-monaco-diff-editor.hc-black .slider.active{background:none}.monaco-diff-editor .delete-sign,.monaco-diff-editor .insert-sign,.monaco-editor .delete-sign,.monaco-editor .insert-sign{background-size:60%;opacity:.7;background-repeat:no-repeat;background-position:50% 50%;background-position:50%;background-size:11px 11px}.monaco-diff-editor.hc-black .delete-sign,.monaco-diff-editor.hc-black .insert-sign,.monaco-editor.hc-black .delete-sign,.monaco-editor.hc-black .insert-sign{opacity:1}.monaco-diff-editor .insert-sign,.monaco-editor .insert-sign{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPkxheWVyIDE8L3RpdGxlPjxyZWN0IGhlaWdodD0iMTEiIHdpZHRoPSIzIiB5PSIzIiB4PSI3IiBmaWxsPSIjNDI0MjQyIi8+PHJlY3QgaGVpZ2h0PSIzIiB3aWR0aD0iMTEiIHk9IjciIHg9IjMiIGZpbGw9IiM0MjQyNDIiLz48L3N2Zz4=)}.monaco-diff-editor .delete-sign,.monaco-editor .delete-sign{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPkxheWVyIDE8L3RpdGxlPjxyZWN0IGhlaWdodD0iMyIgd2lkdGg9IjExIiB5PSI3IiB4PSIzIiBmaWxsPSIjNDI0MjQyIi8+PC9zdmc+)}.monaco-diff-editor.hc-black .insert-sign,.monaco-diff-editor.vs-dark .insert-sign,.monaco-editor.hc-black .insert-sign,.monaco-editor.vs-dark .insert-sign{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPkxheWVyIDE8L3RpdGxlPjxyZWN0IGhlaWdodD0iMTEiIHdpZHRoPSIzIiB5PSIzIiB4PSI3IiBmaWxsPSIjQzVDNUM1Ii8+PHJlY3QgaGVpZ2h0PSIzIiB3aWR0aD0iMTEiIHk9IjciIHg9IjMiIGZpbGw9IiNDNUM1QzUiLz48L3N2Zz4=)}.monaco-diff-editor.hc-black .delete-sign,.monaco-diff-editor.vs-dark .delete-sign,.monaco-editor.hc-black .delete-sign,.monaco-editor.vs-dark .delete-sign{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPkxheWVyIDE8L3RpdGxlPjxyZWN0IGhlaWdodD0iMyIgd2lkdGg9IjExIiB5PSI3IiB4PSIzIiBmaWxsPSIjQzVDNUM1Ii8+PC9zdmc+)}.monaco-editor .inline-added-margin-view-zone,.monaco-editor .inline-deleted-margin-view-zone{text-align:right}.monaco-editor .diagonal-fill{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAChJREFUKFNjOH/+fAMDDgCSu3Dhwn9c8gwwBTgNGR4KQP4HhQOhsAIAZCBTkhtqePcAAAAASUVORK5CYII=)}.monaco-editor.vs-dark .diagonal-fill{opacity:.2}.monaco-editor.hc-black .diagonal-fill{background:none}.monaco-editor .view-zones .view-lines .view-line span{display:inline-block}.monaco-diff-editor .diff-review-line-number{text-align:right;display:inline-block}.monaco-diff-editor .diff-review{position:absolute;-webkit-user-select:none;-ms-user-select:none;-moz-user-select:none;-o-user-select:none;user-select:none}.monaco-diff-editor .diff-review-summary{padding-left:10px}.monaco-diff-editor .diff-review-shadow{position:absolute}.monaco-diff-editor .diff-review-row{white-space:pre}.monaco-diff-editor .diff-review-table{display:table;min-width:100%}.monaco-diff-editor .diff-review-row{display:table-row;width:100%}.monaco-diff-editor .diff-review-cell{display:table-cell}.monaco-diff-editor .diff-review-spacer{display:inline-block;width:10px}.monaco-diff-editor .diff-review-actions{display:inline-block;position:absolute;right:10px;top:2px}.monaco-diff-editor .diff-review-actions .action-label{width:16px;height:16px;margin:2px 0}.monaco-diff-editor .action-label.icon.close-diff-review{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMyAzIDE2IDE2IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDMgMyAxNiAxNiI+PHBvbHlnb24gZmlsbD0iIzQyNDI0MiIgcG9pbnRzPSIxMi41OTcsMTEuMDQyIDE1LjQsMTMuODQ1IDEzLjg0NCwxNS40IDExLjA0MiwxMi41OTggOC4yMzksMTUuNCA2LjY4MywxMy44NDUgOS40ODUsMTEuMDQyIDYuNjgzLDguMjM5IDguMjM4LDYuNjgzIDExLjA0Miw5LjQ4NiAxMy44NDUsNi42ODMgMTUuNCw4LjIzOSIvPjwvc3ZnPg==) 50% no-repeat}.monaco-diff-editor.hc-black .action-label.icon.close-diff-review,.monaco-diff-editor.vs-dark .action-label.icon.close-diff-review{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMyAzIDE2IDE2IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDMgMyAxNiAxNiI+PHBvbHlnb24gZmlsbD0iI2U4ZThlOCIgcG9pbnRzPSIxMi41OTcsMTEuMDQyIDE1LjQsMTMuODQ1IDEzLjg0NCwxNS40IDExLjA0MiwxMi41OTggOC4yMzksMTUuNCA2LjY4MywxMy44NDUgOS40ODUsMTEuMDQyIDYuNjgzLDguMjM5IDguMjM4LDYuNjgzIDExLjA0Miw5LjQ4NiAxMy44NDUsNi42ODMgMTUuNCw4LjIzOSIvPjwvc3ZnPg==) 50% no-repeat}::-ms-clear{display:none}.monaco-editor .editor-widget input{color:inherit}.monaco-editor{position:relative;overflow:visible;-webkit-text-size-adjust:100%;-webkit-font-feature-settings:"liga" off,"calt" off;font-feature-settings:"liga" off,"calt" off}.monaco-editor.enable-ligatures{-webkit-font-feature-settings:"liga" on,"calt" on;font-feature-settings:"liga" on,"calt" on}.monaco-editor .overflow-guard{position:relative;overflow:hidden}.monaco-editor .view-overlays{position:absolute;top:0}.monaco-editor .vs-whitespace{display:inline-block}.monaco-editor .bracket-match{box-sizing:border-box}.monaco-menu .monaco-action-bar.vertical .action-label.hover{background-color:#eee}.monaco-editor .lightbulb-glyph{display:flex;align-items:center;justify-content:center;height:16px;width:20px;padding-left:2px}.monaco-editor .lightbulb-glyph:hover{cursor:pointer}.monaco-editor.vs .lightbulb-glyph{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIGhlaWdodD0iMTYiIHdpZHRoPSIxNiI+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTEzLjUgNC4yQzEzLjEgMi4xIDEwLjggMCA5LjMgMEg2LjdjLS40IDAtLjYuMi0uNi4yQzQgLjggMi41IDIuNyAyLjUgNC45YzAgLjUtLjEgMi4zIDEuNyAzLjguNS41IDEuMiAyIDEuMyAyLjR2My4zTDcuMSAxNmgybDEuNS0xLjZWMTFjLjEtLjQuOC0xLjkgMS4zLTIuMyAxLjEtLjkgMS41LTEuOSAxLjYtMi43VjQuMnoiLz48Zz48ZyBmaWxsPSIjODQ4NDg0Ij48cGF0aCBkPSJNNi41IDEyaDN2MWgtM3pNNy41IDE1aDEuMWwuOS0xaC0zeiIvPjwvZz48cGF0aCBmaWxsPSIjZmMwIiBkPSJNMTIuNiA1YzAtMi4zLTEuOC00LjEtNC4xLTQuMS0uMSAwLTEuNC4xLTEuNC4xLTIuMS4zLTMuNyAyLTMuNyA0IDAgLjEtLjIgMS42IDEuNCAzIC43LjcgMS41IDIuNCAxLjYgMi45bC4xLjFoM2wuMS0uMmMuMS0uNS45LTIuMiAxLjYtMi45IDEuNi0xLjMgMS40LTIuOCAxLjQtMi45em0tMyAxbC0uNSAzaC0uNlY2YzEuMSAwIC45LTEgLjktMUg2LjV2LjFjMCAuMi4xLjkgMSAuOXYzSDdsLS4yLS43TDYuNSA2Yy0uNyAwLS45LS40LTEtLjd2LS40YzAtLjguOS0uOS45LS45aDMuMXMxIC4xIDEgMWMwIDAgLjEgMS0uOSAxeiIvPjwvZz48cGF0aCBmaWxsPSIjRjBFRkYxIiBkPSJNMTAuNSA1YzAtLjktMS0xLTEtMUg2LjRzLS45LjEtLjkuOXYuNGMwIC4zLjMuNy45LjdsLjQgMi4zLjIuN2guNVY2Yy0xIDAtMS0uNy0xLS45VjVoM3MuMSAxLS45IDF2M2guNmwuNS0zYy45IDAgLjgtMSAuOC0xeiIvPjwvc3ZnPg==) 50% no-repeat}.monaco-editor.vs .lightbulb-glyph.autofixable{background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjAwMSA0LjJDMTIuNjAxIDIuMSAxMC4zMDEgMCA4LjgwMSAwSDYuMjAxQzUuODAxIDAgNS42MDEgMC4yIDUuNjAxIDAuMkMzLjUwMSAwLjggMi4wMDEgMi43IDIuMDAxIDQuOUMyLjAwMSA1LjQgMS45MDEgNy4yIDMuNzAxIDguN0M0LjIwMSA5LjIgNC45MDEgMTAuNyA1LjAwMSAxMS4xVjE0LjRMNi42MDEgMTZIOC42MDFMMTAuMTAxIDE0LjRWMTFDMTAuMjAxIDEwLjYgMTAuOTAxIDkuMSAxMS40MDEgOC43QzEyLjUwMSA3LjggMTIuOTAxIDYuOCAxMy4wMDEgNlY0LjJaIiBmaWxsPSIjRjZGNkY2Ii8+CjxwYXRoIGQ9Ik02LjAwMDk4IDEySDkuMDAwOThWMTNINi4wMDA5OFYxMlpNNy4wMDA5OCAxNUg4LjEwMDk4TDkuMDAwOTggMTRINi4wMDA5OEw3LjAwMDk4IDE1WiIgZmlsbD0iIzg0ODQ4NCIvPgo8cGF0aCBkPSJNMTIuMTAxMSA0Ljk5OTlDMTIuMTAxMSAyLjY5OTkgMTAuMzAxMSAwLjg5OTkwMiA4LjAwMTA3IDAuODk5OTAyQzcuOTAxMDcgMC44OTk5MDIgNi42MDEwNyAwLjk5OTkwMiA2LjYwMTA3IDAuOTk5OTAyQzQuNTAxMDcgMS4yOTk5IDIuOTAxMDcgMi45OTk5IDIuOTAxMDcgNC45OTk5QzIuOTAxMDcgNS4wOTk5IDIuNzAxMDcgNi41OTk5IDQuMzAxMDcgNy45OTk5QzUuMDAxMDcgOC42OTk5IDUuODAxMDcgMTAuMzk5OSA1LjkwMTA3IDEwLjg5OTlMNi4wMDEwNyAxMC45OTk5SDkuMDAxMDdMOS4xMDEwNyAxMC43OTk5QzkuMjAxMDcgMTAuMjk5OSAxMC4wMDExIDguNTk5OSAxMC43MDExIDcuODk5OUMxMi4zMDExIDYuNTk5OSAxMi4xMDExIDUuMDk5OSAxMi4xMDExIDQuOTk5OVY0Ljk5OTlaTTkuMTAxMDcgNS45OTk5TDguNjAxMDcgOC45OTk5SDguMDAxMDdWNS45OTk5QzkuMTAxMDcgNS45OTk5IDguOTAxMDcgNC45OTk5IDguOTAxMDcgNC45OTk5SDYuMDAxMDdWNS4wOTk5QzYuMDAxMDcgNS4yOTk5IDYuMTAxMDcgNS45OTk5IDcuMDAxMDcgNS45OTk5VjguOTk5OUg2LjUwMTA3TDYuMzAxMDcgOC4yOTk5TDYuMDAxMDcgNS45OTk5QzUuMzAxMDcgNS45OTk5IDUuMTAxMDcgNS41OTk5IDUuMDAxMDcgNS4yOTk5VjQuODk5OUM1LjAwMTA3IDQuMDk5OSA1LjkwMTA3IDMuOTk5OSA1LjkwMTA3IDMuOTk5OUg5LjAwMTA3QzkuMDAxMDcgMy45OTk5IDEwLjAwMTEgNC4wOTk5IDEwLjAwMTEgNC45OTk5QzEwLjAwMTEgNC45OTk5IDEwLjEwMTEgNS45OTk5IDkuMTAxMDcgNS45OTk5WiIgZmlsbD0iI0ZGQ0MwMCIvPgo8cGF0aCBkPSJNMTAuMDAxIDVDMTAuMDAxIDQuMSA5LjAwMDk4IDQgOS4wMDA5OCA0SDUuOTAwOThDNS45MDA5OCA0IDUuMDAwOTggNC4xIDUuMDAwOTggNC45VjUuM0M1LjAwMDk4IDUuNiA1LjMwMDk4IDYgNS45MDA5OCA2TDYuMzAwOTggOC4zTDYuNTAwOTggOUg3LjAwMDk4VjZDNi4wMDA5OCA2IDYuMDAwOTggNS4zIDYuMDAwOTggNS4xVjVIOS4wMDA5OEM5LjAwMDk4IDUgOS4xMDA5OCA2IDguMTAwOTggNlY5SDguNzAwOThMOS4yMDA5OCA2QzEwLjEwMSA2IDEwLjAwMSA1IDEwLjAwMSA1WiIgZmlsbD0iI0YwRUZGMSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTggMTJDOCA5Ljc3Mzg2IDkuNzczODYgOCAxMiA4QzE0LjIyNjEgOCAxNiA5Ljc3Mzg2IDE2IDEyQzE2IDE0LjIyNjEgMTQuMjI2MSAxNiAxMiAxNkM5Ljc3Mzg2IDE2IDggMTQuMjI2MSA4IDEyWiIgZmlsbD0iI0Y2RjZGNiIvPgo8cGF0aCBkPSJNMTIuMzE5MiAxMi4zMDMxTDEzLjM0OTUgMTMuMzMzNEwxMy4zMzM0IDEzLjM0OTVMMTIuMzAzMSAxMi4zMTkyTDEyIDEyLjAxNjJMMTEuNjk3IDEyLjMxOTJMMTAuNjY2NyAxMy4zNDk1TDEwLjY1MDYgMTMuMzMzNEwxMS42ODA5IDEyLjMwMzFMMTEuOTgzOSAxMkwxMS42ODA5IDExLjY5N0wxMC42NTA2IDEwLjY2NjdMMTAuNjY2NyAxMC42NTA2TDExLjY5NyAxMS42ODA5TDEyIDExLjk4MzlMMTIuMzAzMSAxMS42ODA5TDEzLjMzMzQgMTAuNjUwNkwxMy4zNDk1IDEwLjY2NjdMMTIuMzE5MiAxMS42OTdMMTIuMDE2MiAxMkwxMi4zMTkyIDEyLjMwMzFaTTEyIDguNDYwMzRDMTAuMDMgOC40NjAzNCA4LjQ2MDM0IDEwLjAzIDguNDYwMzQgMTJDOC40NjAzNCAxMy45NzAxIDEwLjAzIDE1LjUzOTcgMTIgMTUuNTM5N0MxMy45NzAxIDE1LjUzOTcgMTUuNTM5NyAxMy45NzAxIDE1LjUzOTcgMTJDMTUuNTM5NyAxMC4wMyAxMy45NzAxIDguNDYwMzQgMTIgOC40NjAzNFoiIGZpbGw9IiMwMDdBQ0MiIHN0cm9rZT0iI0Y2RjZGNiIgc3Ryb2tlLXdpZHRoPSIwLjg1NzE0MyIvPgo8cGF0aCBkPSJNMTIuNjIyNSAxMi4wMDAyTDEzLjk1NTggMTMuMzMzNkwxMy4zMzM2IDEzLjk1NThMMTIuMDAwMiAxMi42MjI1TDEwLjY2NjkgMTMuOTU1OEwxMC4wNDQ3IDEzLjMzMzZMMTEuMzc4IDEyLjAwMDJMMTAuMDQ0NyAxMC42NjY5TDEwLjY2NjkgMTAuMDQ0N0wxMi4wMDAyIDExLjM3OEwxMy4zMzM2IDEwLjA0NDdMMTMuOTU1OCAxMC42NjY5TDEyLjYyMjUgMTIuMDAwMloiIGZpbGw9IiMwMDdBQ0MiLz4KPHBhdGggZD0iTTEwLjcwNCAxNEwxMS4yMDI4IDEyLjQ3MTJMMTAgMTEuNjM5NEgxMS40NzMyTDEyIDEwTDEyLjUzNjEgMTEuNjM5NEgxNEwxMi43OTcyIDEyLjQ3MTJMMTMuMzA1NCAxNEwxMiAxMy4wMjRMMTAuNzA0IDE0WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==) 50% no-repeat}.monaco-editor.hc-black .lightbulb-glyph,.monaco-editor.vs-dark .lightbulb-glyph{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIGhlaWdodD0iMTYiIHdpZHRoPSIxNiI+PHBhdGggZmlsbD0iIzFFMUUxRSIgZD0iTTEzLjUgNC4yQzEzLjEgMi4xIDEwLjggMCA5LjMgMEg2LjdjLS40IDAtLjYuMi0uNi4yQzQgLjggMi41IDIuNyAyLjUgNC45YzAgLjUtLjEgMi4zIDEuNyAzLjguNS41IDEuMiAyIDEuMyAyLjR2My4zTDcuMSAxNmgybDEuNS0xLjZWMTFjLjEtLjQuOC0xLjkgMS4zLTIuMyAxLjEtLjkgMS41LTEuOSAxLjYtMi43VjQuMnoiLz48Zz48ZyBmaWxsPSIjQzVDNUM1Ij48cGF0aCBkPSJNNi41IDEyaDN2MWgtM3pNNy41IDE1aDEuMWwuOS0xaC0zeiIvPjwvZz48cGF0aCBmaWxsPSIjRERCMjA0IiBkPSJNMTIuNiA1YzAtMi4zLTEuOC00LjEtNC4xLTQuMS0uMSAwLTEuNC4xLTEuNC4xLTIuMS4zLTMuNyAyLTMuNyA0IDAgLjEtLjIgMS42IDEuNCAzIC43LjcgMS41IDIuNCAxLjYgMi45bC4xLjFoM2wuMS0uMmMuMS0uNS45LTIuMiAxLjYtMi45IDEuNi0xLjMgMS40LTIuOCAxLjQtMi45em0tMyAxbC0uNSAzaC0uNlY2YzEuMSAwIC45LTEgLjktMUg2LjV2LjFjMCAuMi4xLjkgMSAuOXYzSDdsLS4yLS43TDYuNSA2Yy0uNyAwLS45LS40LTEtLjd2LS40YzAtLjguOS0uOS45LS45aDMuMXMxIC4xIDEgMWMwIDAgLjEgMS0uOSAxeiIvPjwvZz48cGF0aCBmaWxsPSIjMjUyNTI2IiBkPSJNMTAuNSA1YzAtLjktMS0xLTEtMUg2LjRzLS45LjEtLjkuOXYuNGMwIC4zLjMuNy45LjdsLjQgMi4zLjIuN2guNVY2Yy0xIDAtMS0uNy0xLS45VjVoM3MuMSAxLS45IDF2M2guNmwuNS0zYy45IDAgLjgtMSAuOC0xeiIvPjwvc3ZnPg==) 50% no-repeat}.monaco-editor.hc-black .lightbulb-glyph.autofixable,.monaco-editor.vs-dark .lightbulb-glyph.autofixable{background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjAwMSA0LjJDMTIuNjAxIDIuMSAxMC4zMDEgMCA4LjgwMSAwSDYuMjAxQzUuODAxIDAgNS42MDEgMC4yIDUuNjAxIDAuMkMzLjUwMSAwLjggMi4wMDEgMi43IDIuMDAxIDQuOUMyLjAwMSA1LjQgMS45MDEgNy4yIDMuNzAxIDguN0M0LjIwMSA5LjIgNC45MDEgMTAuNyA1LjAwMSAxMS4xVjE0LjRMNi42MDEgMTZIOC42MDFMMTAuMTAxIDE0LjRWMTFDMTAuMjAxIDEwLjYgMTAuOTAxIDkuMSAxMS40MDEgOC43QzEyLjUwMSA3LjggMTIuOTAxIDYuOCAxMy4wMDEgNlY0LjJaIiBmaWxsPSIjMUUxRTFFIi8+CjxwYXRoIGQ9Ik02LjAwMDk4IDEySDkuMDAwOThWMTNINi4wMDA5OFYxMlpNNy4wMDA5OCAxNUg4LjEwMDk4TDkuMDAwOTggMTRINi4wMDA5OEw3LjAwMDk4IDE1WiIgZmlsbD0iI0M1QzVDNSIvPgo8cGF0aCBkPSJNMTIuMTAxMSA0Ljk5OTlDMTIuMTAxMSAyLjY5OTkgMTAuMzAxMSAwLjg5OTkwMiA4LjAwMTA3IDAuODk5OTAyQzcuOTAxMDcgMC44OTk5MDIgNi42MDEwNyAwLjk5OTkwMiA2LjYwMTA3IDAuOTk5OTAyQzQuNTAxMDcgMS4yOTk5IDIuOTAxMDcgMi45OTk5IDIuOTAxMDcgNC45OTk5QzIuOTAxMDcgNS4wOTk5IDIuNzAxMDcgNi41OTk5IDQuMzAxMDcgNy45OTk5QzUuMDAxMDcgOC42OTk5IDUuODAxMDcgMTAuMzk5OSA1LjkwMTA3IDEwLjg5OTlMNi4wMDEwNyAxMC45OTk5SDkuMDAxMDdMOS4xMDEwNyAxMC43OTk5QzkuMjAxMDcgMTAuMjk5OSAxMC4wMDExIDguNTk5OSAxMC43MDExIDcuODk5OUMxMi4zMDExIDYuNTk5OSAxMi4xMDExIDUuMDk5OSAxMi4xMDExIDQuOTk5OVY0Ljk5OTlaTTkuMTAxMDcgNS45OTk5TDguNjAxMDcgOC45OTk5SDguMDAxMDdWNS45OTk5QzkuMTAxMDcgNS45OTk5IDguOTAxMDcgNC45OTk5IDguOTAxMDcgNC45OTk5SDYuMDAxMDdWNS4wOTk5QzYuMDAxMDcgNS4yOTk5IDYuMTAxMDcgNS45OTk5IDcuMDAxMDcgNS45OTk5VjguOTk5OUg2LjUwMTA3TDYuMzAxMDcgOC4yOTk5TDYuMDAxMDcgNS45OTk5QzUuMzAxMDcgNS45OTk5IDUuMTAxMDcgNS41OTk5IDUuMDAxMDcgNS4yOTk5VjQuODk5OUM1LjAwMTA3IDQuMDk5OSA1LjkwMTA3IDMuOTk5OSA1LjkwMTA3IDMuOTk5OUg5LjAwMTA3QzkuMDAxMDcgMy45OTk5IDEwLjAwMTEgNC4wOTk5IDEwLjAwMTEgNC45OTk5QzEwLjAwMTEgNC45OTk5IDEwLjEwMTEgNS45OTk5IDkuMTAxMDcgNS45OTk5WiIgZmlsbD0iI0REQjIwNCIvPgo8cGF0aCBkPSJNMTAuMDAxIDVDMTAuMDAxIDQuMSA5LjAwMDk4IDQgOS4wMDA5OCA0SDUuOTAwOThDNS45MDA5OCA0IDUuMDAwOTggNC4xIDUuMDAwOTggNC45VjUuM0M1LjAwMDk4IDUuNiA1LjMwMDk4IDYgNS45MDA5OCA2TDYuMzAwOTggOC4zTDYuNTAwOTggOUg3LjAwMDk4VjZDNi4wMDA5OCA2IDYuMDAwOTggNS4zIDYuMDAwOTggNS4xVjVIOS4wMDA5OEM5LjAwMDk4IDUgOS4xMDA5OCA2IDguMTAwOTggNlY5SDguNzAwOThMOS4yMDA5OCA2QzEwLjEwMSA2IDEwLjAwMSA1IDEwLjAwMSA1WiIgZmlsbD0iIzI1MjUyNiIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTggMTJDOCA5Ljc3Mzg2IDkuNzczODYgOCAxMiA4QzE0LjIyNjEgOCAxNiA5Ljc3Mzg2IDE2IDEyQzE2IDE0LjIyNjEgMTQuMjI2MSAxNiAxMiAxNkM5Ljc3Mzg2IDE2IDggMTQuMjI2MSA4IDEyWiIgZmlsbD0iIzFFMUUxRSIvPgo8cGF0aCBkPSJNMTIuMzE5MiAxMi4zMDMxTDEzLjM0OTUgMTMuMzMzNEwxMy4zMzM0IDEzLjM0OTVMMTIuMzAzMSAxMi4zMTkyTDEyIDEyLjAxNjJMMTEuNjk3IDEyLjMxOTJMMTAuNjY2NyAxMy4zNDk1TDEwLjY1MDYgMTMuMzMzNEwxMS42ODA5IDEyLjMwMzFMMTEuOTgzOSAxMkwxMS42ODA5IDExLjY5N0wxMC42NTA2IDEwLjY2NjdMMTAuNjY2NyAxMC42NTA2TDExLjY5NyAxMS42ODA5TDEyIDExLjk4MzlMMTIuMzAzMSAxMS42ODA5TDEzLjMzMzQgMTAuNjUwNkwxMy4zNDk1IDEwLjY2NjdMMTIuMzE5MiAxMS42OTdMMTIuMDE2MiAxMkwxMi4zMTkyIDEyLjMwMzFaTTEyIDguNDYwMzRDMTAuMDMgOC40NjAzNCA4LjQ2MDM0IDEwLjAzIDguNDYwMzQgMTJDOC40NjAzNCAxMy45NzAxIDEwLjAzIDE1LjUzOTcgMTIgMTUuNTM5N0MxMy45NzAxIDE1LjUzOTcgMTUuNTM5NyAxMy45NzAxIDE1LjUzOTcgMTJDMTUuNTM5NyAxMC4wMyAxMy45NzAxIDguNDYwMzQgMTIgOC40NjAzNFoiIGZpbGw9IiMwMDdBQ0MiIHN0cm9rZT0iIzFFMUUxRSIgc3Ryb2tlLXdpZHRoPSIwLjg1NzE0MyIvPgo8cGF0aCBkPSJNMTIuNjIyNSAxMi4wMDAyTDEzLjk1NTggMTMuMzMzNkwxMy4zMzM2IDEzLjk1NThMMTIuMDAwMiAxMi42MjI1TDEwLjY2NjkgMTMuOTU1OEwxMC4wNDQ3IDEzLjMzMzZMMTEuMzc4IDEyLjAwMDJMMTAuMDQ0NyAxMC42NjY5TDEwLjY2NjkgMTAuMDQ0N0wxMi4wMDAyIDExLjM3OEwxMy4zMzM2IDEwLjA0NDdMMTMuOTU1OCAxMC42NjY5TDEyLjYyMjUgMTIuMDAwMloiIGZpbGw9IiMwMDdBQ0MiLz4KPHBhdGggZD0iTTEwLjcwNCAxNEwxMS4yMDI4IDEyLjQ3MTJMMTAgMTEuNjM5NEgxMS40NzMyTDEyIDEwTDEyLjUzNjEgMTEuNjM5NEgxNEwxMi43OTcyIDEyLjQ3MTJMMTMuMzA1NCAxNEwxMiAxMy4wMjRMMTAuNzA0IDE0WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==) 50% no-repeat}.monaco-editor .codelens-decoration{overflow:hidden;display:inline-block;text-overflow:ellipsis}.monaco-editor .codelens-decoration>a,.monaco-editor .codelens-decoration>span{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;white-space:nowrap;vertical-align:sub}.monaco-editor .codelens-decoration>a{text-decoration:none}.monaco-editor .codelens-decoration>a:hover{text-decoration:underline;cursor:pointer}.monaco-editor .codelens-decoration.invisible-cl{opacity:0}@keyframes fadein{0%{opacity:0;visibility:visible}to{opacity:1}}.monaco-editor .codelens-decoration.fadein{animation:fadein .1s linear}.colorpicker-widget{height:190px;user-select:none}.monaco-editor .colorpicker-hover:focus{outline:none}.colorpicker-header{display:flex;height:24px;position:relative;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTZEaa/1AAAAHUlEQVQYV2PYvXu3JAi7uLiAMaYAjAGTQBPYLQkAa/0Zef3qRswAAAAASUVORK5CYII=);background-size:9px 9px;image-rendering:pixelated}.colorpicker-header .picked-color{width:216px;line-height:24px;cursor:pointer;color:#fff;flex:1;text-align:center}.colorpicker-header .picked-color.light{color:#000}.colorpicker-header .original-color{width:74px;z-index:inherit;cursor:pointer}.colorpicker-body{display:flex;padding:8px;position:relative}.colorpicker-body .saturation-wrap{overflow:hidden;height:150px;position:relative;min-width:220px;flex:1}.colorpicker-body .saturation-box{height:150px;position:absolute}.colorpicker-body .saturation-selection{width:9px;height:9px;margin:-5px 0 0 -5px;border:1px solid #fff;border-radius:100%;box-shadow:0 0 2px rgba(0,0,0,.8);position:absolute}.colorpicker-body .strip{width:25px;height:150px}.colorpicker-body .hue-strip{position:relative;margin-left:8px;cursor:-webkit-grab;background:linear-gradient(180deg,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red)}.colorpicker-body .opacity-strip{position:relative;margin-left:8px;cursor:-webkit-grab;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTZEaa/1AAAAHUlEQVQYV2PYvXu3JAi7uLiAMaYAjAGTQBPYLQkAa/0Zef3qRswAAAAASUVORK5CYII=);background-size:9px 9px;image-rendering:pixelated}.colorpicker-body .strip.grabbing{cursor:-webkit-grabbing}.colorpicker-body .slider{position:absolute;top:0;left:-2px;width:calc(100% + 4px);height:4px;box-sizing:border-box;border:1px solid hsla(0,0%,100%,.71);box-shadow:0 0 1px rgba(0,0,0,.85)}.colorpicker-body .strip .overlay{height:150px;pointer-events:none}.monaco-editor.vs .dnd-target{border-right:2px dotted #000;color:#fff}.monaco-editor.vs-dark .dnd-target{border-right:2px dotted #aeafad;color:#51504f}.monaco-editor.hc-black .dnd-target{border-right:2px dotted #fff;color:#000}.monaco-editor.hc-black.mac.mouse-default .view-lines,.monaco-editor.mouse-default .view-lines,.monaco-editor.vs-dark.mac.mouse-default .view-lines{cursor:default}.monaco-editor.hc-black.mac.mouse-copy .view-lines,.monaco-editor.mouse-copy .view-lines,.monaco-editor.vs-dark.mac.mouse-copy .view-lines{cursor:copy}.monaco-checkbox .label{width:12px;height:12px;border:1px solid #000;background-color:transparent;display:inline-block}.monaco-checkbox .checkbox{position:absolute;overflow:hidden;clip:rect(0 0 0 0);height:1px;width:1px;margin:-1px;padding:0;border:0}.monaco-checkbox .checkbox:checked+.label{background-color:#000}.monaco-editor .find-widget{position:absolute;z-index:10;top:-44px;height:34px;overflow:hidden;line-height:19px;transition:top .2s linear;padding:0 4px}.monaco-editor .find-widget.replaceToggled{top:-74px;height:64px}.monaco-editor .find-widget.replaceToggled>.replace-part{display:flex;display:-webkit-flex;align-items:center}.monaco-editor .find-widget.replaceToggled.visible,.monaco-editor .find-widget.visible{top:0}.monaco-editor .find-widget .monaco-inputbox .input{background-color:transparent;min-height:0}.monaco-editor .find-widget .replace-input .input{font-size:13px}.monaco-editor .find-widget>.find-part,.monaco-editor .find-widget>.replace-part{margin:4px 0 0 17px;font-size:12px;display:flex;display:-webkit-flex;align-items:center}.monaco-editor .find-widget>.find-part .monaco-inputbox,.monaco-editor .find-widget>.replace-part .monaco-inputbox{height:25px}.monaco-editor .find-widget>.find-part .monaco-inputbox>.wrapper>.input,.monaco-editor .find-widget>.replace-part .monaco-inputbox>.wrapper>.input{padding-top:2px;padding-bottom:2px}.monaco-editor .find-widget .monaco-findInput{vertical-align:middle;display:flex;display:-webkit-flex;flex:1}.monaco-editor .find-widget .matchesCount{display:flex;display:-webkit-flex;flex:initial;margin:0 1px 0 3px;padding:2px 2px 0;height:25px;vertical-align:middle;box-sizing:border-box;text-align:center;line-height:23px}.monaco-editor .find-widget .button{min-width:20px;width:20px;height:20px;display:flex;display:-webkit-flex;flex:initial;margin-left:3px;background-position:50%;background-repeat:no-repeat;cursor:pointer}.monaco-editor .find-widget .button:not(.disabled):hover{background-color:rgba(0,0,0,.1)}.monaco-editor .find-widget .button.left{margin-left:0;margin-right:3px}.monaco-editor .find-widget .button.wide{width:auto;padding:1px 6px;top:-1px}.monaco-editor .find-widget .button.toggle{position:absolute;top:0;left:0;width:18px;height:100%;-o-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.monaco-editor .find-widget .button.toggle.disabled{display:none}.monaco-editor .find-widget .previous{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiCgkgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKCSB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iLTEgLTMgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgLTEgLTMgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8cG9seWdvbiBmaWxsPSIjNDI0MjQyIiBwb2ludHM9IjEzLDQgNiw0IDksMSA2LDEgMiw1IDYsOSA5LDkgNiw2IDEzLDYgIi8+Cjwvc3ZnPgo=)}.monaco-editor .find-widget .next{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiCgkgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKCSB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iLTEgLTMgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgLTEgLTMgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8cGF0aCBmaWxsPSIjNDI0MjQyIiBkPSJNMSw0aDdMNSwxaDNsNCw0TDgsOUg1bDMtM0gxVjR6Ii8+Cjwvc3ZnPgo=)}.monaco-editor .find-widget .disabled{opacity:.3;cursor:default}.monaco-editor .find-widget .monaco-checkbox{width:20px;height:20px;display:inline-block;vertical-align:middle;margin-left:3px}.monaco-editor .find-widget .monaco-checkbox .label{content:"";display:inline-block;background-repeat:no-repeat;background-position:0 0;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCI+CjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsLTEwMzIuMzYyMikiPgogIDxyZWN0IHdpZHRoPSI5IiBoZWlnaHQ9IjIiIHg9IjIiIHk9IjEwNDYuMzYyMiIgc3R5bGU9ImZpbGw6IzQyNDI0MjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICA8cmVjdCB3aWR0aD0iMTMiIGhlaWdodD0iMiIgeD0iMiIgeT0iMTA0My4zNjIyIiBzdHlsZT0iZmlsbDojNDI0MjQyO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgogIDxyZWN0IHdpZHRoPSI2IiBoZWlnaHQ9IjIiIHg9IjIiIHk9IjEwNDAuMzYyMiIgc3R5bGU9ImZpbGw6IzQyNDI0MjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICA8cmVjdCB3aWR0aD0iMTIiIGhlaWdodD0iMiIgeD0iMiIgeT0iMTAzNy4zNjIyIiBzdHlsZT0iZmlsbDojNDI0MjQyO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L2c+Cjwvc3ZnPg==);width:20px;height:20px;border:none}.monaco-editor .find-widget .monaco-checkbox .checkbox:disabled+.label{opacity:.3;cursor:default}.monaco-editor .find-widget .monaco-checkbox .checkbox:not(:disabled)+.label{cursor:pointer}.monaco-editor .find-widget .monaco-checkbox .checkbox:not(:disabled):hover:before+.label{background-color:#ddd}.monaco-editor .find-widget .monaco-checkbox .checkbox:checked+.label{background-color:hsla(0,0%,39%,.2)}.monaco-editor .find-widget .close-fw{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMyAzIDE2IDE2IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDMgMyAxNiAxNiI+PHBvbHlnb24gZmlsbD0iIzQyNDI0MiIgcG9pbnRzPSIxMi41OTcsMTEuMDQyIDE1LjQsMTMuODQ1IDEzLjg0NCwxNS40IDExLjA0MiwxMi41OTggOC4yMzksMTUuNCA2LjY4MywxMy44NDUgOS40ODUsMTEuMDQyIDYuNjgzLDguMjM5IDguMjM4LDYuNjgzIDExLjA0Miw5LjQ4NiAxMy44NDUsNi42ODMgMTUuNCw4LjIzOSIvPjwvc3ZnPg==)}.monaco-editor .find-widget .expand{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iIzY0NjQ2NSIgZD0iTTExIDEwLjA3aC01LjY1Nmw1LjY1Ni01LjY1NnY1LjY1NnoiLz48L3N2Zz4=)}.monaco-editor .find-widget .collapse{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iIzY0NjQ2NSIgZD0iTTYgNHY4bDQtNC00LTR6bTEgMi40MTRsMS41ODYgMS41ODYtMS41ODYgMS41ODZ2LTMuMTcyeiIvPjwvc3ZnPg==)}.monaco-editor .find-widget .replace{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMTZweCIKCSBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8ZyBpZD0iaWNvbl94NUZfYmciPgoJPGc+CgkJPHBhdGggZmlsbD0iIzQyNDI0MiIgZD0iTTExLDNWMWgtMXY1djFoMWgyaDFWNFYzSDExeiBNMTMsNmgtMlY0aDJWNnoiLz4KCQk8cGF0aCBmaWxsPSIjNDI0MjQyIiBkPSJNMiwxNWg3VjlIMlYxNXogTTQsMTBoM3YxSDV2MmgydjFINFYxMHoiLz4KCTwvZz4KPC9nPgo8ZyBpZD0iY29sb3JfeDVGX2ltcG9ydGFuY2UiPgoJPHBhdGggZmlsbD0iIzAwNTM5QyIgZD0iTTMuOTc5LDMuNUw0LDZMMyw1djEuNUw0LjUsOEw2LDYuNVY1TDUsNkw0Ljk3OSwzLjVjMC0wLjI3NSwwLjIyNS0wLjUsMC41LTAuNUg5VjJINS40NzkKCQlDNC42NTEsMiwzLjk3OSwyLjY3MywzLjk3OSwzLjV6Ii8+CjwvZz4KPC9zdmc+Cg==)}.monaco-editor .find-widget .replace-all{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMTZweCIKCSBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8ZyBpZD0iaWNvbl94NUZfYmciPgoJPHBhdGggZmlsbD0iIzQyNDI0MiIgZD0iTTExLDE1VjlIMXY2SDExeiBNMiwxNHYtMmgxdi0xSDJ2LTFoM3Y0SDJ6IE0xMCwxMUg4djJoMnYxSDd2LTRoM1YxMXogTTMsMTN2LTFoMXYxSDN6IE0xMyw3djZoLTFWOEg1VjcKCQlIMTN6IE0xMywyVjFoLTF2NWgzVjJIMTN6IE0xNCw1aC0xVjNoMVY1eiBNMTEsMnY0SDhWNGgxdjFoMVY0SDlWM0g4VjJIMTF6Ii8+CjwvZz4KPGcgaWQ9ImNvbG9yX3g1Rl9hY3Rpb24iPgoJPHBhdGggZmlsbD0iIzAwNTM5QyIgZD0iTTEuOTc5LDMuNUwyLDZMMSw1djEuNUwyLjUsOEw0LDYuNVY1TDMsNkwyLjk3OSwzLjVjMC0wLjI3NSwwLjIyNS0wLjUsMC41LTAuNUg3VjJIMy40NzkKCQlDMi42NTEsMiwxLjk3OSwyLjY3MywxLjk3OSwzLjV6Ii8+CjwvZz4KPC9zdmc+Cg==)}.monaco-editor .find-widget>.replace-part{display:none}.monaco-editor .find-widget>.replace-part>.replace-input{display:flex;display:-webkit-flex;vertical-align:middle;width:auto!important}.monaco-editor .find-widget.reduced-find-widget .matchesCount,.monaco-editor .find-widget.reduced-find-widget .monaco-checkbox{display:none}.monaco-editor .find-widget.narrow-find-widget{max-width:257px!important}.monaco-editor .find-widget.collapsed-find-widget{max-width:170px!important}.monaco-editor .find-widget.collapsed-find-widget .button.next,.monaco-editor .find-widget.collapsed-find-widget .button.previous,.monaco-editor .find-widget.collapsed-find-widget .button.replace,.monaco-editor .find-widget.collapsed-find-widget .button.replace-all,.monaco-editor .find-widget.collapsed-find-widget>.find-part .monaco-findInput .controls{display:none}.monaco-editor .findMatch{-webkit-animation-duration:0;-webkit-animation-name:inherit!important;-moz-animation-duration:0;-moz-animation-name:inherit!important;-ms-animation-duration:0;-ms-animation-name:inherit!important;animation-duration:0;animation-name:inherit!important}.monaco-editor .find-widget .monaco-sash{width:2px!important;margin-left:-4px}.monaco-editor.hc-black .find-widget .previous,.monaco-editor.vs-dark .find-widget .previous{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiCgkgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKCSB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iLTEgLTMgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgLTEgLTMgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8cG9seWdvbiBmaWxsPSIjQzVDNUM1IiBwb2ludHM9IjEzLDQgNiw0IDksMSA2LDEgMiw1IDYsOSA5LDkgNiw2IDEzLDYgIi8+Cjwvc3ZnPgo=)}.monaco-editor.hc-black .find-widget .next,.monaco-editor.vs-dark .find-widget .next{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiCgkgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKCSB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iLTEgLTMgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgLTEgLTMgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8cGF0aCBmaWxsPSIjQzVDNUM1IiBkPSJNMSw0aDdMNSwxaDNsNCw0TDgsOUg1bDMtM0gxVjR6Ii8+Cjwvc3ZnPgo=)}.monaco-editor.hc-black .find-widget .monaco-checkbox .label,.monaco-editor.vs-dark .find-widget .monaco-checkbox .label{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCI+CjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsLTEwMzIuMzYyMikiPgogIDxyZWN0IHdpZHRoPSI5IiBoZWlnaHQ9IjIiIHg9IjIiIHk9IjEwNDYuMzYyMiIgc3R5bGU9ImZpbGw6I0M1QzVDNTtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICA8cmVjdCB3aWR0aD0iMTMiIGhlaWdodD0iMiIgeD0iMiIgeT0iMTA0My4zNjIyIiBzdHlsZT0iZmlsbDojQzVDNUM1O2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgogIDxyZWN0IHdpZHRoPSI2IiBoZWlnaHQ9IjIiIHg9IjIiIHk9IjEwNDAuMzYyMiIgc3R5bGU9ImZpbGw6I0M1QzVDNTtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICA8cmVjdCB3aWR0aD0iMTIiIGhlaWdodD0iMiIgeD0iMiIgeT0iMTAzNy4zNjIyIiBzdHlsZT0iZmlsbDojQzVDNUM1O2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L2c+Cjwvc3ZnPg==)}.monaco-editor.vs-dark .find-widget .monaco-checkbox .checkbox:checked+.label,.monaco-editor.vs-dark .find-widget .monaco-checkbox .checkbox:not(:disabled):hover:before+.label{background-color:hsla(0,0%,100%,.1)}.monaco-editor.hc-black .find-widget .close-fw,.monaco-editor.vs-dark .find-widget .close-fw{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMyAzIDE2IDE2IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDMgMyAxNiAxNiI+PHBvbHlnb24gZmlsbD0iI2U4ZThlOCIgcG9pbnRzPSIxMi41OTcsMTEuMDQyIDE1LjQsMTMuODQ1IDEzLjg0NCwxNS40IDExLjA0MiwxMi41OTggOC4yMzksMTUuNCA2LjY4MywxMy44NDUgOS40ODUsMTEuMDQyIDYuNjgzLDguMjM5IDguMjM4LDYuNjgzIDExLjA0Miw5LjQ4NiAxMy44NDUsNi42ODMgMTUuNCw4LjIzOSIvPjwvc3ZnPg==)}.monaco-editor.hc-black .find-widget .replace,.monaco-editor.vs-dark .find-widget .replace{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMTZweCIKCSBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8ZyBpZD0iaWNvbl94NUZfYmciPgoJPGc+CgkJPHBhdGggZmlsbD0iI0M1QzVDNSIgZD0iTTExLDNWMWgtMXY1djFoMWgyaDFWNFYzSDExeiBNMTMsNmgtMlY0aDJWNnoiLz4KCQk8cGF0aCBmaWxsPSIjQzVDNUM1IiBkPSJNMiwxNWg3VjlIMlYxNXogTTQsMTBoM3YxSDV2MmgydjFINFYxMHoiLz4KCTwvZz4KPC9nPgo8ZyBpZD0iY29sb3JfeDVGX2ltcG9ydGFuY2UiPgoJPHBhdGggZmlsbD0iIzc1QkVGRiIgZD0iTTMuOTc5LDMuNUw0LDZMMyw1djEuNUw0LjUsOEw2LDYuNVY1TDUsNkw0Ljk3OSwzLjVjMC0wLjI3NSwwLjIyNS0wLjUsMC41LTAuNUg5VjJINS40NzkKCQlDNC42NTEsMiwzLjk3OSwyLjY3MywzLjk3OSwzLjV6Ii8+CjwvZz4KPC9zdmc+Cg==)}.monaco-editor.hc-black .find-widget .replace-all,.monaco-editor.vs-dark .find-widget .replace-all{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMTZweCIKCSBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8ZyBpZD0iaWNvbl94NUZfYmciPgoJPHBhdGggZmlsbD0iI0M1QzVDNSIgZD0iTTExLDE1VjlIMXY2SDExeiBNMiwxNHYtMmgxdi0xSDJ2LTFoM3Y0SDJ6IE0xMCwxMUg4djJoMnYxSDd2LTRoM1YxMXogTTMsMTN2LTFoMXYxSDN6IE0xMyw3djZoLTFWOEg1VjcKCQlIMTN6IE0xMywyVjFoLTF2NWgzVjJIMTN6IE0xNCw1aC0xVjNoMVY1eiBNMTEsMnY0SDhWNGgxdjFoMVY0SDlWM0g4VjJIMTF6Ii8+CjwvZz4KPGcgaWQ9ImNvbG9yX3g1Rl9hY3Rpb24iPgoJPHBhdGggZmlsbD0iIzc1QkVGRiIgZD0iTTEuOTc5LDMuNUwyLDZMMSw1djEuNUwyLjUsOEw0LDYuNVY1TDMsNkwyLjk3OSwzLjVjMC0wLjI3NSwwLjIyNS0wLjUsMC41LTAuNUg3VjJIMy40NzkKCQlDMi42NTEsMiwxLjk3OSwyLjY3MywxLjk3OSwzLjV6Ii8+CjwvZz4KPC9zdmc+Cg==)}.monaco-editor.hc-black .find-widget .expand,.monaco-editor.vs-dark .find-widget .expand{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI2U4ZThlOCIgZD0iTTExIDEwLjA3aC01LjY1Nmw1LjY1Ni01LjY1NnY1LjY1NnoiLz48L3N2Zz4=)}.monaco-editor.hc-black .find-widget .collapse,.monaco-editor.vs-dark .find-widget .collapse{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI2U4ZThlOCIgZD0iTTYgNHY4bDQtNC00LTR6bTEgMi40MTRsMS41ODYgMS41ODYtMS41ODYgMS41ODZ2LTMuMTcyeiIvPjwvc3ZnPg==)}.monaco-editor.hc-black .find-widget .button:not(.disabled):hover,.monaco-editor.vs-dark .find-widget .button:not(.disabled):hover{background-color:hsla(0,0%,100%,.1)}.monaco-editor.hc-black .find-widget .button:before{position:relative;top:1px;left:2px}.monaco-editor.hc-black .find-widget .monaco-checkbox .checkbox:checked+.label{background-color:hsla(0,0%,100%,.1)}.monaco-editor .margin-view-overlays .folding{cursor:pointer;background-repeat:no-repeat;background-origin:border-box;background-position:calc(50% + 2px) 50%;background-size:auto calc(100% - 3px);opacity:0;transition:opacity .5s;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTUgMTUiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDE1IDE1OyI+CjxwYXRoIHN0eWxlPSJmaWxsOiNCNkI2QjYiIGQ9Ik0xMSw0djdINFY0SDExIE0xMiwzSDN2OWg5VjNMMTIsM3oiLz4KPGxpbmUgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzZCNkI2QjtzdHJva2UtbWl0ZXJsaW1pdDoxMCIgeDE9IjEwIiB5MT0iNy41IiB4Mj0iNSIgeTI9IjcuNSIvPgo8L3N2Zz4=)}.monaco-editor.hc-black .margin-view-overlays .folding,.monaco-editor.vs-dark .margin-view-overlays .folding{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTUgMTUiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDE1IDE1OyI+CjxwYXRoIHN0eWxlPSJmaWxsOiM1QTVBNUEiIGQ9Ik0xMSw0djdINFY0SDExIE0xMiwzSDN2OWg5VjNMMTIsM3oiLz4KPGxpbmUgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6I0M1QzVDNTtzdHJva2UtbWl0ZXJsaW1pdDoxMCIgeDE9IjEwIiB5MT0iNy41IiB4Mj0iNSIgeTI9IjcuNSIvPgo8L3N2Zz4=)}.monaco-editor .margin-view-overlays .folding.alwaysShowFoldIcons,.monaco-editor .margin-view-overlays:hover .folding{opacity:1}.monaco-editor .margin-view-overlays .folding.collapsed{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTUgMTUiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDE1IDE1OyI+CjxyZWN0IHg9IjMiIHk9IjMiIHN0eWxlPSJmaWxsOiNFOEU4RTgiIHdpZHRoPSI5IiBoZWlnaHQ9IjkiLz4KPHBhdGggc3R5bGU9ImZpbGw6I0I2QjZCNiIgZD0iTTExLDR2N0g0VjRIMTEgTTEyLDNIM3Y5aDlWM0wxMiwzeiIvPgo8bGluZSBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNkI2QjZCO3N0cm9rZS1taXRlcmxpbWl0OjEwIiB4MT0iMTAiIHkxPSI3LjUiIHgyPSI1IiB5Mj0iNy41Ii8+CjxsaW5lIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiM2QjZCNkI7c3Ryb2tlLW1pdGVybGltaXQ6MTAiIHgxPSI3LjUiIHkxPSI1IiB4Mj0iNy41IiB5Mj0iMTAiLz4KPC9zdmc+);opacity:1}.monaco-editor.hc-black .margin-view-overlays .folding.collapsed,.monaco-editor.vs-dark .margin-view-overlays .folding.collapsed{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTUgMTUiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDE1IDE1OyI+CjxyZWN0IHg9IjMiIHk9IjMiIHN0eWxlPSJvcGFjaXR5OjAuMTtmaWxsOiNGRkZGRkYiIHdpZHRoPSI5IiBoZWlnaHQ9IjkiLz4KPHBhdGggc3R5bGU9ImZpbGw6IzVBNUE1QSIgZD0iTTExLDR2N0g0VjRIMTEgTTEyLDNIM3Y5aDlWM0wxMiwzeiIvPgo8bGluZSBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojQzVDNUM1O3N0cm9rZS1taXRlcmxpbWl0OjEwIiB4MT0iMTAiIHkxPSI3LjUiIHgyPSI1IiB5Mj0iNy41Ii8+CjxsaW5lIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiNDNUM1QzU7c3Ryb2tlLW1pdGVybGltaXQ6MTAiIHgxPSI3LjUiIHkxPSI1IiB4Mj0iNy41IiB5Mj0iMTAiLz4KPC9zdmc+)}.monaco-editor .inline-folded:after{color:grey;margin:.1em .2em 0;content:"⋯";display:inline;line-height:1em;cursor:pointer}.monaco-editor .goto-definition-link{text-decoration:underline;cursor:pointer}.monaco-editor .peekview-widget .head .peekview-title .icon.warning{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIGhlaWdodD0iMTYiIHdpZHRoPSIxNiI+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTcuNSAyTDIgMTJsMiAyaDlsMi0yTDkuNSAyeiIvPjxwYXRoIGQ9Ik05IDNIOGwtNC41IDkgMSAxaDhsMS0xTDkgM3ptMCA5SDh2LTFoMXYxem0wLTJIOFY2aDF2NHoiIGZpbGw9IiNmYzAiLz48cGF0aCBkPSJNOSAxMEg4VjZoMXY0em0wIDFIOHYxaDF2LTF6Ii8+PC9zdmc+) 50% no-repeat}.monaco-editor .peekview-widget .head .peekview-title .icon.error{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIGhlaWdodD0iMTYiIHdpZHRoPSIxNiI+PGNpcmNsZSBjeD0iOCIgY3k9IjgiIHI9IjYiIGZpbGw9IiNGNkY2RjYiLz48cGF0aCBkPSJNOCAzQzUuMjM4IDMgMyA1LjIzOCAzIDhzMi4yMzggNSA1IDUgNS0yLjIzOCA1LTUtMi4yMzgtNS01LTV6bTMgN2wtMSAxLTItMi0yIDItMS0xIDItMi4wMjdMNSA2bDEtMSAyIDIgMi0yIDEgMS0yIDEuOTczTDExIDEweiIgZmlsbD0iI0U1MTQwMCIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMSA2bC0xLTEtMiAyLTItMi0xIDEgMiAxLjk3M0w1IDEwbDEgMSAyLTIgMiAyIDEtMS0yLTIuMDI3eiIvPjwvc3ZnPg==) 50% no-repeat}.monaco-editor .peekview-widget .head .peekview-title .icon.info{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIGhlaWdodD0iMTYiIHdpZHRoPSIxNiI+PGNpcmNsZSBjeD0iOC41IiBjeT0iNy41IiByPSI1LjUiIGZpbGw9IiNGNkY2RjYiLz48cGF0aCBkPSJNOC41IDNDNi4wMTUgMyA0IDUuMDE1IDQgNy41UzYuMDE1IDEyIDguNSAxMiAxMyA5Ljk4NSAxMyA3LjUgMTAuOTg1IDMgOC41IDN6bS41IDhIOFY2aDF2NXptMC02SDhWNGgxdjF6IiBmaWxsPSIjMUJBMUUyIi8+PHBhdGggZD0iTTggNmgxdjVIOFY2em0wLTJ2MWgxVjRIOHoiIGZpbGw9IiNmZmYiLz48L3N2Zz4=) 50% no-repeat}.vs-dark .monaco-editor .peekview-widget .head .peekview-title .icon.warning{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgaGVpZ2h0PSIxNiIgd2lkdGg9IjE2Ij48cGF0aCBmaWxsPSIjMUUxRTFFIiBkPSJNNy41IDJMMiAxMmwyIDJoOWwyLTJMOS41IDJ6Ii8+PHBhdGggZD0iTTkgM0g4bC00LjUgOSAxIDFoOGwxLTFMOSAzem0wIDlIOHYtMWgxdjF6bTAtMkg4VjZoMXY0eiIgZmlsbD0iI2ZjMCIvPjxwYXRoIGQ9Ik05IDEwSDhWNmgxdjR6bTAgMUg4djFoMXYtMXoiLz48L3N2Zz4=) 50% no-repeat}.vs-dark .monaco-editor .peekview-widget .head .peekview-title .icon.error{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgaGVpZ2h0PSIxNiIgd2lkdGg9IjE2Ij48Y2lyY2xlIGN4PSI4IiBjeT0iOCIgcj0iNiIgZmlsbD0iIzFFMUUxRSIvPjxwYXRoIGQ9Ik04IDNDNS4yMzggMyAzIDUuMjM4IDMgOHMyLjIzOCA1IDUgNSA1LTIuMjM4IDUtNS0yLjIzOC01LTUtNXptMyA3bC0xIDEtMi0yLTIgMi0xLTEgMi0yLjAyN0w1IDZsMS0xIDIgMiAyLTIgMSAxLTIgMS45NzNMMTEgMTB6IiBmaWxsPSIjRjQ4NzcxIi8+PHBhdGggZmlsbD0iIzI1MjUyNiIgZD0iTTExIDZsLTEtMS0yIDItMi0yLTEgMSAyIDEuOTczTDUgMTBsMSAxIDItMiAyIDIgMS0xLTItMi4wMjd6Ii8+PC9zdmc+) 50% no-repeat}.vs-dark .monaco-editor .peekview-widget .head .peekview-title .icon.info{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIGhlaWdodD0iMTYiIHdpZHRoPSIxNiI+PGNpcmNsZSBjeD0iOC41IiBjeT0iNy41IiByPSI1LjUiIGZpbGw9IiMxRTFFMUUiLz48cGF0aCBkPSJNOC41IDNDNi4wMTUgMyA0IDUuMDE1IDQgNy41UzYuMDE1IDEyIDguNSAxMiAxMyA5Ljk4NSAxMyA3LjUgMTAuOTg1IDMgOC41IDN6bS41IDhIOFY2aDF2NXptMC02SDhWNGgxdjF6IiBmaWxsPSIjMUJBMUUyIi8+PHBhdGggZD0iTTggNmgxdjVIOFY2em0wLTJ2MWgxVjRIOHoiIGZpbGw9IiMyNTI1MjYiLz48L3N2Zz4=) 50% no-repeat}.monaco-editor .marker-widget{text-overflow:ellipsis;white-space:nowrap}.monaco-editor .marker-widget>.stale{opacity:.6;font-style:italic}.monaco-editor .marker-widget .title{display:inline-block;padding-right:5px}.monaco-editor .marker-widget .descriptioncontainer{position:absolute;white-space:pre;-webkit-user-select:text;user-select:text;padding:8px 12px 0 20px}.monaco-editor .marker-widget .descriptioncontainer .message{display:flex;flex-direction:column}.monaco-editor .marker-widget .descriptioncontainer .message .details{padding-left:6px}.monaco-editor .marker-widget .descriptioncontainer .message .code,.monaco-editor .marker-widget .descriptioncontainer .message .source{opacity:.6}.monaco-editor .marker-widget .descriptioncontainer .filename{cursor:pointer}.monaco-editor-hover{cursor:default;position:absolute;overflow:hidden;z-index:50;-webkit-user-select:text;-ms-user-select:text;-moz-user-select:text;-o-user-select:text;user-select:text;box-sizing:initial;animation:fadein .1s linear;line-height:1.5em}.monaco-editor-hover.hidden{display:none}.monaco-editor-hover .hover-contents{padding:4px 8px}.monaco-editor-hover .markdown-hover>.hover-contents:not(.code-hover-contents){max-width:500px}.monaco-editor-hover p,.monaco-editor-hover ul{margin:8px 0}.monaco-editor-hover hr{margin:4px -10px -6px;height:1px}.monaco-editor-hover p:first-child,.monaco-editor-hover ul:first-child{margin-top:0}.monaco-editor-hover p:last-child,.monaco-editor-hover ul:last-child{margin-bottom:0}.monaco-editor-hover ul{padding-left:20px}.monaco-editor-hover li>p{margin-bottom:0}.monaco-editor-hover li>ul{margin-top:0}.monaco-editor-hover code{border-radius:3px;padding:0 .4em}.monaco-editor-hover .monaco-tokenized-source{white-space:pre-wrap;word-break:break-all}.monaco-editor-hover .hover-row.status-bar{font-size:12px;line-height:22px}.monaco-editor-hover .hover-row.status-bar .actions{display:flex}.monaco-editor-hover .hover-row.status-bar .actions .action-container{margin:0 8px;cursor:pointer}.monaco-editor-hover .hover-row.status-bar .actions .action-container .action .icon{padding-right:4px}.monaco-editor .detected-link,.monaco-editor .detected-link-active{text-decoration:underline;text-underline-position:under}.monaco-editor .detected-link-active{cursor:pointer}.monaco-editor .monaco-editor-overlaymessage{padding-bottom:8px}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.monaco-editor .monaco-editor-overlaymessage.fadeIn{animation:fadeIn .15s ease-out}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.monaco-editor .monaco-editor-overlaymessage.fadeOut{animation:fadeOut .1s ease-out}.monaco-editor .monaco-editor-overlaymessage .message{padding:1px 4px}.monaco-editor .monaco-editor-overlaymessage .anchor{width:0!important;height:0!important;border:8px solid transparent;z-index:1000;position:absolute}.monaco-editor .parameter-hints-widget{z-index:10;display:flex;flex-direction:column;line-height:1.5em}.monaco-editor .parameter-hints-widget>.wrapper{max-width:440px;display:flex;flex-direction:column}.monaco-editor .parameter-hints-widget.multiple{min-height:3.3em;padding:0 0 0 1.9em}.monaco-editor .parameter-hints-widget.visible{transition:left .05s ease-in-out}.monaco-editor .parameter-hints-widget p,.monaco-editor .parameter-hints-widget ul{margin:8px 0}.monaco-editor .parameter-hints-widget .body,.monaco-editor .parameter-hints-widget .monaco-scrollable-element{display:flex;flex-direction:column}.monaco-editor .parameter-hints-widget .signature{padding:4px 5px}.monaco-editor .parameter-hints-widget .docs{padding:0 10px 0 5px;white-space:pre-wrap}.monaco-editor .parameter-hints-widget .docs .markdown-docs{white-space:normal}.monaco-editor .parameter-hints-widget .docs .code{white-space:pre-wrap}.monaco-editor .parameter-hints-widget .docs code{border-radius:3px;padding:0 .4em}.monaco-editor .parameter-hints-widget .buttons{position:absolute;display:none;bottom:0;left:0}.monaco-editor .parameter-hints-widget.multiple .buttons{display:block}.monaco-editor .parameter-hints-widget.multiple .button{position:absolute;left:2px;width:16px;height:16px;background-repeat:no-repeat;cursor:pointer}.monaco-editor .parameter-hints-widget .button.previous{bottom:24px;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iIzQyNDI0MiIgZD0iTTEwLjggOS41bC45LS45TDguMSA1IDQuMiA4LjZsLjkuOSAzLTIuNyAyLjcgMi43eiIvPjwvc3ZnPg==)}.monaco-editor .parameter-hints-widget .button.next{bottom:0;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iIzQyNDI0MiIgZD0iTTUuMSA1bC0uOS45IDMuNiAzLjYgMy45LTMuNi0xLS45LTMgMi43TDUuMSA1eiIvPjwvc3ZnPg==)}.monaco-editor .parameter-hints-widget .overloads{position:absolute;display:none;text-align:center;bottom:14px;left:0;width:22px;height:12px;line-height:12px;opacity:.5}.monaco-editor .parameter-hints-widget.multiple .overloads{display:block}.monaco-editor .parameter-hints-widget .signature .parameter.active{font-weight:700;text-decoration:underline}.monaco-editor .parameter-hints-widget .documentation-parameter>.parameter{font-weight:700;margin-right:.5em}.monaco-editor.hc-black .parameter-hints-widget .button.previous,.monaco-editor.vs-dark .parameter-hints-widget .button.previous{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iI0U4RThFOCIgZD0iTTEwLjggOS41bC45LS45TDguMSA1IDQuMiA4LjZsLjkuOSAzLTIuNyAyLjcgMi43eiIvPjwvc3ZnPg==)}.monaco-editor.hc-black .parameter-hints-widget .button.next,.monaco-editor.vs-dark .parameter-hints-widget .button.next{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iI0U4RThFOCIgZD0iTTUuMSA1bC0uOS45IDMuNiAzLjYgMy45LTMuNi0xLS45LTMgMi43TDUuMSA1eiIvPjwvc3ZnPg==)}.monaco-editor .peekview-widget .head{-o-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;display:flex}.monaco-editor .peekview-widget .head .peekview-title{display:inline-block;font-size:13px;margin-left:20px;cursor:pointer}.monaco-editor .peekview-widget .head .peekview-title .icon{display:inline-block;height:16px;width:16px;vertical-align:text-bottom;margin-right:4px}.monaco-editor .peekview-widget .head .peekview-title .dirname:not(:empty){font-size:.9em;margin-left:.5em}.monaco-editor .peekview-widget .head .peekview-actions{flex:1;text-align:right;padding-right:2px}.monaco-editor .peekview-widget .head .peekview-actions>.monaco-action-bar{display:inline-block}.monaco-editor .peekview-widget .head .peekview-actions>.monaco-action-bar,.monaco-editor .peekview-widget .head .peekview-actions>.monaco-action-bar>.actions-container{height:100%}.monaco-editor .peekview-widget .head .peekview-actions>.monaco-action-bar .action-item{margin-left:4px}.monaco-editor .peekview-widget .head .peekview-actions>.monaco-action-bar .action-label{width:16px;height:100%;margin:0;line-height:inherit;background-repeat:no-repeat;background-position:50%}.monaco-editor .peekview-widget .head .peekview-actions>.monaco-action-bar .action-label.octicon{margin:0}.monaco-editor .peekview-widget .head .peekview-actions .action-label.icon.close-peekview-action{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMyAzIDE2IDE2IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDMgMyAxNiAxNiI+PHBvbHlnb24gZmlsbD0iIzQyNDI0MiIgcG9pbnRzPSIxMi41OTcsMTEuMDQyIDE1LjQsMTMuODQ1IDEzLjg0NCwxNS40IDExLjA0MiwxMi41OTggOC4yMzksMTUuNCA2LjY4MywxMy44NDUgOS40ODUsMTEuMDQyIDYuNjgzLDguMjM5IDguMjM4LDYuNjgzIDExLjA0Miw5LjQ4NiAxMy44NDUsNi42ODMgMTUuNCw4LjIzOSIvPjwvc3ZnPg==) 50% no-repeat}.monaco-editor .peekview-widget>.body{border-top:1px solid;position:relative}.monaco-editor.hc-black .peekview-widget .head .peekview-actions .action-label.icon.close-peekview-action,.monaco-editor.vs-dark .peekview-widget .head .peekview-actions .action-label.icon.close-peekview-action{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMyAzIDE2IDE2IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDMgMyAxNiAxNiI+PHBvbHlnb24gZmlsbD0iI2U4ZThlOCIgcG9pbnRzPSIxMi41OTcsMTEuMDQyIDE1LjQsMTMuODQ1IDEzLjg0NCwxNS40IDExLjA0MiwxMi41OTggOC4yMzksMTUuNCA2LjY4MywxMy44NDUgOS40ODUsMTEuMDQyIDYuNjgzLDguMjM5IDguMjM4LDYuNjgzIDExLjA0Miw5LjQ4NiAxMy44NDUsNi42ODMgMTUuNCw4LjIzOSIvPjwvc3ZnPg==) 50% no-repeat}.monaco-editor .peekview-widget .peekview-actions .icon.chevron-up{background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwKSI+CjxwYXRoIGQ9Ik0xNCAxMC41TDEyLjk0NTMgMTEuNTU1N0w4IDYuNjEwNDVMMy4wNTQ3MyAxMS41NTU3TDIgMTAuNUw4IDQuNUwxNCAxMC41WiIgZmlsbD0iIzRCNEI0QiIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwIj4KPHJlY3Qgd2lkdGg9IjEyIiBoZWlnaHQ9IjEyIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMiAyKSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=) 50% no-repeat}.vs-dark .monaco-editor .peekview-widget .peekview-actions .icon.chevron-up{background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwKSI+CjxwYXRoIGQ9Ik0xNCAxMC41TDEyLjk0NTMgMTEuNTU1N0w4IDYuNjEwNDVMMy4wNTQ3MyAxMS41NTU3TDIgMTAuNUw4IDQuNUwxNCAxMC41WiIgZmlsbD0iI0M4QzhDOCIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwIj4KPHJlY3Qgd2lkdGg9IjEyIiBoZWlnaHQ9IjEyIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMiAyKSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=) 50% no-repeat}.hc-black .monaco-editor .peekview-widget .peekview-actions .icon.chevron-up{background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwKSI+CjxwYXRoIGQ9Ik0xNCAxMC41TDEyLjk0NTMgMTEuNTU1N0w4IDYuNjEwNDVMMy4wNTQ3MyAxMS41NTU3TDIgMTAuNUw4IDQuNUwxNCAxMC41WiIgZmlsbD0id2hpdGUiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMCI+CjxyZWN0IHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIgMikiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K) 50% no-repeat}.monaco-editor .peekview-widget .peekview-actions .icon.chevron-down{background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwKSI+CjxwYXRoIGQ9Ik0yIDUuNUwzLjA1NDczIDQuNDQ0MjhMOCA5LjM4OTU1TDEyLjk0NTMgNC40NDQyOEwxNCA1LjVMOCAxMS41TDIgNS41WiIgZmlsbD0iIzRCNEI0QiIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwIj4KPHJlY3Qgd2lkdGg9IjEyIiBoZWlnaHQ9IjEyIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMiAyKSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=) 50% no-repeat}.vs-dark .monaco-editor .peekview-widget .peekview-actions .icon.chevron-down{background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwKSI+CjxwYXRoIGQ9Ik0yIDUuNUwzLjA1NDczIDQuNDQ0MjhMOCA5LjM4OTU1TDEyLjk0NTMgNC40NDQyOEwxNCA1LjVMOCAxMS41TDIgNS41WiIgZmlsbD0iI0M4QzhDOCIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwIj4KPHJlY3Qgd2lkdGg9IjEyIiBoZWlnaHQ9IjEyIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMiAyKSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=) 50% no-repeat}.hc-black .monaco-editor .peekview-widget .peekview-actions .icon.chevron-down{background:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwKSI+CjxwYXRoIGQ9Ik0yIDUuNUwzLjA1NDczIDQuNDQ0MjhMOCA5LjM4OTU1TDEyLjk0NTMgNC40NDQyOEwxNCA1LjVMOCAxMS41TDIgNS41WiIgZmlsbD0id2hpdGUiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMCI+CjxyZWN0IHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIgMikiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K) 50% no-repeat}.monaco-editor .zone-widget .zone-widget-container.reference-zone-widget{border-top-width:1px;border-bottom-width:1px}.monaco-editor .reference-zone-widget .inline{display:inline-block;vertical-align:top}.monaco-editor .reference-zone-widget .messages{height:100%;width:100%;text-align:center;padding:3em 0}.monaco-editor .reference-zone-widget .ref-tree{line-height:23px}.monaco-editor .reference-zone-widget .ref-tree .reference{text-overflow:ellipsis;overflow:hidden}.monaco-editor .reference-zone-widget .ref-tree .reference-file{display:inline-flex;width:100%;height:100%}.monaco-editor .reference-zone-widget .ref-tree .monaco-list:focus .selected .reference-file{color:inherit!important}.monaco-editor .reference-zone-widget .ref-tree .reference-file .count{margin-right:12px;margin-left:auto}.monaco-editor.hc-black .reference-zone-widget .ref-tree .reference-file{font-weight:700}.monaco-editor .rename-box{z-index:100;color:inherit}.monaco-editor .rename-box .rename-input{padding:4px}.monaco-editor .snippet-placeholder{min-width:2px}.monaco-editor .finish-snippet-placeholder,.monaco-editor .snippet-placeholder{outline-style:solid;outline-width:1px}.monaco-editor .suggest-widget{z-index:40;width:430px}.monaco-editor .suggest-widget>.details,.monaco-editor .suggest-widget>.message,.monaco-editor .suggest-widget>.tree{width:100%;border-style:solid;border-width:1px;box-sizing:border-box}.monaco-editor.hc-black .suggest-widget>.details,.monaco-editor.hc-black .suggest-widget>.message,.monaco-editor.hc-black .suggest-widget>.tree{border-width:2px}.monaco-editor .suggest-widget.docs-side{width:660px}.monaco-editor .suggest-widget.docs-side>.details,.monaco-editor .suggest-widget.docs-side>.tree{width:50%;float:left}.monaco-editor .suggest-widget.docs-side.list-right>.details,.monaco-editor .suggest-widget.docs-side.list-right>.tree{float:right}.monaco-editor .suggest-widget>.message{padding-left:22px}.monaco-editor .suggest-widget>.tree{height:100%}.monaco-editor .suggest-widget .monaco-list{-webkit-user-select:none;-moz-user-select:-moz-none;-ms-user-select:none;-o-user-select:none;user-select:none}.monaco-editor .suggest-widget .monaco-list .monaco-list-row{display:flex;-mox-box-sizing:border-box;box-sizing:border-box;padding-right:10px;background-repeat:no-repeat;background-position:2px 2px;white-space:nowrap;cursor:pointer;touch-action:none}.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents{flex:1;height:100%;overflow:hidden;padding-left:2px}.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main{display:flex;overflow:hidden;text-overflow:ellipsis;white-space:pre}.monaco-editor .suggest-widget:not(.frozen) .monaco-highlighted-label .highlight{font-weight:700}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.close,.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.readMore{opacity:.6;background-position:50%;background-repeat:no-repeat;background-size:70%;cursor:pointer}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.close{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMyAzIDE2IDE2IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDMgMyAxNiAxNiI+PHBvbHlnb24gZmlsbD0iIzQyNDI0MiIgcG9pbnRzPSIxMi41OTcsMTEuMDQyIDE1LjQsMTMuODQ1IDEzLjg0NCwxNS40IDExLjA0MiwxMi41OTggOC4yMzksMTUuNCA2LjY4MywxMy44NDUgOS40ODUsMTEuMDQyIDYuNjgzLDguMjM5IDguMjM4LDYuNjgzIDExLjA0Miw5LjQ4NiAxMy44NDUsNi42ODMgMTUuNCw4LjIzOSIvPjwvc3ZnPg==);float:right;margin-right:5px}.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.readMore{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTggMWMtMy44NjUgMC03IDMuMTM1LTcgN3MzLjEzNSA3IDcgNyA3LTMuMTM1IDctNy0zLjEzNS03LTctN3ptMSAxMmgtMnYtN2gydjd6bTAtOGgtMnYtMmgydjJ6IiBmaWxsPSIjMUJBMUUyIi8+PHBhdGggZD0iTTcgNmgydjdoLTJ2LTd6bTAtMWgydi0yaC0ydjJ6IiBmaWxsPSIjZmZmIi8+PC9zdmc+)}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.close:hover,.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.readMore:hover{opacity:1}.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.type-label{margin-left:.8em;flex:1;text-align:right;overflow:hidden;text-overflow:ellipsis;opacity:.7;white-space:nowrap}.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.type-label>.monaco-tokenized-source{display:inline}.monaco-editor .suggest-widget.docs-below .monaco-list .monaco-list-row.focused>.contents>.main>.readMore,.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused>.contents>.main>.readMore,.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused>.contents>.main>.type-label,.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.readMore,.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.type-label{display:none}.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused>.contents>.main>.readMore,.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused>.contents>.main>.type-label{display:inline}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label:before{height:100%}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon{display:block;height:16px;width:16px;margin-left:2px;background-repeat:no-repeat;background-size:80%;background-position:50%}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.hide,.monaco-editor .suggest-widget.no-icons .monaco-list .monaco-list-row .icon,.monaco-editor .suggest-widget.no-icons .monaco-list .monaco-list-row .monaco-icon-label.suggest-icon:before{display:none}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label.suggest-icon:before{content:" ";background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE2IDEwYzAgMi4yMDUtMS43OTQgNC00IDQtMS44NTggMC0zLjQxMS0xLjI3OS0zLjg1OC0zaC0uOTc4bDIuMzE4IDRIMHYtMS43MDNsMi0zLjQwOFYwaDExdjYuMTQyYzEuNzIxLjQ0NyAzIDIgMyAzLjg1OHoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYmciIGQ9Ik0xMiAxdjQuNzVBNC4yNTUgNC4yNTUgMCAwIDAgNy43NSAxMGgtLjczMkw0LjI3NSA1LjI2OSAzIDcuNDQyVjFoOXpNNy43NDcgMTRMNC4yNjkgOCAuNzQ4IDE0aDYuOTk5ek0xNSAxMGEzIDMgMCAxIDEtNiAwIDMgMyAwIDAgMSA2IDB6IiBpZD0iaWNvbkJnIi8+PC9zdmc+);background-repeat:no-repeat;background-position:50%;background-size:75%}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constructor:before,.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.function:before,.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.method:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtZmd7ZmlsbDojZjBlZmYxfS5pY29uLXZzLWFjdGlvbi1wdXJwbGV7ZmlsbDojNjUyZDkwfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE1IDMuMzQ5djguNDAzTDguOTc1IDE2SDguMDdMMSAxMS41ODJWMy4zMjdMNy41OTUgMGgxLjExOEwxNSAzLjM0OXoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik0xMi43MTUgNC4zOThMOC40ODcgNy4wMiAzLjU2NSA0LjI3Mmw0LjU3OC0yLjMwOSA0LjU3MiAyLjQzNXpNMyA1LjEwMmw1IDIuNzkydjUuNzA1bC01LTMuMTI1VjUuMTAyem02IDguNDM0VjcuODc4bDQtMi40OHY1LjMxN2wtNCAyLjgyMXoiIGlkPSJpY29uRmciLz48cGF0aCBjbGFzcz0iaWNvbi12cy1hY3Rpb24tcHVycGxlIiBkPSJNOC4xNTYuODM3TDIgMy45NDJ2Ny4wODVMOC41MTcgMTUuMSAxNCAxMS4yMzNWMy45NUw4LjE1Ni44Mzd6bTQuNTU5IDMuNTYxTDguNDg3IDcuMDIgMy41NjUgNC4yNzJsNC41NzgtMi4zMDkgNC41NzIgMi40MzV6TTMgNS4xMDJsNSAyLjc5MnY1LjcwNWwtNS0zLjEyNVY1LjEwMnptNiA4LjQzNFY3Ljg3OGw0LTIuNDh2NS4zMTdsLTQgMi44MjF6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.field:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtZmd7ZmlsbDojZjBlZmYxfS5pY29uLXZzLWFjdGlvbi1ibHVle2ZpbGw6IzAwNTM5Y308L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0wIDEwLjczNlY0LjVMOSAwbDcgMy41djYuMjM2bC05IDQuNS03LTMuNXoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLWJsdWUiIGQ9Ik05IDFMMSA1djVsNiAzIDgtNFY0TDkgMXpNNyA2Ljg4MkwzLjIzNiA1IDkgMi4xMTggMTIuNzY0IDQgNyA2Ljg4MnoiIGlkPSJpY29uQmciLz48cGF0aCBjbGFzcz0iaWNvbi12cy1mZyIgZD0iTTkgMi4xMThMMTIuNzY0IDQgNyA2Ljg4MiAzLjIzNiA1IDkgMi4xMTh6IiBpZD0iaWNvbkZnIi8+PC9zdmc+)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.event:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYWN0aW9uLW9yYW5nZXtmaWxsOiNjMjdkMWF9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0iY2FudmFzIi8+PHBhdGggY2xhc3M9Imljb24tdnMtb3V0IiBkPSJNMTQgMS40MTRMOS40MTQgNkgxNHYxLjQxNEw1LjQxNCAxNkgzdi0xLjIzNEw1LjM3MSAxMEgyVjguNzY0TDYuMzgyIDBIMTR2MS40MTR6IiBpZD0ib3V0bGluZSIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ii8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLW9yYW5nZSIgZD0iTTcgN2g2bC04IDhINGwyLjk4NS02SDNsNC04aDZMNyA3eiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.operator:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtZmd7ZmlsbDojZjBlZmYxfS5pY29uLXZzLWFjdGlvbi1ibHVle2ZpbGw6IzAwNTM5Y308L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0ib3V0bGluZSIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ii8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLWJsdWUiIGQ9Ik0xIDF2MTRoMTRWMUgxem02IDEySDN2LTFoNHYxem0wLTNIM1Y5aDR2MXptMC01SDV2Mkg0VjVIMlY0aDJWMmgxdjJoMnYxem0zLjI4MSA4SDguNzE5bDMtNGgxLjU2M2wtMy4wMDEgNHpNMTQgNUg5VjRoNXYxeiIgaWQ9Imljb25CZyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWZnIiBkPSJNNyA1SDV2Mkg0VjVIMlY0aDJWMmgxdjJoMnYxem03LTFIOXYxaDVWNHpNNyA5SDN2MWg0Vjl6bTAgM0gzdjFoNHYtMXptMy4yODEgMWwzLTRoLTEuNTYzbC0zIDRoMS41NjN6IiBpZD0iaWNvbkZnIiBzdHlsZT0iZGlzcGxheTogbm9uZTsiLz48L3N2Zz4=)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.variable:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfS5pY29uLXZzLWZne2ZpbGw6I2YwZWZmMX0uaWNvbi12cy1hY3Rpb24tYmx1ZXtmaWxsOiMwMDUzOWN9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0iY2FudmFzIi8+PHBhdGggY2xhc3M9Imljb24tdnMtb3V0IiBkPSJNMTEgM3YxLjAxNUw4LjczMyAyLjg4MiA1IDQuNzQ5VjNIMHYxMGg1di0xLjg1OWwyLjE1NiAxLjA3N0wxMSAxMC4yOTVWMTNoNVYzaC01eiIgaWQ9Im91dGxpbmUiIHN0eWxlPSJkaXNwbGF5OiBub25lOyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNMiA1djZoMnYxSDFWNGgzdjFIMnptMTAgNnYxaDNWNGgtM3YxaDJ2NmgtMnoiIGlkPSJpY29uQmciLz48cGF0aCBjbGFzcz0iaWNvbi12cy1mZyIgZD0iTTcuMTU2IDcuMTU2bC0xLjU3OC0uNzg5IDMuMTU2LTEuNTc4IDEuNTc4Ljc4OS0zLjE1NiAxLjU3OHoiIGlkPSJpY29uRmciIHN0eWxlPSJkaXNwbGF5OiBub25lOyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWFjdGlvbi1ibHVlIiBkPSJNOC43MzMgNEw0IDYuMzY3djMuMTU2TDcuMTU2IDExLjFsNC43MzMtMi4zNjdWNS41NzhMOC43MzMgNHpNNy4xNTYgNy4xNTZsLTEuNTc4LS43ODkgMy4xNTYtMS41NzggMS41NzguNzg5LTMuMTU2IDEuNTc4eiIgaWQ9ImNvbG9ySW1wb3J0YW5jZSIvPjwvc3ZnPg==)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.class:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYWN0aW9uLW9yYW5nZXtmaWxsOiNjMjdkMWF9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0iY2FudmFzIi8+PHBhdGggY2xhc3M9Imljb24tdnMtb3V0IiBkPSJNMTYgNi41ODZsLTMtM0wxMS41ODYgNUg5LjQxNGwxLTEtNC00aC0uODI4TDAgNS41ODZ2LjgyOGw0IDRMNi40MTQgOEg3djVoMS41ODZsMyAzaC44MjhMMTYgMTIuNDE0di0uODI4TDEzLjkxNCA5LjUgMTYgNy40MTR2LS44Mjh6IiBpZD0ib3V0bGluZSIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWFjdGlvbi1vcmFuZ2UiIGQ9Ik0xMyAxMGwyIDItMyAzLTItMiAxLTFIOFY3SDZMNCA5IDEgNmw1LTUgMyAzLTIgMmg1bDEtMSAyIDItMyAzLTItMiAxLTFIOXY0bDIuOTk5LjAwMkwxMyAxMHoiIGlkPSJpY29uQmciLz48L3N2Zz4=)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.interface:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtZmd7ZmlsbDojZjBlZmYxfS5pY29uLXZzLWFjdGlvbi1ibHVle2ZpbGw6IzAwNTM5Y308L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xMS41IDEyYy0xLjkxNSAwLTMuNjAyLTEuMjQxLTQuMjI4LTNoLTEuNDFhMy4xMSAzLjExIDAgMCAxLTIuNzM3IDEuNjI1QzEuNDAyIDEwLjYyNSAwIDkuMjIzIDAgNy41czEuNDAyLTMuMTI1IDMuMTI1LTMuMTI1YzEuMTY1IDAgMi4yMDEuNjM5IDIuNzM3IDEuNjI1aDEuNDFjLjYyNi0xLjc1OSAyLjMxMy0zIDQuMjI4LTNDMTMuOTgxIDMgMTYgNS4wMTkgMTYgNy41UzEzLjk4MSAxMiAxMS41IDEyeiIgaWQ9Im91dGxpbmUiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1mZyIgZD0iTTExLjUgOUExLjUwMSAxLjUwMSAwIDEgMSAxMyA3LjVjMCAuODI2LS42NzMgMS41LTEuNSAxLjV6IiBpZD0iaWNvbkZnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLWJsdWUiIGQ9Ik0xMS41IDRhMy40OSAzLjQ5IDAgMCAwLTMuNDUgM0g1LjE4NUEyLjEyMiAyLjEyMiAwIDAgMCAxIDcuNWEyLjEyMyAyLjEyMyAwIDEgMCA0LjE4NS41SDguMDVhMy40OSAzLjQ5IDAgMCAwIDMuNDUgMyAzLjUgMy41IDAgMSAwIDAtN3ptMCA1Yy0uODI3IDAtMS41LS42NzMtMS41LTEuNVMxMC42NzMgNiAxMS41IDZzMS41LjY3MyAxLjUgMS41UzEyLjMyNyA5IDExLjUgOXoiIGlkPSJpY29uQmciLz48L3N2Zz4=)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.struct:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYWN0aW9uLWJsdWV7ZmlsbDojMDA1MzljfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTkgMTRWOEg3djZIMVYyaDE0djEySDl6IiBpZD0ib3V0bGluZSIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ii8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLWJsdWUiIGQ9Ik0xMCA5aDR2NGgtNFY5em0tOCA0aDRWOUgydjR6TTIgM3Y0aDEyVjNIMnoiIGlkPSJpY29uQmciLz48L3N2Zz4=)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.type-parameter:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTEwLjcwMiAxMC41bDItMi0yLTIgLjUtLjVIMTB2NWgxdjNINXYtM2gxVjZINC43OThsLjUuNS0yIDIgMiAyTDMgMTIuNzk3bC0zLTNWNy4yMDFsMy0zVjJoMTB2Mi4yMDFsMyAzdjIuNTk2bC0zIDMtMi4yOTgtMi4yOTd6IiBpZD0ib3V0bGluZSIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ii8+PHBhdGggY2xhc3M9Imljb24tdnMtYmciIGQ9Ik00IDNoOHYyaC0xdi0uNWMwLS4yNzctLjIyNC0uNS0uNS0uNUg5djcuNWMwIC4yNzUuMjI0LjUuNS41aC41djFINnYtMWguNWEuNS41IDAgMCAwIC41LS41VjRINS41YS41LjUgMCAwIDAtLjUuNVY1SDRWM3pNMyA1LjYxNUwuMTE2IDguNSAzIDExLjM4M2wuODg0LS44ODMtMi0yIDItMkwzIDUuNjE1em0xMCAwbC0uODg0Ljg4NSAyIDItMiAyIC44ODQuODgzTDE1Ljg4NCA4LjUgMTMgNS42MTV6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.module:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTkuMjYgMTEuOTg0bC45NzgtLjAyMWEuOTYyLjk2MiAwIDAgMCAuMDktLjAwNmMuMDExLS4wNjMuMDI2LS4xNzkuMDI2LS4zNjFWOS42ODhjMC0uNjc5LjE4NS0xLjI1Ny41My0xLjcwNy0uMzQ2LS40NTItLjUzLTEuMDMtLjUzLTEuNzA1VjQuMzVjMC0uMTY3LS4wMjEtLjI1OS0uMDM0LS4zMDJMOS4yNiA0LjAyVi45NzNsMS4wMTEuMDExYzIuMTY3LjAyNCAzLjQwOSAxLjE1NiAzLjQwOSAzLjEwNXYxLjk2MmMwIC4zNTEuMDcxLjQ2MS4wNzIuNDYybC45MzYuMDYuMDUzLjkyN3YxLjkzNmwtLjkzNi4wNjFjLS4wNzYuMDE2LS4xMjUuMTQ2LS4xMjUuNDI0djIuMDE3YzAgLjkxNC0uMzMyIDMuMDQzLTMuNDA4IDMuMDc4bC0xLjAxMi4wMTF2LTMuMDQzem0tMy41MjEgMy4wMzJjLTMuMDg5LS4wMzUtMy40MjItMi4xNjQtMy40MjItMy4wNzhWOS45MjFjMC0uMzI3LS4wNjYtLjQzMi0uMDY3LS40MzNsLS45MzctLjA2LS4wNjMtLjkyOVY2LjU2M2wuOTQyLS4wNmMuMDU4IDAgLjEyNS0uMTE0LjEyNS0uNDUyVjQuMDljMC0xLjk0OSAxLjI0OC0zLjA4MSAzLjQyMi0zLjEwNUw2Ljc1Ljk3M1Y0LjAybC0uOTc1LjAyM2EuNTcyLjU3MiAwIDAgMC0uMDkzLjAxYy4wMDYuMDIxLS4wMTkuMTE1LS4wMTkuMjk3djEuOTI4YzAgLjY3NS0uMTg2IDEuMjUzLS41MzQgMS43MDUuMzQ4LjQ1LjUzNCAxLjAyOC41MzQgMS43MDd2MS45MDdjMCAuMTc1LjAxNC4yOTEuMDI3LjM2My4wMjMuMDAyIDEuMDYuMDI1IDEuMDYuMDI1djMuMDQzbC0xLjAxMS0uMDEyeiIgaWQ9Im91dGxpbmUiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1iZyIgZD0iTTUuNzUgMTQuMDE2Yy0xLjYyMy0uMDE5LTIuNDM0LS43MTEtMi40MzQtMi4wNzhWOS45MjFjMC0uOTAyLS4zNTUtMS4zNzYtMS4wNjYtMS40MjJ2LS45OThjLjcxMS0uMDQ1IDEuMDY2LS41MjkgMS4wNjYtMS40NDlWNC4wOWMwLTEuMzg1LjgxMS0yLjA4NyAyLjQzNC0yLjEwNXYxLjA2Yy0uNzI1LjAxNy0xLjA4Ny40NTMtMS4wODcgMS4zMDV2MS45MjhjMCAuOTItLjQ1NCAxLjQ4OC0xLjM2IDEuNzAyVjhjLjkwNy4yMDEgMS4zNi43NjMgMS4zNiAxLjY4OHYxLjkwN2MwIC40ODguMDgxLjgzNS4yNDMgMS4wNDIuMTYyLjIwOC40NDMuMzE2Ljg0NC4zMjV2MS4wNTR6bTcuOTktNS41MTdjLS43MDYuMDQ1LTEuMDYuNTItMS4wNiAxLjQyMnYyLjAxN2MwIDEuMzY3LS44MDcgMi4wNi0yLjQyIDIuMDc4di0xLjA1M2MuMzk2LS4wMDkuNjc4LS4xMTguODQ0LS4zMjguMTY3LS4yMS4yNS0uNTU2LjI1LTEuMDM5VjkuNjg4YzAtLjkyNS40NDktMS40ODggMS4zNDctMS42ODh2LS4wMjFjLS44OTgtLjIxNC0xLjM0Ny0uNzgyLTEuMzQ3LTEuNzAyVjQuMzVjMC0uODUyLS4zNjQtMS4yODgtMS4wOTQtMS4zMDZ2LTEuMDZjMS42MTMuMDE4IDIuNDIuNzIgMi40MiAyLjEwNXYxLjk2MmMwIC45Mi4zNTQgMS40MDQgMS4wNiAxLjQ0OXYuOTk5eiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.property:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE2IDUuNWE1LjUgNS41IDAgMCAxLTUuNSA1LjVjLS4yNzUgMC0uNTQzLS4wMjctLjgwNy0uMDY2bC0uMDc5LS4wMTJhNS40MjkgNS40MjkgMCAwIDEtLjgxLS4xOTJsLTQuNTM3IDQuNTM3Yy0uNDcyLjQ3My0xLjEuNzMzLTEuNzY3LjczM3MtMS4yOTUtLjI2LTEuNzY4LS43MzJhMi41MDIgMi41MDIgMCAwIDEgMC0zLjUzNWw0LjUzNy00LjUzN2E1LjQ1MiA1LjQ1MiAwIDAgMS0uMTkxLS44MTJjLS4wMDUtLjAyNS0uMDA4LS4wNTEtLjAxMi0uMDc3QTUuNTAzIDUuNTAzIDAgMCAxIDUgNS41YTUuNSA1LjUgMCAxIDEgMTEgMHoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYmciIGQ9Ik0xNSA1LjVhNC41IDQuNSAwIDAgMS00LjUgNC41Yy0uNjkzIDAtMS4zNDItLjE3LTEuOTI5LS40NWwtNS4wMSA1LjAxYy0uMjkzLjI5NC0uNjc3LjQ0LTEuMDYxLjQ0cy0uNzY4LS4xNDYtMS4wNjEtLjQzOWExLjUgMS41IDAgMCAxIDAtMi4xMjFsNS4wMS01LjAxQTQuNDgzIDQuNDgzIDAgMCAxIDYgNS41IDQuNSA0LjUgMCAwIDEgMTAuNSAxYy42OTMgMCAxLjM0Mi4xNyAxLjkyOS40NUw5LjYzNiA0LjI0M2wyLjEyMSAyLjEyMSAyLjc5My0yLjc5M2MuMjguNTg3LjQ1IDEuMjM2LjQ1IDEuOTI5eiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.unit:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfS5pY29uLXZzLWZne2ZpbGw6I2YwZWZmMX08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNiAxMS4wMTNIMVY0aDE1djcuMDEzeiIgaWQ9Im91dGxpbmUiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1mZyIgZD0iTTggOUg3VjZoM3YzSDlWN0g4djJ6TTQgN2gxdjJoMVY2SDN2M2gxVjd6bTggMGgxdjJoMVY2aC0zdjNoMVY3eiIgaWQ9Imljb25GZyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNMiA1djVoMTNWNUgyem00IDRINVY3SDR2MkgzVjZoM3Yzem00IDBIOVY3SDh2Mkg3VjZoM3Yzem00IDBoLTFWN2gtMXYyaC0xVjZoM3YzeiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constant:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfS5pY29uLXZzLWZne2ZpbGw6I2YwZWZmMX0uaWNvbi12cy1hY3Rpb24tYmx1ZXtmaWxsOiMwMDUzOWN9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0iY2FudmFzIi8+PHBhdGggY2xhc3M9Imljb24tdnMtb3V0IiBkPSJNMi44NzkgMTRMMSAxMi4xMjFWMy44NzlMMi44NzkgMmgxMC4yNDJMMTUgMy44Nzl2OC4yNDJMMTMuMTIxIDE0SDIuODc5eiIgaWQ9Im91dGxpbmUiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1mZyIgZD0iTTEyLjI5MyA0SDMuNzA3TDMgNC43MDd2Ni41ODZsLjcwNy43MDdoOC41ODZsLjcwNy0uNzA3VjQuNzA3TDEyLjI5MyA0ek0xMSAxMEg1VjloNnYxem0wLTNINVY2aDZ2MXoiIGlkPSJpY29uRmciLz48ZyBpZD0iaWNvbkJnIj48cGF0aCBjbGFzcz0iaWNvbi12cy1iZyIgZD0iTTEyLjcwNyAxM0gzLjI5M0wyIDExLjcwN1Y0LjI5M0wzLjI5MyAzaDkuNDE0TDE0IDQuMjkzdjcuNDE0TDEyLjcwNyAxM3ptLTktMWg4LjU4NmwuNzA3LS43MDdWNC43MDdMMTIuMjkzIDRIMy43MDdMMyA0LjcwN3Y2LjU4NmwuNzA3LjcwN3oiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1hY3Rpb24tYmx1ZSIgZD0iTTExIDdINVY2aDZ2MXptMCAySDV2MWg2Vjl6Ii8+PC9nPjwvc3ZnPg==)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum:before,.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.value:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtZmd7ZmlsbDojZjBlZmYxfS5pY29uLXZzLWFjdGlvbi1vcmFuZ2V7ZmlsbDojYzI3ZDFhfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE0LjQxNCAxTDE2IDIuNTg2djUuODI4TDE0LjQxNCAxMEgxMHYzLjQxNkw4LjQxNCAxNUgxLjU4NkwwIDEzLjQxNnYtNS44M0wxLjU4NiA2SDZWMi41ODZMNy41ODYgMWg2LjgyOHoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik0yIDEzaDZWOEgydjV6bTEtNGg0djFIM1Y5em0wIDJoNHYxSDN2LTF6bTExLTVWM0g4djNoLjQxNEw5IDYuNTg2VjZoNHYxSDkuNDE0bC41ODYuNTg2VjhoNFY2em0tMS0xSDlWNGg0djF6IiBpZD0iaWNvbkZnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLW9yYW5nZSIgZD0iTTMgMTFoNC4wMDF2MUgzdi0xem0wLTFoNC4wMDFWOUgzdjF6bTYtMnY1bC0xIDFIMmwtMS0xVjhsMS0xaDZsMSAxek04IDhIMnY1aDZWOHptMS0ybDEgMWgzVjZIOXptMC0xaDRWNEg5djF6bTUtM0g4TDcgM3YzaDFWM2g2djVoLTR2MWg0bDEtMVYzbC0xLTF6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum-member:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtZmd7ZmlsbDojZjBlZmYxfS5pY29uLXZzLWFjdGlvbi1ibHVle2ZpbGw6IzAwNTM5Y308L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0wIDE1VjZoNlYyLjU4Nkw3LjU4NSAxaDYuODI5TDE2IDIuNTg2djUuODI5TDE0LjQxNCAxMEgxMHY1SDB6bTMtNnoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik04IDN2M2g1djFoLTN2MWg0VjNIOHptNSAySDlWNGg0djF6TTIgOHY1aDZWOEgyem01IDNIM3YtMWg0djF6IiBpZD0iaWNvbkZnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLWJsdWUiIGQ9Ik0xMCA2aDN2MWgtM1Y2ek05IDR2MWg0VjRIOXptNS0ySDhMNyAzdjNoMVYzaDZ2NWgtNHYxaDRsMS0xVjNsLTEtMXptLTcgOEgzdjFoNHYtMXptMi0zdjdIMVY3aDh6TTggOEgydjVoNlY4eiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.keyword:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfS5pY29uLXZzLWZne2ZpbGw6I2YwZWZmMX08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNiA1VjJIOVYxSDB2MTRoMTN2LTNoM1Y5aC0xVjZIOVY1aDd6bS04IDdWOWgxdjNIOHoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik0yIDNoNXYxSDJWM3oiIGlkPSJpY29uRmciLz48cGF0aCBjbGFzcz0iaWNvbi12cy1iZyIgZD0iTTE1IDRoLTVWM2g1djF6bS0xIDNoLTJ2MWgyVjd6bS00IDBIMXYxaDlWN3ptMiA2SDF2MWgxMXYtMXptLTUtM0gxdjFoNnYtMXptOCAwaC01djFoNXYtMXpNOCAydjNIMVYyaDd6TTcgM0gydjFoNVYzeiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.text:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfS5pY29uLXZzLWZne2ZpbGw6I2YwZWZmMX08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNiAxNUgwVjFoMTZ2MTR6IiBpZD0ib3V0bGluZSIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWZnIiBkPSJNOS4yMjkgNy4zNTRjLjAzNS4xNDYuMDUyLjMxLjA1Mi40OTQgMCAuMjM0LS4wMi40NDEtLjA2LjYyMS0uMDM5LjE4LS4wOTUuMzI4LS4xNjguNDQ1YS42ODcuNjg3IDAgMCAxLS45MTQuMjgxLjc2Ljc2IDAgMCAxLS4yMzctLjIwNy45ODguOTg4IDAgMCAxLS4xNTQtLjMwNiAxLjI2MiAxLjI2MiAwIDAgMS0uMDU3LS4zODF2LS41MDZjMC0uMTcuMDItLjMyNi4wNjEtLjQ2NXMuMDk2LS4yNTguMTY4LS4zNTlhLjc1Ni43NTYgMCAwIDEgLjI1Ny0uMjMyYy4xLS4wNTUuMjEtLjA4Mi4zMzEtLjA4MmEuNjQ2LjY0NiAwIDAgMSAuNTcxLjMyYy4wNjcuMTA1LjExNi4yMy4xNS4zNzd6bS01LjEyNi44NjlhLjU1Ny41NTcgMCAwIDAtLjE5Ni4xMzJjLS4wNDcuMDUzLS4wOC4xMTItLjA5Ny4xOHMtLjAyOC4xNDctLjAyOC4yMzNhLjUxMy41MTMgMCAwIDAgLjE1Ny4zOS41MjguNTI4IDAgMCAwIC4xODYuMTEzLjY4Mi42ODIgMCAwIDAgLjI0Mi4wNDEuNzYuNzYgMCAwIDAgLjU5My0uMjcxLjg5Ny44OTcgMCAwIDAgLjE2NS0uMjk1Yy4wMzgtLjExMy4wNTktLjIzNC4wNTktLjM2NXYtLjM0NmwtLjc2MS4xMWExLjI5IDEuMjkgMCAwIDAtLjMyLjA3OHpNMTQgM3YxMEgyVjNoMTJ6TTUuOTYyIDcuNDY5YzAtLjIzOC0uMDI3LS40NTEtLjA4My0uNjM3YTEuMjg2IDEuMjg2IDAgMCAwLS4yNDktLjQ3MSAxLjA4IDEuMDggMCAwIDAtLjQyNC0uMjk1IDEuNjQ0IDEuNjQ0IDAgMCAwLS42MDgtLjEwMWMtLjExOSAwLS4yNDEuMDEyLS4zNjguMDMzYTMuMjEzIDMuMjEzIDAgMCAwLS42NzMuMTk1IDEuMzEzIDEuMzEzIDAgMCAwLS4yMTIuMTE0di43NjhjLjE1OC0uMTMyLjM0MS0uMjM1LjU0NC0uMzEzLjIwNC0uMDc4LjQxMy0uMTE3LjYyNy0uMTE3LjIxMyAwIC4zNzcuMDYzLjQ5NC4xODYuMTE2LjEyNS4xNzQuMzI0LjE3NC42bC0xLjAzLjE1NGMtLjIwNS4wMjYtLjM4LjA3Ny0uNTI2LjE1MWExLjA4MyAxLjA4MyAwIDAgMC0uNTYzLjY2QTEuNTYyIDEuNTYyIDAgMCAwIDMgOC44NTdjMCAuMTcuMDI1LjMyMy4wNzQuNDYzYS45NDUuOTQ1IDAgMCAwIC41NjguNTk2Yy4xMzkuMDU3LjI5Ny4wODQuNDc4LjA4NC4yMjkgMCAuNDMxLS4wNTMuNjA0LS4xNmExLjMgMS4zIDAgMCAwIC40MzktLjQ2M2guMDE0di41MjloLjc4NVY3LjQ2OXpNMTAgNy44NjFhMy41NCAzLjU0IDAgMCAwLS4wNzQtLjczNCAyLjA0NyAyLjA0NyAwIDAgMC0uMjI4LS42MTEgMS4yMDMgMS4yMDMgMCAwIDAtLjM5NC0uNDE2IDEuMDMgMS4wMyAwIDAgMC0uNTc0LS4xNTNjLS4xMjMgMC0uMjM0LjAxOC0uMzM2LjA1MWExIDEgMCAwIDAtLjI3OC4xNDcgMS4xNTMgMS4xNTMgMCAwIDAtLjIyNS4yMjIgMi4wMjIgMi4wMjIgMCAwIDAtLjE4MS4yODloLS4wMTNWNUg3djQuODg3aC42OTd2LS40ODVoLjAxM2MuMDQ0LjA4Mi4wOTUuMTU4LjE1MS4yMjkuMDU3LjA3LjExOS4xMzMuMTkxLjE4NmEuODM1LjgzNSAwIDAgMCAuMjM4LjEyMS45NDMuOTQzIDAgMCAwIC4yOTMuMDQyYy4yMyAwIC40MzQtLjA1My42MDktLjE2YTEuMzQgMS4zNCAwIDAgMCAuNDQzLS40NDNjLjEyLS4xODguMjExLS40MTIuMjcyLS42NzJBMy42MiAzLjYyIDAgMCAwIDEwIDcuODYxem0zLTEuNjU4YS43LjcgMCAwIDAtLjEwNi0uMDY2IDEuMTgzIDEuMTgzIDAgMCAwLS4xNDItLjA2MyAxLjIzMyAxLjIzMyAwIDAgMC0uMzYzLS4wNjVjLS4yMDkgMC0uMzk5LjA1MS0uNTY5LjE1YTEuMzU1IDEuMzU1IDAgMCAwLS40MzMuNDI0Yy0uMTE4LjE4Mi0uMjEuNDAyLS4yNzMuNjZhMy42MyAzLjYzIDAgMCAwLS4wMDggMS42MTVjLjA2LjIzLjE0My40My4yNTIuNjAyLjEwOS4xNjguMjQxLjMwMy4zOTYuMzk2YS45NzIuOTcyIDAgMCAwIC41MjQuMTQ0Yy4xNTggMCAuMjk2LS4wMjEuNDEzLS4wNjguMTE3LS4wNDUuMjE5LS4xMDguMzA5LS4xODR2LS43N2ExLjA5NCAxLjA5NCAwIDAgMS0uMjg4LjIyNS44MTkuODE5IDAgMCAxLS4xNTguMDY4LjQ4LjQ4IDAgMCAxLS4xNTMuMDI3LjYyLjYyIDAgMCAxLS4yNzQtLjA3NGMtLjI0MS0uMTM2LS40MjMtLjQ3OS0uNDIzLTEuMTQ2IDAtLjcxNS4yMDYtMS4xMi40NjktMS4zMDEuMDc3LS4wMzIuMTUzLS4wNjQuMjM4LS4wNjQuMTEzIDAgLjIyLjAyNy4zMTcuMDgyLjA5Ni4wNTcuMTg4LjEzMS4yNzIuMjIzdi0uODE1eiIgaWQ9Imljb25GZyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNMSAydjEyaDE0VjJIMXptMTMgMTFIMlYzaDEydjEwek01LjYzIDYuMzYxYTEuMDggMS4wOCAwIDAgMC0uNDI0LS4yOTUgMS42NDQgMS42NDQgMCAwIDAtLjYwOC0uMTAxYy0uMTE5IDAtLjI0MS4wMTItLjM2OC4wMzNhMy4yMTMgMy4yMTMgMCAwIDAtLjY3My4xOTUgMS4zMTMgMS4zMTMgMCAwIDAtLjIxMi4xMTR2Ljc2OGMuMTU4LS4xMzIuMzQxLS4yMzUuNTQ0LS4zMTMuMjA0LS4wNzguNDEzLS4xMTcuNjI3LS4xMTcuMjEzIDAgLjM3Ny4wNjMuNDk0LjE4Ni4xMTYuMTI1LjE3NC4zMjQuMTc0LjZsLTEuMDMuMTU0Yy0uMjA1LjAyNi0uMzguMDc3LS41MjYuMTUxYTEuMDgzIDEuMDgzIDAgMCAwLS41NjMuNjZBMS41NjIgMS41NjIgMCAwIDAgMyA4Ljg1N2MwIC4xNy4wMjUuMzIzLjA3NC40NjNhLjk0NS45NDUgMCAwIDAgLjU2OC41OTZjLjEzOS4wNTcuMjk3LjA4NC40NzguMDg0LjIyOSAwIC40MzEtLjA1My42MDQtLjE2YTEuMyAxLjMgMCAwIDAgLjQzOS0uNDYzaC4wMTR2LjUyOWguNzg1VjcuNDY5YzAtLjIzOC0uMDI3LS40NTEtLjA4My0uNjM3YTEuMjg2IDEuMjg2IDAgMCAwLS4yNDktLjQ3MXptLS40NDYgMi4wMmMwIC4xMzEtLjAyLjI1Mi0uMDU5LjM2NWEuODk3Ljg5NyAwIDAgMS0uMTY1LjI5NS43NTguNzU4IDAgMCAxLS41OTMuMjcyLjY4Mi42ODIgMCAwIDEtLjI0Mi0uMDQxLjUwNy41MDcgMCAwIDEtLjMwMi0uMjg2LjU4My41ODMgMCAwIDEtLjA0MS0uMjE4YzAtLjA4Ni4wMS0uMTY0LjAyNy0uMjMycy4wNTEtLjEyNy4wOTgtLjE4YS41NDYuNTQ2IDAgMCAxIC4xOTYtLjEzM2MuMDgzLS4wMzMuMTg5LS4wNjEuMzItLjA3OGwuNzYxLS4xMDl2LjM0NXptNC41MTQtMS44NjVhMS4yMDMgMS4yMDMgMCAwIDAtLjM5NC0uNDE2IDEuMDMgMS4wMyAwIDAgMC0uNTc0LS4xNTNjLS4xMjMgMC0uMjM0LjAxOC0uMzM2LjA1MWExIDEgMCAwIDAtLjI3OC4xNDcgMS4xNTMgMS4xNTMgMCAwIDAtLjIyNS4yMjIgMi4wMjIgMi4wMjIgMCAwIDAtLjE4MS4yODloLS4wMTNWNUg3djQuODg3aC42OTd2LS40ODVoLjAxM2MuMDQ0LjA4Mi4wOTUuMTU4LjE1MS4yMjkuMDU3LjA3LjExOS4xMzMuMTkxLjE4NmEuODM1LjgzNSAwIDAgMCAuMjM4LjEyMS45NDMuOTQzIDAgMCAwIC4yOTMuMDQyYy4yMyAwIC40MzQtLjA1My42MDktLjE2YTEuMzQgMS4zNCAwIDAgMCAuNDQzLS40NDNjLjEyLS4xODguMjExLS40MTIuMjcyLS42NzJBMy42MiAzLjYyIDAgMCAwIDEwIDcuODYxYTMuNTQgMy41NCAwIDAgMC0uMDc0LS43MzQgMi4wNDcgMi4wNDcgMCAwIDAtLjIyOC0uNjExem0tLjQ3NiAxLjk1M2MtLjAzOS4xOC0uMDk1LjMyOC0uMTY4LjQ0NWEuNzU1Ljc1NSAwIDAgMS0uMjY0LjI2Ni42ODcuNjg3IDAgMCAxLS42NTEuMDE1Ljc2Ljc2IDAgMCAxLS4yMzctLjIwNy45ODguOTg4IDAgMCAxLS4xNTQtLjMwNiAxLjI2MiAxLjI2MiAwIDAgMS0uMDU3LS4zODF2LS41MDZjMC0uMTcuMDItLjMyNi4wNjEtLjQ2NXMuMDk2LS4yNTguMTY4LS4zNTlhLjc1Ni43NTYgMCAwIDEgLjI1Ny0uMjMyYy4xLS4wNTUuMjEtLjA4Mi4zMzEtLjA4MmEuNjQ2LjY0NiAwIDAgMSAuNTcxLjMyYy4wNjYuMTA1LjExNi4yMy4xNS4zNzcuMDM1LjE0Ni4wNTIuMzEuMDUyLjQ5NCAwIC4yMzQtLjAxOS40NDEtLjA1OS42MjF6bTMuNjcyLTIuMzMyYS43LjcgMCAwIDEgLjEwNi4wNjZ2LjgxNGExLjE3OCAxLjE3OCAwIDAgMC0uMjczLS4yMjMuNjQ1LjY0NSAwIDAgMC0uMzE3LS4wODFjLS4wODUgMC0uMTYxLjAzMi0uMjM4LjA2NC0uMjYzLjE4MS0uNDY5LjU4Ni0uNDY5IDEuMzAxIDAgLjY2OC4xODIgMS4wMTEuNDIzIDEuMTQ2LjA4NC4wNC4xNzEuMDc0LjI3NC4wNzQuMDQ5IDAgLjEwMS0uMDEuMTUzLS4wMjdhLjg1Ni44NTYgMCAwIDAgLjE1OC0uMDY4IDEuMTYgMS4xNiAwIDAgMCAuMjg4LS4yMjV2Ljc3Yy0uMDkuMDc2LS4xOTIuMTM5LS4zMDkuMTg0YTEuMDk4IDEuMDk4IDAgMCAxLS40MTIuMDY4Ljk3NC45NzQgMCAwIDEtLjUyMy0uMTQzIDEuMjU3IDEuMjU3IDAgMCAxLS4zOTYtLjM5NiAyLjA5OCAyLjA5OCAwIDAgMS0uMjUyLS42MDIgMy4xMTggMy4xMTggMCAwIDEtLjA4OC0uNzU0YzAtLjMxNi4wMzItLjYwNC4wOTYtLjg2MS4wNjMtLjI1OC4xNTUtLjQ3OS4yNzMtLjY2LjExOS0uMTgyLjI2NS0uMzIyLjQzMy0uNDI0YTEuMTAyIDEuMTAyIDAgMCAxIDEuMDczLS4wMjN6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.color:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfS5pY29uLXZzLXJlZHtmaWxsOiNlNTE0MDB9Lmljb24tdnMteWVsbG93e2ZpbGw6I2ZmY2MwMH0uaWNvbi12cy1ncmVlbntmaWxsOiMzMzk5MzN9Lmljb24tdnMtYmx1ZXtmaWxsOiMxYmExZTJ9Lmljb24tdnMtYWN0aW9uLXB1cnBsZXtmaWxsOiM2NTJkOTB9Lmljb24td2hpdGV7ZmlsbDojZmZmZmZmfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE2IDhjMCA0LjQxMS0zLjU4OSA4LTggOGEyLjgwMyAyLjgwMyAwIDAgMS0yLjgtMi44YzAtLjgzMy4yNzItMS42MjkuNzY2LTIuMjQxYS41OTYuNTk2IDAgMCAwIC4xMDEtLjM1OS42NjcuNjY3IDAgMCAwLS42NjctLjY2Ni41OC41OCAwIDAgMC0uMzU4LjEwMkEzLjU4NCAzLjU4NCAwIDAgMSAyLjggMTAuOCAyLjgwMyAyLjgwMyAwIDAgMSAwIDhjMC00LjQxMSAzLjU4OS04IDgtOHM4IDMuNTg5IDggOHoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24td2hpdGUiIGQ9Ik01LjQgNy45MzNhMi42NyAyLjY3IDAgMCAxIDIuNjY3IDIuNjY2YzAgLjYwNi0uMTkzIDEuMTc5LS41NDQgMS42MTRhMS41OTkgMS41OTkgMCAwIDAtLjMyMy45ODcuOC44IDAgMCAwIC44LjhjMy4zMDkgMCA2LTIuNjkxIDYtNnMtMi42OTEtNi02LTYtNiAyLjY5MS02IDZjMCAuNDQxLjM1OS44LjguOC4zNzggMCAuNzI5LS4xMTQuOTg2LS4zMjJBMi41NjggMi41NjggMCAwIDEgNS40IDcuOTMzeiIgaWQ9Imljb25GZyIvPjxnIGlkPSJpY29uQmciPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNOCAxNWMtLjk5MiAwLTEuOC0uODA4LTEuOC0xLjggMC0uNjA2LjE5My0xLjE3OS41NDQtMS42MTMuMjA4LS4yNTkuMzIzLS42MDkuMzIzLS45ODcgMC0uOTE5LS43NDgtMS42NjYtMS42NjctMS42NjYtLjM3NyAwLS43MjguMTE1LS45ODYuMzIzQTIuNTggMi41OCAwIDAgMSAyLjggOS44QzEuODA4IDkuOCAxIDguOTkyIDEgOGMwLTMuODYgMy4xNC03IDctNyAzLjg1OSAwIDcgMy4xNCA3IDcgMCAzLjg1OS0zLjE0MSA3LTcgN3pNNS40IDcuOTMzYTIuNjcgMi42NyAwIDAgMSAyLjY2NyAyLjY2NmMwIC42MDYtLjE5MyAxLjE3OS0uNTQ0IDEuNjE0YTEuNTk5IDEuNTk5IDAgMCAwLS4zMjMuOTg3LjguOCAwIDAgMCAuOC44YzMuMzA5IDAgNi0yLjY5MSA2LTZzLTIuNjkxLTYtNi02LTYgMi42OTEtNiA2YzAgLjQ0MS4zNTkuOC44LjguMzc4IDAgLjcyOS0uMTE0Ljk4Ni0uMzIyQTIuNTY4IDIuNTY4IDAgMCAxIDUuNCA3LjkzM3oiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1hY3Rpb24tcHVycGxlIiBkPSJNNC41IDUuMzc1YS44NzUuODc1IDAgMSAwIDAgMS43NS44NzUuODc1IDAgMCAwIDAtMS43NXoiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1ibHVlIiBkPSJNNy4xMjUgMy42MjVhLjg3NS44NzUgMCAxIDAgMCAxLjc1Ljg3NS44NzUgMCAwIDAgMC0xLjc1eiIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWdyZWVuIiBkPSJNMTAuNjI1IDQuNWEuODc1Ljg3NSAwIDEgMCAwIDEuNzUuODc1Ljg3NSAwIDAgMCAwLTEuNzV6Ii8+PHBhdGggY2xhc3M9Imljb24tdnMteWVsbG93IiBkPSJNMTEuNSA4YS44NzUuODc1IDAgMSAwIDAgMS43NS44NzUuODc1IDAgMCAwIDAtMS43NXoiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1yZWQiIGQ9Ik05Ljc1IDEwLjYyNWEuODc1Ljg3NSAwIDEgMCAwIDEuNzUuODc1Ljg3NSAwIDAgMCAwLTEuNzV6Ii8+PC9nPjwvc3ZnPg==)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.file:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfS5pY29uLXZzLWZne2ZpbGw6I2YwZWZmMX08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNSAxNkgyVjBoOC42MjFMMTUgNC4zNzlWMTZ6IiBpZD0ib3V0bGluZSIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWZnIiBkPSJNMTMgMTRINFYyaDV2NGg0djh6bS0zLTlWMi4yMDdMMTIuNzkzIDVIMTB6IiBpZD0iaWNvbkZnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYmciIGQ9Ik0zIDF2MTRoMTFWNC43OTNMMTAuMjA3IDFIM3ptMTAgMTNINFYyaDV2NGg0djh6bS0zLTlWMi4yMDdMMTIuNzkzIDVIMTB6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.reference:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojZjZmNmY2fS5pY29uLXZzLW91dHtmaWxsOiNmNmY2ZjZ9Lmljb24tdnMtYmd7ZmlsbDojNDI0MjQyfS5pY29uLXZzLWZne2ZpbGw6I2YwZWZmMX0uaWNvbi12cy1hY3Rpb24tYmx1ZXtmaWxsOiMwMDUzOWN9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0iY2FudmFzIi8+PHBhdGggY2xhc3M9Imljb24tdnMtb3V0IiBkPSJNMTQgNC41NTZWMTNjMCAuOTctLjcwMSAyLTIgMkg0Yy0uOTcgMC0yLS43MDEtMi0yVjYuNjQ5QTMuNDk1IDMuNDk1IDAgMCAxIDAgMy41QzAgMS41NyAxLjU3IDAgMy41IDBINXYxaDUuMDYxTDE0IDQuNTU2eiIgaWQ9Im91dGxpbmUiIHN0eWxlPSJkaXNwbGF5OiBub25lOyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNMTMgNXY4cy0uMDM1IDEtMS4wMzUgMWgtOFMzIDE0IDMgMTNWOWgxdjRoOFY2SDkuMzk3bC41MTctLjUyTDkgNC41NzJWM0g3LjQxOUw2LjQxMyAyaDMuMjI4TDEzIDV6IiBpZD0iaWNvbkJnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik03LjQxOSAzSDl2MS41NzJMNy40MTkgM3ptMS45NzggM0w2LjQxNiA5SDR2NGg4VjZIOS4zOTd6IiBpZD0iaWNvbkZnIiBzdHlsZT0iZGlzcGxheTogbm9uZTsiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1hY3Rpb24tYmx1ZSIgZD0iTTUuOTg4IDZIMy41YTIuNSAyLjUgMCAxIDEgMC01SDR2MWgtLjVDMi42NzMgMiAyIDIuNjczIDIgMy41UzIuNjczIDUgMy41IDVoMi41MTNMNCAzaDJsMi41IDIuNDg0TDYgOEg0bDEuOTg4LTJ6IiBpZD0iY29sb3JBY3Rpb24iLz48L3N2Zz4=)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.snippet:before{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnNDY5NCIKICAgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE0NzA1Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZGVmcwogICAgIGlkPSJkZWZzNDcwMyIgLz4KICA8c3R5bGUKICAgICBpZD0ic3R5bGU0Njk2Ij4uaWNvbi1jYW52YXMtdHJhbnNwYXJlbnR7b3BhY2l0eTowO2ZpbGw6I2Y2ZjZmNn0uaWNvbi12cy1vdXR7ZmlsbDojZjZmNmY2fS5pY29uLXZzLWFjdGlvbi1vcmFuZ2V7ZmlsbDojYzI3ZDFhfTwvc3R5bGU+CiAgPGcKICAgICBpZD0iZzQ3MDciCiAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMS4zMzMzMzMzLDAsMCwxLjMzMzMzMzMsLTI0NS45OTk5OSwtNS4zMzMzMzMpIj4KICAgIDxwYXRoCiAgICAgICBkPSJtIDE4NSw0IDExLDAgMCwxMiAtMTEsMCB6IgogICAgICAgaWQ9InBhdGg0NTM0IgogICAgICAgc3R5bGU9ImZpbGw6I2Y2ZjZmNiIgLz4KICAgIDxwYXRoCiAgICAgICBkPSJtIDE5NCwxMyAwLC03IC03LDAgMCw3IC0xLDAgMCwtOCA5LDAgMCw4IC0xLDAgeiBtIC03LDIgLTEsMCAwLC0xIDEsMCAwLDEgeiBtIDIsLTEgLTEsMCAwLDEgMSwwIDAsLTEgeiBtIDIsMCAtMSwwIDAsMSAxLDAgMCwtMSB6IG0gMiwxIC0xLDAgMCwtMSAxLDAgMCwxIHogbSAyLC0xIC0xLDAgMCwxIDEsMCAwLC0xIHoiCiAgICAgICBpZD0icGF0aDQ1MzYiCiAgICAgICBzdHlsZT0iZmlsbDojNDI0MjQyIiAvPgogICAgPHBhdGgKICAgICAgIGQ9Im0gMTg3LDEzIDAsLTcgNywwIDAsNyAtNywwIHoiCiAgICAgICBpZD0icGF0aDQ1MzgiCiAgICAgICBzdHlsZT0iZmlsbDojZjBlZmYxIiAvPgogIDwvZz4KICA8cGF0aAogICAgIGlkPSJjYW52YXMiCiAgICAgZD0iTTE2IDE2SDBWMGgxNnYxNnoiCiAgICAgY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiAvPgo8L3N2Zz4K)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.customcolor:before{background-image:none}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.folder:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uaWNvbi1jYW52YXMtdHJhbnNwYXJlbnR7b3BhY2l0eTowO2ZpbGw6I0Y2RjZGNjt9IC5pY29uLXZzLW91dHtvcGFjaXR5OjA7ZmlsbDojRjZGNkY2O30gLmljb24tdnMtZmd7ZmlsbDojRjBFRkYxO30gLmljb24tZm9sZGVye2ZpbGw6IzY1NjU2NTt9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNmgtMTZ2LTE2aDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE2IDIuNXYxMGMwIC44MjctLjY3MyAxLjUtMS41IDEuNWgtMTEuOTk2Yy0uODI3IDAtMS41LS42NzMtMS41LTEuNXYtOGMwLS44MjcuNjczLTEuNSAxLjUtMS41aDIuODg2bDEtMmg4LjExYy44MjcgMCAxLjUuNjczIDEuNSAxLjV6IiBpZD0ib3V0bGluZSIvPjxwYXRoIGNsYXNzPSJpY29uLWZvbGRlciIgZD0iTTE0LjUgMmgtNy40OTJsLTEgMmgtMy41MDRjLS4yNzcgMC0uNS4yMjQtLjUuNXY4YzAgLjI3Ni4yMjMuNS41LjVoMTEuOTk2Yy4yNzUgMCAuNS0uMjI0LjUtLjV2LTEwYzAtLjI3Ni0uMjI1LS41LS41LS41em0tLjQ5NiAyaC02LjQ5NmwuNS0xaDUuOTk2djF6IiBpZD0iaWNvbkJnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik0xNCAzdjFoLTYuNWwuNS0xaDZ6IiBpZD0iaWNvbkZnIi8+PC9zdmc+)}.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.customcolor .colorspan{margin:0 0 0 .3em;border:.1em solid #000;width:.7em;height:.7em;display:inline-block}.monaco-editor .suggest-widget .details{display:flex;flex-direction:column;cursor:default}.monaco-editor .suggest-widget .details.no-docs{display:none}.monaco-editor .suggest-widget.docs-below .details{border-top-width:0}.monaco-editor .suggest-widget .details>.monaco-scrollable-element{flex:1}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body{position:absolute;box-sizing:border-box;height:100%;width:100%}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.type{flex:2;overflow:hidden;text-overflow:ellipsis;opacity:.7;word-break:break-all;margin:0;padding:4px 0 12px 5px}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs{margin:0;padding:4px 5px;white-space:pre-wrap}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs{padding:0;white-space:normal}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>div,.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>span:not(:empty){padding:4px 5px}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:first-child{margin-top:0}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:last-child{margin-bottom:0}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs .code{white-space:pre-wrap;word-wrap:break-word}.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>p:empty{display:none}.monaco-editor .suggest-widget .details code{border-radius:3px;padding:0 .4em}.monaco-editor.hc-black .suggest-widget .details>.monaco-scrollable-element>.body>.header>.close,.monaco-editor.vs-dark .suggest-widget .details>.monaco-scrollable-element>.body>.header>.close{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMyAzIDE2IDE2IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDMgMyAxNiAxNiI+PHBvbHlnb24gZmlsbD0iI2U4ZThlOCIgcG9pbnRzPSIxMi41OTcsMTEuMDQyIDE1LjQsMTMuODQ1IDEzLjg0NCwxNS40IDExLjA0MiwxMi41OTggOC4yMzksMTUuNCA2LjY4MywxMy44NDUgOS40ODUsMTEuMDQyIDYuNjgzLDguMjM5IDguMjM4LDYuNjgzIDExLjA0Miw5LjQ4NiAxMy44NDUsNi42ODMgMTUuNCw4LjIzOSIvPjwvc3ZnPg==)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE2IDEwYzAgMi4yMDUtMS43OTQgNC00IDQtMS44NTggMC0zLjQxMS0xLjI3OS0zLjg1OC0zaC0uOTc4bDIuMzE4IDRIMHYtMS43MDNsMi0zLjQwOFYwaDExdjYuMTQyYzEuNzIxLjQ0NyAzIDIgMyAzLjg1OHoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYmciIGQ9Ik0xMiAxdjQuNzVBNC4yNTUgNC4yNTUgMCAwIDAgNy43NSAxMGgtLjczMkw0LjI3NSA1LjI2OSAzIDcuNDQyVjFoOXpNNy43NDcgMTRMNC4yNjkgOCAuNzQ4IDE0aDYuOTk5ek0xNSAxMGEzIDMgMCAxIDEtNiAwIDMgMyAwIDAgMSA2IDB6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constructor:before,.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.function:before,.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.method:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constructor:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.function:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.method:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtZmd7ZmlsbDojMmIyODJlfS5pY29uLXZzLWFjdGlvbi1wdXJwbGV7ZmlsbDojYjE4MGQ3fTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE1IDMuMzQ5djguNDAzTDguOTc1IDE2SDguMDdMMSAxMS41ODJWMy4zMjdMNy41OTUgMGgxLjExOEwxNSAzLjM0OXoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik0xMi43MTUgNC4zOThMOC40ODcgNy4wMiAzLjU2NSA0LjI3Mmw0LjU3OC0yLjMwOSA0LjU3MiAyLjQzNXpNMyA1LjEwMmw1IDIuNzkydjUuNzA1bC01LTMuMTI1VjUuMTAyem02IDguNDM0VjcuODc4bDQtMi40OHY1LjMxN2wtNCAyLjgyMXoiIGlkPSJpY29uRmciLz48cGF0aCBjbGFzcz0iaWNvbi12cy1hY3Rpb24tcHVycGxlIiBkPSJNOC4xNTYuODM3TDIgMy45NDJ2Ny4wODVMOC41MTcgMTUuMSAxNCAxMS4yMzNWMy45NUw4LjE1Ni44Mzd6bTQuNTU5IDMuNTYxTDguNDg3IDcuMDIgMy41NjUgNC4yNzJsNC41NzgtMi4zMDkgNC41NzIgMi40MzV6TTMgNS4xMDJsNSAyLjc5MnY1LjcwNWwtNS0zLjEyNVY1LjEwMnptNiA4LjQzNFY3Ljg3OGw0LTIuNDh2NS4zMTdsLTQgMi44MjF6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.field:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.field:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtZmd7ZmlsbDojMmIyODJlfS5pY29uLXZzLWFjdGlvbi1ibHVle2ZpbGw6Izc1YmVmZn08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0wIDEwLjczNlY0LjVMOSAwbDcgMy41djYuMjM2bC05IDQuNS03LTMuNXoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLWJsdWUiIGQ9Ik05IDFMMSA1djVsNiAzIDgtNFY0TDkgMXpNNyA2Ljg4MkwzLjIzNiA1IDkgMi4xMTggMTIuNzY0IDQgNyA2Ljg4MnoiIGlkPSJpY29uQmciLz48cGF0aCBjbGFzcz0iaWNvbi12cy1mZyIgZD0iTTkgMi4xMThMMTIuNzY0IDQgNyA2Ljg4MiAzLjIzNiA1IDkgMi4xMTh6IiBpZD0iaWNvbkZnIi8+PC9zdmc+)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.event:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.event:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYWN0aW9uLW9yYW5nZXtmaWxsOiNlOGFiNTN9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0iY2FudmFzIi8+PHBhdGggY2xhc3M9Imljb24tdnMtb3V0IiBkPSJNMTQgMS40MTRMOS40MTQgNkgxNHYxLjQxNEw1LjQxNCAxNkgzdi0xLjIzNEw1LjM3MSAxMEgyVjguNzY0TDYuMzgyIDBIMTR2MS40MTR6IiBpZD0ib3V0bGluZSIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ii8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLW9yYW5nZSIgZD0iTTcgN2g2bC04IDhINGwyLjk4NS02SDNsNC04aDZMNyA3eiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.operator:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.operator:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtZmd7ZmlsbDojMmIyODJlfS5pY29uLXZzLWFjdGlvbi1ibHVle2ZpbGw6Izc1YmVmZn08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0ib3V0bGluZSIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ii8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLWJsdWUiIGQ9Ik0xIDF2MTRoMTRWMUgxem02IDEySDN2LTFoNHYxem0wLTNIM1Y5aDR2MXptMC01SDV2Mkg0VjVIMlY0aDJWMmgxdjJoMnYxem0zLjI4MSA4SDguNzE5bDMtNGgxLjU2M2wtMy4wMDEgNHpNMTQgNUg5VjRoNXYxeiIgaWQ9Imljb25CZyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWZnIiBkPSJNNyA1SDV2Mkg0VjVIMlY0aDJWMmgxdjJoMnYxem03LTFIOXYxaDVWNHpNNyA5SDN2MWg0Vjl6bTAgM0gzdjFoNHYtMXptMy4yODEgMWwzLTRoLTEuNTYzbC0zIDRoMS41NjN6IiBpZD0iaWNvbkZnIiBzdHlsZT0iZGlzcGxheTogbm9uZTsiLz48L3N2Zz4=)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.variable:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.variable:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fS5pY29uLXZzLWZne2ZpbGw6IzJiMjgyZX0uaWNvbi12cy1hY3Rpb24tYmx1ZXtmaWxsOiM3NWJlZmZ9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0iY2FudmFzIi8+PHBhdGggY2xhc3M9Imljb24tdnMtb3V0IiBkPSJNMTEgM3YxLjAxNUw4LjczMyAyLjg4MiA1IDQuNzQ5VjNIMHYxMGg1di0xLjg1OWwyLjE1NiAxLjA3N0wxMSAxMC4yOTVWMTNoNVYzaC01eiIgaWQ9Im91dGxpbmUiIHN0eWxlPSJkaXNwbGF5OiBub25lOyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNMiA1djZoMnYxSDFWNGgzdjFIMnptMTAgNnYxaDNWNGgtM3YxaDJ2NmgtMnoiIGlkPSJpY29uQmciLz48cGF0aCBjbGFzcz0iaWNvbi12cy1mZyIgZD0iTTcuMTU2IDcuMTU2bC0xLjU3OC0uNzg5IDMuMTU2LTEuNTc4IDEuNTc4Ljc4OS0zLjE1NiAxLjU3OHoiIGlkPSJpY29uRmciIHN0eWxlPSJkaXNwbGF5OiBub25lOyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWFjdGlvbi1ibHVlIiBkPSJNOC43MzMgNEw0IDYuMzY3djMuMTU2TDcuMTU2IDExLjFsNC43MzMtMi4zNjdWNS41NzhMOC43MzMgNHpNNy4xNTYgNy4xNTZsLTEuNTc4LS43ODkgMy4xNTYtMS41NzggMS41NzguNzg5LTMuMTU2IDEuNTc4eiIgaWQ9ImNvbG9ySW1wb3J0YW5jZSIvPjwvc3ZnPg==)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.class:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.class:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYWN0aW9uLW9yYW5nZXtmaWxsOiNlOGFiNTN9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0iY2FudmFzIi8+PHBhdGggY2xhc3M9Imljb24tdnMtb3V0IiBkPSJNMTYgNi41ODZsLTMtM0wxMS41ODYgNUg5LjQxNGwxLTEtNC00aC0uODI4TDAgNS41ODZ2LjgyOGw0IDRMNi40MTQgOEg3djVoMS41ODZsMyAzaC44MjhMMTYgMTIuNDE0di0uODI4TDEzLjkxNCA5LjUgMTYgNy40MTR2LS44Mjh6IiBpZD0ib3V0bGluZSIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWFjdGlvbi1vcmFuZ2UiIGQ9Ik0xMyAxMGwyIDItMyAzLTItMiAxLTFIOFY3SDZMNCA5IDEgNmw1LTUgMyAzLTIgMmg1bDEtMSAyIDItMyAzLTItMiAxLTFIOXY0bDIuOTk5LjAwMkwxMyAxMHoiIGlkPSJpY29uQmciLz48L3N2Zz4=)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.interface:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.interface:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtZmd7ZmlsbDojMmIyODJlfS5pY29uLXZzLWFjdGlvbi1ibHVle2ZpbGw6Izc1YmVmZn08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xMS41IDEyYy0xLjkxNSAwLTMuNjAyLTEuMjQxLTQuMjI4LTNoLTEuNDFhMy4xMSAzLjExIDAgMCAxLTIuNzM3IDEuNjI1QzEuNDAyIDEwLjYyNSAwIDkuMjIzIDAgNy41czEuNDAyLTMuMTI1IDMuMTI1LTMuMTI1YzEuMTY1IDAgMi4yMDEuNjM5IDIuNzM3IDEuNjI1aDEuNDFjLjYyNi0xLjc1OSAyLjMxMy0zIDQuMjI4LTNDMTMuOTgxIDMgMTYgNS4wMTkgMTYgNy41UzEzLjk4MSAxMiAxMS41IDEyeiIgaWQ9Im91dGxpbmUiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1mZyIgZD0iTTExLjUgOUExLjUwMSAxLjUwMSAwIDEgMSAxMyA3LjVjMCAuODI2LS42NzMgMS41LTEuNSAxLjV6IiBpZD0iaWNvbkZnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLWJsdWUiIGQ9Ik0xMS41IDRhMy40OSAzLjQ5IDAgMCAwLTMuNDUgM0g1LjE4NUEyLjEyMiAyLjEyMiAwIDAgMCAxIDcuNWEyLjEyMyAyLjEyMyAwIDEgMCA0LjE4NS41SDguMDVhMy40OSAzLjQ5IDAgMCAwIDMuNDUgMyAzLjUgMy41IDAgMSAwIDAtN3ptMCA1Yy0uODI3IDAtMS41LS42NzMtMS41LTEuNVMxMC42NzMgNiAxMS41IDZzMS41LjY3MyAxLjUgMS41UzEyLjMyNyA5IDExLjUgOXoiIGlkPSJpY29uQmciLz48L3N2Zz4=)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.struct:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.struct:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYWN0aW9uLWJsdWV7ZmlsbDojNzViZWZmfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTkgMTRWOEg3djZIMVYyaDE0djEySDl6IiBpZD0ib3V0bGluZSIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ii8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLWJsdWUiIGQ9Ik0xMCA5aDR2NGgtNFY5em0tOCA0aDRWOUgydjR6TTIgM3Y0aDEyVjNIMnoiIGlkPSJpY29uQmciLz48L3N2Zz4=)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.type-parameter:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.type-parameter:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTEwLjcwMiAxMC41bDItMi0yLTIgLjUtLjVIMTB2NWgxdjNINXYtM2gxVjZINC43OThsLjUuNS0yIDIgMiAyTDMgMTIuNzk3bC0zLTNWNy4yMDFsMy0zVjJoMTB2Mi4yMDFsMyAzdjIuNTk2bC0zIDMtMi4yOTgtMi4yOTd6IiBpZD0ib3V0bGluZSIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ii8+PHBhdGggY2xhc3M9Imljb24tdnMtYmciIGQ9Ik00IDNoOHYyaC0xdi0uNWMwLS4yNzctLjIyNC0uNS0uNS0uNUg5djcuNWMwIC4yNzUuMjI0LjUuNS41aC41djFINnYtMWguNWEuNS41IDAgMCAwIC41LS41VjRINS41YS41LjUgMCAwIDAtLjUuNVY1SDRWM3pNMyA1LjYxNUwuMTE2IDguNSAzIDExLjM4M2wuODg0LS44ODMtMi0yIDItMkwzIDUuNjE1em0xMCAwbC0uODg0Ljg4NSAyIDItMiAyIC44ODQuODgzTDE1Ljg4NCA4LjUgMTMgNS42MTV6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.module:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.module:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTkuMjYgMTEuOTg0bC45NzgtLjAyMWEuOTYyLjk2MiAwIDAgMCAuMDktLjAwNmMuMDExLS4wNjMuMDI2LS4xNzkuMDI2LS4zNjFWOS42ODhjMC0uNjc5LjE4NS0xLjI1Ny41My0xLjcwNy0uMzQ2LS40NTItLjUzLTEuMDMtLjUzLTEuNzA1VjQuMzVjMC0uMTY3LS4wMjEtLjI1OS0uMDM0LS4zMDJMOS4yNiA0LjAyVi45NzNsMS4wMTEuMDExYzIuMTY3LjAyNCAzLjQwOSAxLjE1NiAzLjQwOSAzLjEwNXYxLjk2MmMwIC4zNTEuMDcxLjQ2MS4wNzIuNDYybC45MzYuMDYuMDUzLjkyN3YxLjkzNmwtLjkzNi4wNjFjLS4wNzYuMDE2LS4xMjUuMTQ2LS4xMjUuNDI0djIuMDE3YzAgLjkxNC0uMzMyIDMuMDQzLTMuNDA4IDMuMDc4bC0xLjAxMi4wMTF2LTMuMDQzem0tMy41MjEgMy4wMzJjLTMuMDg5LS4wMzUtMy40MjItMi4xNjQtMy40MjItMy4wNzhWOS45MjFjMC0uMzI3LS4wNjYtLjQzMi0uMDY3LS40MzNsLS45MzctLjA2LS4wNjMtLjkyOVY2LjU2M2wuOTQyLS4wNmMuMDU4IDAgLjEyNS0uMTE0LjEyNS0uNDUyVjQuMDljMC0xLjk0OSAxLjI0OC0zLjA4MSAzLjQyMi0zLjEwNUw2Ljc1Ljk3M1Y0LjAybC0uOTc1LjAyM2EuNTcyLjU3MiAwIDAgMC0uMDkzLjAxYy4wMDYuMDIxLS4wMTkuMTE1LS4wMTkuMjk3djEuOTI4YzAgLjY3NS0uMTg2IDEuMjUzLS41MzQgMS43MDUuMzQ4LjQ1LjUzNCAxLjAyOC41MzQgMS43MDd2MS45MDdjMCAuMTc1LjAxNC4yOTEuMDI3LjM2My4wMjMuMDAyIDEuMDYuMDI1IDEuMDYuMDI1djMuMDQzbC0xLjAxMS0uMDEyeiIgaWQ9Im91dGxpbmUiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1iZyIgZD0iTTUuNzUgMTQuMDE2Yy0xLjYyMy0uMDE5LTIuNDM0LS43MTEtMi40MzQtMi4wNzhWOS45MjFjMC0uOTAyLS4zNTUtMS4zNzYtMS4wNjYtMS40MjJ2LS45OThjLjcxMS0uMDQ1IDEuMDY2LS41MjkgMS4wNjYtMS40NDlWNC4wOWMwLTEuMzg1LjgxMS0yLjA4NyAyLjQzNC0yLjEwNXYxLjA2Yy0uNzI1LjAxNy0xLjA4Ny40NTMtMS4wODcgMS4zMDV2MS45MjhjMCAuOTItLjQ1NCAxLjQ4OC0xLjM2IDEuNzAyVjhjLjkwNy4yMDEgMS4zNi43NjMgMS4zNiAxLjY4OHYxLjkwN2MwIC40ODguMDgxLjgzNS4yNDMgMS4wNDIuMTYyLjIwOC40NDMuMzE2Ljg0NC4zMjV2MS4wNTR6bTcuOTktNS41MTdjLS43MDYuMDQ1LTEuMDYuNTItMS4wNiAxLjQyMnYyLjAxN2MwIDEuMzY3LS44MDcgMi4wNi0yLjQyIDIuMDc4di0xLjA1M2MuMzk2LS4wMDkuNjc4LS4xMTguODQ0LS4zMjguMTY3LS4yMS4yNS0uNTU2LjI1LTEuMDM5VjkuNjg4YzAtLjkyNS40NDktMS40ODggMS4zNDctMS42ODh2LS4wMjFjLS44OTgtLjIxNC0xLjM0Ny0uNzgyLTEuMzQ3LTEuNzAyVjQuMzVjMC0uODUyLS4zNjQtMS4yODgtMS4wOTQtMS4zMDZ2LTEuMDZjMS42MTMuMDE4IDIuNDIuNzIgMi40MiAyLjEwNXYxLjk2MmMwIC45Mi4zNTQgMS40MDQgMS4wNiAxLjQ0OXYuOTk5eiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.property:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.property:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE2IDUuNWE1LjUgNS41IDAgMCAxLTUuNSA1LjVjLS4yNzUgMC0uNTQzLS4wMjctLjgwNy0uMDY2bC0uMDc5LS4wMTJhNS40MjkgNS40MjkgMCAwIDEtLjgxLS4xOTJsLTQuNTM3IDQuNTM3Yy0uNDcyLjQ3My0xLjEuNzMzLTEuNzY3LjczM3MtMS4yOTUtLjI2LTEuNzY4LS43MzJhMi41MDIgMi41MDIgMCAwIDEgMC0zLjUzNWw0LjUzNy00LjUzN2E1LjQ1MiA1LjQ1MiAwIDAgMS0uMTkxLS44MTJjLS4wMDUtLjAyNS0uMDA4LS4wNTEtLjAxMi0uMDc3QTUuNTAzIDUuNTAzIDAgMCAxIDUgNS41YTUuNSA1LjUgMCAxIDEgMTEgMHoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYmciIGQ9Ik0xNSA1LjVhNC41IDQuNSAwIDAgMS00LjUgNC41Yy0uNjkzIDAtMS4zNDItLjE3LTEuOTI5LS40NWwtNS4wMSA1LjAxYy0uMjkzLjI5NC0uNjc3LjQ0LTEuMDYxLjQ0cy0uNzY4LS4xNDYtMS4wNjEtLjQzOWExLjUgMS41IDAgMCAxIDAtMi4xMjFsNS4wMS01LjAxQTQuNDgzIDQuNDgzIDAgMCAxIDYgNS41IDQuNSA0LjUgMCAwIDEgMTAuNSAxYy42OTMgMCAxLjM0Mi4xNyAxLjkyOS40NUw5LjYzNiA0LjI0M2wyLjEyMSAyLjEyMSAyLjc5My0yLjc5M2MuMjguNTg3LjQ1IDEuMjM2LjQ1IDEuOTI5eiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.unit:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.unit:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fS5pY29uLXZzLWZne2ZpbGw6IzJiMjgyZX08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNiAxMS4wMTNIMVY0aDE1djcuMDEzeiIgaWQ9Im91dGxpbmUiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1mZyIgZD0iTTggOUg3VjZoM3YzSDlWN0g4djJ6TTQgN2gxdjJoMVY2SDN2M2gxVjd6bTggMGgxdjJoMVY2aC0zdjNoMVY3eiIgaWQ9Imljb25GZyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNMiA1djVoMTNWNUgyem00IDRINVY3SDR2MkgzVjZoM3Yzem00IDBIOVY3SDh2Mkg3VjZoM3Yzem00IDBoLTFWN2gtMXYyaC0xVjZoM3YzeiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constant:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constant:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMjUyNTI2fS5pY29uLXZzLW91dHtmaWxsOiMyNTI1MjZ9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fS5pY29uLXZzLWZne2ZpbGw6IzJiMjgyZX0uaWNvbi12cy1hY3Rpb24tYmx1ZXtmaWxsOiM3NWJlZmZ9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0iY2FudmFzIi8+PHBhdGggY2xhc3M9Imljb24tdnMtb3V0IiBkPSJNMi44NzkgMTRMMSAxMi4xMjFWMy44NzlMMi44NzkgMmgxMC4yNDJMMTUgMy44Nzl2OC4yNDJMMTMuMTIxIDE0SDIuODc5eiIgaWQ9Im91dGxpbmUiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1mZyIgZD0iTTEyLjI5MyA0SDMuNzA3TDMgNC43MDd2Ni41ODZsLjcwNy43MDdoOC41ODZsLjcwNy0uNzA3VjQuNzA3TDEyLjI5MyA0ek0xMSAxMEg1VjloNnYxem0wLTNINVY2aDZ2MXoiIGlkPSJpY29uRmciLz48ZyBpZD0iaWNvbkJnIj48cGF0aCBjbGFzcz0iaWNvbi12cy1iZyIgZD0iTTEyLjcwNyAxM0gzLjI5M0wyIDExLjcwN1Y0LjI5M0wzLjI5MyAzaDkuNDE0TDE0IDQuMjkzdjcuNDE0TDEyLjcwNyAxM3ptLTktMWg4LjU4NmwuNzA3LS43MDdWNC43MDdMMTIuMjkzIDRIMy43MDdMMyA0LjcwN3Y2LjU4NmwuNzA3LjcwN3oiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1hY3Rpb24tYmx1ZSIgZD0iTTExIDdINVY2aDZ2MXptMCAySDV2MWg2Vjl6Ii8+PC9nPjwvc3ZnPg==)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum:before,.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.value:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.value:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtZmd7ZmlsbDojMmIyODJlfS5pY29uLXZzLWFjdGlvbi1vcmFuZ2V7ZmlsbDojZThhYjUzfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE0LjQxNCAxTDE2IDIuNTg2djUuODI4TDE0LjQxNCAxMEgxMHYzLjQxNkw4LjQxNCAxNUgxLjU4NkwwIDEzLjQxNnYtNS44M0wxLjU4NiA2SDZWMi41ODZMNy41ODYgMWg2LjgyOHoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik0yIDEzaDZWOEgydjV6bTEtNGg0djFIM1Y5em0wIDJoNHYxSDN2LTF6bTExLTVWM0g4djNoLjQxNEw5IDYuNTg2VjZoNHYxSDkuNDE0bC41ODYuNTg2VjhoNFY2em0tMS0xSDlWNGg0djF6IiBpZD0iaWNvbkZnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLW9yYW5nZSIgZD0iTTMgMTFoNC4wMDF2MUgzdi0xem0wLTFoNC4wMDFWOUgzdjF6bTYtMnY1bC0xIDFIMmwtMS0xVjhsMS0xaDZsMSAxek04IDhIMnY1aDZWOHptMS0ybDEgMWgzVjZIOXptMC0xaDRWNEg5djF6bTUtM0g4TDcgM3YzaDFWM2g2djVoLTR2MWg0bDEtMVYzbC0xLTF6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum-member:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum-member:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtZmd7ZmlsbDojMmIyODJlfS5pY29uLXZzLWFjdGlvbi1ibHVle2ZpbGw6Izc1YmVmZn08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0wIDE1VjZoNlYyLjU4Nkw3LjU4NSAxaDYuODI5TDE2IDIuNTg2djUuODI5TDE0LjQxNCAxMEgxMHY1SDB6bTMtNnoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik04IDN2M2g1djFoLTN2MWg0VjNIOHptNSAySDlWNGg0djF6TTIgOHY1aDZWOEgyem01IDNIM3YtMWg0djF6IiBpZD0iaWNvbkZnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYWN0aW9uLWJsdWUiIGQ9Ik0xMCA2aDN2MWgtM1Y2ek05IDR2MWg0VjRIOXptNS0ySDhMNyAzdjNoMVYzaDZ2NWgtNHYxaDRsMS0xVjNsLTEtMXptLTcgOEgzdjFoNHYtMXptMi0zdjdIMVY3aDh6TTggOEgydjVoNlY4eiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.keyword:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.keyword:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fS5pY29uLXZzLWZne2ZpbGw6IzJiMjgyZX08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNiA1VjJIOVYxSDB2MTRoMTN2LTNoM1Y5aC0xVjZIOVY1aDd6bS04IDdWOWgxdjNIOHoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik0yIDNoNXYxSDJWM3oiIGlkPSJpY29uRmciLz48cGF0aCBjbGFzcz0iaWNvbi12cy1iZyIgZD0iTTE1IDRoLTVWM2g1djF6bS0xIDNoLTJ2MWgyVjd6bS00IDBIMXYxaDlWN3ptMiA2SDF2MWgxMXYtMXptLTUtM0gxdjFoNnYtMXptOCAwaC01djFoNXYtMXpNOCAydjNIMVYyaDd6TTcgM0gydjFoNVYzeiIgaWQ9Imljb25CZyIvPjwvc3ZnPg==)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.text:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.text:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fS5pY29uLXZzLWZne2ZpbGw6IzJiMjgyZX08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNiAxNUgwVjFoMTZ2MTR6IiBpZD0ib3V0bGluZSIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWZnIiBkPSJNOS4yMjkgNy4zNTRjLjAzNS4xNDYuMDUyLjMxLjA1Mi40OTQgMCAuMjM0LS4wMi40NDEtLjA2LjYyMS0uMDM5LjE4LS4wOTUuMzI4LS4xNjguNDQ1YS42ODcuNjg3IDAgMCAxLS45MTQuMjgxLjc2Ljc2IDAgMCAxLS4yMzctLjIwNy45ODguOTg4IDAgMCAxLS4xNTQtLjMwNiAxLjI2MiAxLjI2MiAwIDAgMS0uMDU3LS4zODF2LS41MDZjMC0uMTcuMDItLjMyNi4wNjEtLjQ2NXMuMDk2LS4yNTguMTY4LS4zNTlhLjc1Ni43NTYgMCAwIDEgLjI1Ny0uMjMyYy4xLS4wNTUuMjEtLjA4Mi4zMzEtLjA4MmEuNjQ2LjY0NiAwIDAgMSAuNTcxLjMyYy4wNjcuMTA1LjExNi4yMy4xNS4zNzd6bS01LjEyNi44NjlhLjU1Ny41NTcgMCAwIDAtLjE5Ni4xMzJjLS4wNDcuMDUzLS4wOC4xMTItLjA5Ny4xOHMtLjAyOC4xNDctLjAyOC4yMzNhLjUxMy41MTMgMCAwIDAgLjE1Ny4zOS41MjguNTI4IDAgMCAwIC4xODYuMTEzLjY4Mi42ODIgMCAwIDAgLjI0Mi4wNDEuNzYuNzYgMCAwIDAgLjU5My0uMjcxLjg5Ny44OTcgMCAwIDAgLjE2NS0uMjk1Yy4wMzgtLjExMy4wNTktLjIzNC4wNTktLjM2NXYtLjM0NmwtLjc2MS4xMWExLjI5IDEuMjkgMCAwIDAtLjMyLjA3OHpNMTQgM3YxMEgyVjNoMTJ6TTUuOTYyIDcuNDY5YzAtLjIzOC0uMDI3LS40NTEtLjA4My0uNjM3YTEuMjg2IDEuMjg2IDAgMCAwLS4yNDktLjQ3MSAxLjA4IDEuMDggMCAwIDAtLjQyNC0uMjk1IDEuNjQ0IDEuNjQ0IDAgMCAwLS42MDgtLjEwMWMtLjExOSAwLS4yNDEuMDEyLS4zNjguMDMzYTMuMjEzIDMuMjEzIDAgMCAwLS42NzMuMTk1IDEuMzEzIDEuMzEzIDAgMCAwLS4yMTIuMTE0di43NjhjLjE1OC0uMTMyLjM0MS0uMjM1LjU0NC0uMzEzLjIwNC0uMDc4LjQxMy0uMTE3LjYyNy0uMTE3LjIxMyAwIC4zNzcuMDYzLjQ5NC4xODYuMTE2LjEyNS4xNzQuMzI0LjE3NC42bC0xLjAzLjE1NGMtLjIwNS4wMjYtLjM4LjA3Ny0uNTI2LjE1MWExLjA4MyAxLjA4MyAwIDAgMC0uNTYzLjY2QTEuNTYyIDEuNTYyIDAgMCAwIDMgOC44NTdjMCAuMTcuMDI1LjMyMy4wNzQuNDYzYS45NDUuOTQ1IDAgMCAwIC41NjguNTk2Yy4xMzkuMDU3LjI5Ny4wODQuNDc4LjA4NC4yMjkgMCAuNDMxLS4wNTMuNjA0LS4xNmExLjMgMS4zIDAgMCAwIC40MzktLjQ2M2guMDE0di41MjloLjc4NVY3LjQ2OXpNMTAgNy44NjFhMy41NCAzLjU0IDAgMCAwLS4wNzQtLjczNCAyLjA0NyAyLjA0NyAwIDAgMC0uMjI4LS42MTEgMS4yMDMgMS4yMDMgMCAwIDAtLjM5NC0uNDE2IDEuMDMgMS4wMyAwIDAgMC0uNTc0LS4xNTNjLS4xMjMgMC0uMjM0LjAxOC0uMzM2LjA1MWExIDEgMCAwIDAtLjI3OC4xNDcgMS4xNTMgMS4xNTMgMCAwIDAtLjIyNS4yMjIgMi4wMjIgMi4wMjIgMCAwIDAtLjE4MS4yODloLS4wMTNWNUg3djQuODg3aC42OTd2LS40ODVoLjAxM2MuMDQ0LjA4Mi4wOTUuMTU4LjE1MS4yMjkuMDU3LjA3LjExOS4xMzMuMTkxLjE4NmEuODM1LjgzNSAwIDAgMCAuMjM4LjEyMS45NDMuOTQzIDAgMCAwIC4yOTMuMDQyYy4yMyAwIC40MzQtLjA1My42MDktLjE2YTEuMzQgMS4zNCAwIDAgMCAuNDQzLS40NDNjLjEyLS4xODguMjExLS40MTIuMjcyLS42NzJBMy42MiAzLjYyIDAgMCAwIDEwIDcuODYxem0zLTEuNjU4YS43LjcgMCAwIDAtLjEwNi0uMDY2IDEuMTgzIDEuMTgzIDAgMCAwLS4xNDItLjA2MyAxLjIzMyAxLjIzMyAwIDAgMC0uMzYzLS4wNjVjLS4yMDkgMC0uMzk5LjA1MS0uNTY5LjE1YTEuMzU1IDEuMzU1IDAgMCAwLS40MzMuNDI0Yy0uMTE4LjE4Mi0uMjEuNDAyLS4yNzMuNjZhMy42MyAzLjYzIDAgMCAwLS4wMDggMS42MTVjLjA2LjIzLjE0My40My4yNTIuNjAyLjEwOS4xNjguMjQxLjMwMy4zOTYuMzk2YS45NzIuOTcyIDAgMCAwIC41MjQuMTQ0Yy4xNTggMCAuMjk2LS4wMjEuNDEzLS4wNjguMTE3LS4wNDUuMjE5LS4xMDguMzA5LS4xODR2LS43N2ExLjA5NCAxLjA5NCAwIDAgMS0uMjg4LjIyNS44MTkuODE5IDAgMCAxLS4xNTguMDY4LjQ4LjQ4IDAgMCAxLS4xNTMuMDI3LjYyLjYyIDAgMCAxLS4yNzQtLjA3NGMtLjI0MS0uMTM2LS40MjMtLjQ3OS0uNDIzLTEuMTQ2IDAtLjcxNS4yMDYtMS4xMi40NjktMS4zMDEuMDc3LS4wMzIuMTUzLS4wNjQuMjM4LS4wNjQuMTEzIDAgLjIyLjAyNy4zMTcuMDgyLjA5Ni4wNTcuMTg4LjEzMS4yNzIuMjIzdi0uODE1eiIgaWQ9Imljb25GZyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNMSAydjEyaDE0VjJIMXptMTMgMTFIMlYzaDEydjEwek01LjYzIDYuMzYxYTEuMDggMS4wOCAwIDAgMC0uNDI0LS4yOTUgMS42NDQgMS42NDQgMCAwIDAtLjYwOC0uMTAxYy0uMTE5IDAtLjI0MS4wMTItLjM2OC4wMzNhMy4yMTMgMy4yMTMgMCAwIDAtLjY3My4xOTUgMS4zMTMgMS4zMTMgMCAwIDAtLjIxMi4xMTR2Ljc2OGMuMTU4LS4xMzIuMzQxLS4yMzUuNTQ0LS4zMTMuMjA0LS4wNzguNDEzLS4xMTcuNjI3LS4xMTcuMjEzIDAgLjM3Ny4wNjMuNDk0LjE4Ni4xMTYuMTI1LjE3NC4zMjQuMTc0LjZsLTEuMDMuMTU0Yy0uMjA1LjAyNi0uMzguMDc3LS41MjYuMTUxYTEuMDgzIDEuMDgzIDAgMCAwLS41NjMuNjZBMS41NjIgMS41NjIgMCAwIDAgMyA4Ljg1N2MwIC4xNy4wMjUuMzIzLjA3NC40NjNhLjk0NS45NDUgMCAwIDAgLjU2OC41OTZjLjEzOS4wNTcuMjk3LjA4NC40NzguMDg0LjIyOSAwIC40MzEtLjA1My42MDQtLjE2YTEuMyAxLjMgMCAwIDAgLjQzOS0uNDYzaC4wMTR2LjUyOWguNzg1VjcuNDY5YzAtLjIzOC0uMDI3LS40NTEtLjA4My0uNjM3YTEuMjg2IDEuMjg2IDAgMCAwLS4yNDktLjQ3MXptLS40NDYgMi4wMmMwIC4xMzEtLjAyLjI1Mi0uMDU5LjM2NWEuODk3Ljg5NyAwIDAgMS0uMTY1LjI5NS43NTguNzU4IDAgMCAxLS41OTMuMjcyLjY4Mi42ODIgMCAwIDEtLjI0Mi0uMDQxLjUwNy41MDcgMCAwIDEtLjMwMi0uMjg2LjU4My41ODMgMCAwIDEtLjA0MS0uMjE4YzAtLjA4Ni4wMS0uMTY0LjAyNy0uMjMycy4wNTEtLjEyNy4wOTgtLjE4YS41NDYuNTQ2IDAgMCAxIC4xOTYtLjEzM2MuMDgzLS4wMzMuMTg5LS4wNjEuMzItLjA3OGwuNzYxLS4xMDl2LjM0NXptNC41MTQtMS44NjVhMS4yMDMgMS4yMDMgMCAwIDAtLjM5NC0uNDE2IDEuMDMgMS4wMyAwIDAgMC0uNTc0LS4xNTNjLS4xMjMgMC0uMjM0LjAxOC0uMzM2LjA1MWExIDEgMCAwIDAtLjI3OC4xNDcgMS4xNTMgMS4xNTMgMCAwIDAtLjIyNS4yMjIgMi4wMjIgMi4wMjIgMCAwIDAtLjE4MS4yODloLS4wMTNWNUg3djQuODg3aC42OTd2LS40ODVoLjAxM2MuMDQ0LjA4Mi4wOTUuMTU4LjE1MS4yMjkuMDU3LjA3LjExOS4xMzMuMTkxLjE4NmEuODM1LjgzNSAwIDAgMCAuMjM4LjEyMS45NDMuOTQzIDAgMCAwIC4yOTMuMDQyYy4yMyAwIC40MzQtLjA1My42MDktLjE2YTEuMzQgMS4zNCAwIDAgMCAuNDQzLS40NDNjLjEyLS4xODguMjExLS40MTIuMjcyLS42NzJBMy42MiAzLjYyIDAgMCAwIDEwIDcuODYxYTMuNTQgMy41NCAwIDAgMC0uMDc0LS43MzQgMi4wNDcgMi4wNDcgMCAwIDAtLjIyOC0uNjExem0tLjQ3NiAxLjk1M2MtLjAzOS4xOC0uMDk1LjMyOC0uMTY4LjQ0NWEuNzU1Ljc1NSAwIDAgMS0uMjY0LjI2Ni42ODcuNjg3IDAgMCAxLS42NTEuMDE1Ljc2Ljc2IDAgMCAxLS4yMzctLjIwNy45ODguOTg4IDAgMCAxLS4xNTQtLjMwNiAxLjI2MiAxLjI2MiAwIDAgMS0uMDU3LS4zODF2LS41MDZjMC0uMTcuMDItLjMyNi4wNjEtLjQ2NXMuMDk2LS4yNTguMTY4LS4zNTlhLjc1Ni43NTYgMCAwIDEgLjI1Ny0uMjMyYy4xLS4wNTUuMjEtLjA4Mi4zMzEtLjA4MmEuNjQ2LjY0NiAwIDAgMSAuNTcxLjMyYy4wNjYuMTA1LjExNi4yMy4xNS4zNzcuMDM1LjE0Ni4wNTIuMzEuMDUyLjQ5NCAwIC4yMzQtLjAxOS40NDEtLjA1OS42MjF6bTMuNjcyLTIuMzMyYS43LjcgMCAwIDEgLjEwNi4wNjZ2LjgxNGExLjE3OCAxLjE3OCAwIDAgMC0uMjczLS4yMjMuNjQ1LjY0NSAwIDAgMC0uMzE3LS4wODFjLS4wODUgMC0uMTYxLjAzMi0uMjM4LjA2NC0uMjYzLjE4MS0uNDY5LjU4Ni0uNDY5IDEuMzAxIDAgLjY2OC4xODIgMS4wMTEuNDIzIDEuMTQ2LjA4NC4wNC4xNzEuMDc0LjI3NC4wNzQuMDQ5IDAgLjEwMS0uMDEuMTUzLS4wMjdhLjg1Ni44NTYgMCAwIDAgLjE1OC0uMDY4IDEuMTYgMS4xNiAwIDAgMCAuMjg4LS4yMjV2Ljc3Yy0uMDkuMDc2LS4xOTIuMTM5LS4zMDkuMTg0YTEuMDk4IDEuMDk4IDAgMCAxLS40MTIuMDY4Ljk3NC45NzQgMCAwIDEtLjUyMy0uMTQzIDEuMjU3IDEuMjU3IDAgMCAxLS4zOTYtLjM5NiAyLjA5OCAyLjA5OCAwIDAgMS0uMjUyLS42MDIgMy4xMTggMy4xMTggMCAwIDEtLjA4OC0uNzU0YzAtLjMxNi4wMzItLjYwNC4wOTYtLjg2MS4wNjMtLjI1OC4xNTUtLjQ3OS4yNzMtLjY2LjExOS0uMTgyLjI2NS0uMzIyLjQzMy0uNDI0YTEuMTAyIDEuMTAyIDAgMCAxIDEuMDczLS4wMjN6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.color:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.color:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fS5pY29uLXZzLXJlZHtmaWxsOiNmNDg3NzF9Lmljb24tdnMteWVsbG93e2ZpbGw6I2ZmY2MwMH0uaWNvbi12cy1ncmVlbntmaWxsOiMzMzk5MzN9Lmljb24tdnMtYmx1ZXtmaWxsOiMxYmExZTJ9Lmljb24tdnMtYWN0aW9uLXB1cnBsZXtmaWxsOiNiMTgwZDd9Lmljb24td2hpdGV7ZmlsbDojMDAwMDAwfTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZIMFYwaDE2djE2eiIgaWQ9ImNhbnZhcyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLW91dCIgZD0iTTE2IDhjMCA0LjQxMS0zLjU4OSA4LTggOGEyLjgwMyAyLjgwMyAwIDAgMS0yLjgtMi44YzAtLjgzMy4yNzItMS42MjkuNzY2LTIuMjQxYS41OTYuNTk2IDAgMCAwIC4xMDEtLjM1OS42NjcuNjY3IDAgMCAwLS42NjctLjY2Ni41OC41OCAwIDAgMC0uMzU4LjEwMkEzLjU4NCAzLjU4NCAwIDAgMSAyLjggMTAuOCAyLjgwMyAyLjgwMyAwIDAgMSAwIDhjMC00LjQxMSAzLjU4OS04IDgtOHM4IDMuNTg5IDggOHoiIGlkPSJvdXRsaW5lIi8+PHBhdGggY2xhc3M9Imljb24td2hpdGUiIGQ9Ik01LjQgNy45MzNhMi42NyAyLjY3IDAgMCAxIDIuNjY3IDIuNjY2YzAgLjYwNi0uMTkzIDEuMTc5LS41NDQgMS42MTRhMS41OTkgMS41OTkgMCAwIDAtLjMyMy45ODcuOC44IDAgMCAwIC44LjhjMy4zMDkgMCA2LTIuNjkxIDYtNnMtMi42OTEtNi02LTYtNiAyLjY5MS02IDZjMCAuNDQxLjM1OS44LjguOC4zNzggMCAuNzI5LS4xMTQuOTg2LS4zMjJBMi41NjggMi41NjggMCAwIDEgNS40IDcuOTMzeiIgaWQ9Imljb25GZyIvPjxnIGlkPSJpY29uQmciPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNOCAxNWMtLjk5MiAwLTEuOC0uODA4LTEuOC0xLjggMC0uNjA2LjE5My0xLjE3OS41NDQtMS42MTMuMjA4LS4yNTkuMzIzLS42MDkuMzIzLS45ODcgMC0uOTE5LS43NDgtMS42NjYtMS42NjctMS42NjYtLjM3NyAwLS43MjguMTE1LS45ODYuMzIzQTIuNTggMi41OCAwIDAgMSAyLjggOS44QzEuODA4IDkuOCAxIDguOTkyIDEgOGMwLTMuODYgMy4xNC03IDctNyAzLjg1OSAwIDcgMy4xNCA3IDcgMCAzLjg1OS0zLjE0MSA3LTcgN3pNNS40IDcuOTMzYTIuNjcgMi42NyAwIDAgMSAyLjY2NyAyLjY2NmMwIC42MDYtLjE5MyAxLjE3OS0uNTQ0IDEuNjE0YTEuNTk5IDEuNTk5IDAgMCAwLS4zMjMuOTg3LjguOCAwIDAgMCAuOC44YzMuMzA5IDAgNi0yLjY5MSA2LTZzLTIuNjkxLTYtNi02LTYgMi42OTEtNiA2YzAgLjQ0MS4zNTkuOC44LjguMzc4IDAgLjcyOS0uMTE0Ljk4Ni0uMzIyQTIuNTY4IDIuNTY4IDAgMCAxIDUuNCA3LjkzM3oiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1hY3Rpb24tcHVycGxlIiBkPSJNNC41IDUuMzc1YS44NzUuODc1IDAgMSAwIDAgMS43NS44NzUuODc1IDAgMCAwIDAtMS43NXoiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1ibHVlIiBkPSJNNy4xMjUgMy42MjVhLjg3NS44NzUgMCAxIDAgMCAxLjc1Ljg3NS44NzUgMCAwIDAgMC0xLjc1eiIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWdyZWVuIiBkPSJNMTAuNjI1IDQuNWEuODc1Ljg3NSAwIDEgMCAwIDEuNzUuODc1Ljg3NSAwIDAgMCAwLTEuNzV6Ii8+PHBhdGggY2xhc3M9Imljb24tdnMteWVsbG93IiBkPSJNMTEuNSA4YS44NzUuODc1IDAgMSAwIDAgMS43NS44NzUuODc1IDAgMCAwIDAtMS43NXoiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1yZWQiIGQ9Ik05Ljc1IDEwLjYyNWEuODc1Ljg3NSAwIDEgMCAwIDEuNzUuODc1Ljg3NSAwIDAgMCAwLTEuNzV6Ii8+PC9nPjwvc3ZnPg==)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.file:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.file:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fS5pY29uLXZzLWZne2ZpbGw6IzJiMjgyZX08L3N0eWxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTE2IDE2SDBWMGgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNSAxNkgyVjBoOC42MjFMMTUgNC4zNzlWMTZ6IiBpZD0ib3V0bGluZSIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWZnIiBkPSJNMTMgMTRINFYyaDV2NGg0djh6bS0zLTlWMi4yMDdMMTIuNzkzIDVIMTB6IiBpZD0iaWNvbkZnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtYmciIGQ9Ik0zIDF2MTRoMTFWNC43OTNMMTAuMjA3IDFIM3ptMTAgMTNINFYyaDV2NGg0djh6bS0zLTlWMi4yMDdMMTIuNzkzIDVIMTB6IiBpZD0iaWNvbkJnIi8+PC9zdmc+)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.reference:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.reference:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtvcGFjaXR5OjA7ZmlsbDojMmQyZDMwfS5pY29uLXZzLW91dHtmaWxsOiMyZDJkMzB9Lmljb24tdnMtYmd7ZmlsbDojYzVjNWM1fS5pY29uLXZzLWZne2ZpbGw6IzJiMjgyZX0uaWNvbi12cy1hY3Rpb24tYmx1ZXtmaWxsOiM3NWJlZmZ9PC9zdHlsZT48cGF0aCBjbGFzcz0iaWNvbi1jYW52YXMtdHJhbnNwYXJlbnQiIGQ9Ik0xNiAxNkgwVjBoMTZ2MTZ6IiBpZD0iY2FudmFzIi8+PHBhdGggY2xhc3M9Imljb24tdnMtb3V0IiBkPSJNMTQgNC41NTZWMTNjMCAuOTctLjcwMSAyLTIgMkg0Yy0uOTcgMC0yLS43MDEtMi0yVjYuNjQ5QTMuNDk1IDMuNDk1IDAgMCAxIDAgMy41QzAgMS41NyAxLjU3IDAgMy41IDBINXYxaDUuMDYxTDE0IDQuNTU2eiIgaWQ9Im91dGxpbmUiIHN0eWxlPSJkaXNwbGF5OiBub25lOyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWJnIiBkPSJNMTMgNXY4cy0uMDM1IDEtMS4wMzUgMWgtOFMzIDE0IDMgMTNWOWgxdjRoOFY2SDkuMzk3bC41MTctLjUyTDkgNC41NzJWM0g3LjQxOUw2LjQxMyAyaDMuMjI4TDEzIDV6IiBpZD0iaWNvbkJnIi8+PHBhdGggY2xhc3M9Imljb24tdnMtZmciIGQ9Ik03LjQxOSAzSDl2MS41NzJMNy40MTkgM3ptMS45NzggM0w2LjQxNiA5SDR2NGg4VjZIOS4zOTd6IiBpZD0iaWNvbkZnIiBzdHlsZT0iZGlzcGxheTogbm9uZTsiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1hY3Rpb24tYmx1ZSIgZD0iTTUuOTg4IDZIMy41YTIuNSAyLjUgMCAxIDEgMC01SDR2MWgtLjVDMi42NzMgMiAyIDIuNjczIDIgMy41UzIuNjczIDUgMy41IDVoMi41MTNMNCAzaDJsMi41IDIuNDg0TDYgOEg0bDEuOTg4LTJ6IiBpZD0iY29sb3JBY3Rpb24iLz48L3N2Zz4=)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.snippet:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.snippet:before{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnNDY5NCIKICAgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE0NzA1Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZGVmcwogICAgIGlkPSJkZWZzNDcwMyIgLz4KICA8c3R5bGUKICAgICBpZD0ic3R5bGU0Njk2Ij4uaWNvbi1jYW52YXMtdHJhbnNwYXJlbnR7b3BhY2l0eTowO2ZpbGw6I2Y2ZjZmNn0uaWNvbi12cy1vdXR7ZmlsbDojZjZmNmY2fS5pY29uLXZzLWFjdGlvbi1vcmFuZ2V7ZmlsbDojYzI3ZDFhfTwvc3R5bGU+CiAgPGcKICAgICBpZD0iZzQ3MjQiCiAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMS4zMzMzMzMzLDAsMCwxLjMzMzMzMzMsLTI0NS45OTk5OSwtMzEuOTk5OTk5KSI+CiAgICA8cGF0aAogICAgICAgZD0ibSAxODUsMjQgMTEsMCAwLDEyIC0xMSwwIHoiCiAgICAgICBpZD0icGF0aDQ1MjgiCiAgICAgICBzdHlsZT0iZmlsbDojMmQyZDMwIiAvPgogICAgPHBhdGgKICAgICAgIGQ9Im0gMTk0LDMzIDAsLTcgLTcsMCAwLDcgLTEsMCAwLC04IDksMCAwLDggeiBtIC04LDEgMSwwIDAsMSAtMSwwIHogbSAyLDAgMSwwIDAsMSAtMSwwIHogbSAyLDAgMSwwIDAsMSAtMSwwIHogbSAyLDAgMSwwIDAsMSAtMSwwIHogbSAyLDAgMSwwIDAsMSAtMSwwIHoiCiAgICAgICBpZD0icGF0aDQ1MzAiCiAgICAgICBzdHlsZT0iZmlsbDojYzVjNWM1IiAvPgogICAgPHBhdGgKICAgICAgIGQ9Im0gMTg3LDI2IDcsMCAwLDcgLTcsMCB6IgogICAgICAgaWQ9InBhdGg0NTMyIgogICAgICAgc3R5bGU9ImZpbGw6IzJiMjgyZSIgLz4KICA8L2c+Cjwvc3ZnPgo=)}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.customcolor:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.customcolor:before{background-image:none}.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.folder:before,.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.folder:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uaWNvbi1jYW52YXMtdHJhbnNwYXJlbnR7b3BhY2l0eTowO2ZpbGw6I0Y2RjZGNjt9IC5pY29uLXZzLW91dHtvcGFjaXR5OjA7ZmlsbDojRjZGNkY2O30gLmljb24tdnMtZmd7b3BhY2l0eTowO2ZpbGw6I0YwRUZGMTt9IC5pY29uLWZvbGRlcntmaWxsOiNDNUM1QzU7fTwvc3R5bGU+PHBhdGggY2xhc3M9Imljb24tY2FudmFzLXRyYW5zcGFyZW50IiBkPSJNMTYgMTZoLTE2di0xNmgxNnYxNnoiIGlkPSJjYW52YXMiLz48cGF0aCBjbGFzcz0iaWNvbi12cy1vdXQiIGQ9Ik0xNiAyLjV2MTBjMCAuODI3LS42NzMgMS41LTEuNSAxLjVoLTExLjk5NmMtLjgyNyAwLTEuNS0uNjczLTEuNS0xLjV2LThjMC0uODI3LjY3My0xLjUgMS41LTEuNWgyLjg4NmwxLTJoOC4xMWMuODI3IDAgMS41LjY3MyAxLjUgMS41eiIgaWQ9Im91dGxpbmUiLz48cGF0aCBjbGFzcz0iaWNvbi1mb2xkZXIiIGQ9Ik0xNC41IDJoLTcuNDkybC0xIDJoLTMuNTA0Yy0uMjc3IDAtLjUuMjI0LS41LjV2OGMwIC4yNzYuMjIzLjUuNS41aDExLjk5NmMuMjc1IDAgLjUtLjIyNC41LS41di0xMGMwLS4yNzYtLjIyNS0uNS0uNS0uNXptLS40OTYgMmgtNi40OTZsLjUtMWg1Ljk5NnYxeiIgaWQ9Imljb25CZyIvPjxwYXRoIGNsYXNzPSJpY29uLXZzLWZnIiBkPSJNMTQgM3YxaC02LjVsLjUtMWg2eiIgaWQ9Imljb25GZyIvPjwvc3ZnPg==)}.monaco-editor .zone-widget{position:absolute;z-index:10}.monaco-editor .zone-widget .zone-widget-container{border-top-style:solid;border-bottom-style:solid;border-top-width:0;border-bottom-width:0;position:relative}.monaco-editor .accessibilityHelpWidget{padding:10px;vertical-align:middle;overflow:scroll}.monaco-editor .iPadShowKeyboard{width:58px;min-width:0;height:36px;min-height:0;margin:0;padding:0;position:absolute;resize:none;overflow:hidden;background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1OCIgaGVpZ2h0PSIzNiI+PHBhdGggZmlsbD0iI0YwRUZGMSIgZD0iTTU0IDMydi0yOGgtNTB2MjhoNTB6bS0xNi0yaC0xOHYtNmgxOHY2em02IDBoLTR2LTZoNHY2em04IDBoLTZ2LTZoNnY2em0tNC0yNGg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bS02LTEyaDR2NGgtNHYtNHptMCA2aDR2NGgtNHYtNHptMCA2aDR2NGgtNHYtNHptLTYtMTJoNHY0aC00di00em0wIDZoNHY0aC00di00em0wIDZoNHY0aC00di00em0tNi0xMmg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bS02LTEyaDR2NGgtNHYtNHptMCA2aDR2NGgtNHYtNHptMCA2aDR2NGgtNHYtNHptLTYtMTJoNHY0aC00di00em0wIDZoNHY0aC00di00em0wIDZoNHY0aC00di00em0wIDEyaC00di02aDR2NnptLTYtMjRoNHY0aC00di00em0wIDZoNHY0aC00di00em0wIDZoNHY0aC00di00em0tNi0xMmg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bTAgNmg2djZoLTZ2LTZ6Ii8+PHBhdGggZmlsbD0iIzQyNDI0MiIgZD0iTTU1LjMzNiAwaC01My4yODVjLTEuMzQ0IDAtMi4wNTEuNjU2LTIuMDUxIDJ2MzJjMCAxLjM0NC43MDcgMS45NjUgMi4wNTEgMS45NjVsNTMuOTQ5LjAzNWMxLjM0NCAwIDItLjY1NiAyLTJ2LTMyYzAtMS4zNDQtMS4zMi0yLTIuNjY0LTJ6bS0xLjMzNiAzMmgtNTB2LTI4aDUwdjI4eiIvPjxyZWN0IHg9IjYiIHk9IjEyIiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMTIiIHk9IjEyIiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMTgiIHk9IjEyIiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMjQiIHk9IjEyIiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMzAiIHk9IjEyIiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMzYiIHk9IjEyIiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iNDIiIHk9IjEyIiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iNDgiIHk9IjEyIiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iNiIgeT0iNiIgZmlsbD0iIzQyNDI0MiIgd2lkdGg9IjQiIGhlaWdodD0iNCIvPjxyZWN0IHg9IjEyIiB5PSI2IiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMTgiIHk9IjYiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIyNCIgeT0iNiIgZmlsbD0iIzQyNDI0MiIgd2lkdGg9IjQiIGhlaWdodD0iNCIvPjxyZWN0IHg9IjMwIiB5PSI2IiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMzYiIHk9IjYiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSI0MiIgeT0iNiIgZmlsbD0iIzQyNDI0MiIgd2lkdGg9IjQiIGhlaWdodD0iNCIvPjxyZWN0IHg9IjQ4IiB5PSI2IiBmaWxsPSIjNDI0MjQyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iNiIgeT0iMTgiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIxMiIgeT0iMTgiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIxOCIgeT0iMTgiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIyNCIgeT0iMTgiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIzMCIgeT0iMTgiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIzNiIgeT0iMTgiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSI0MiIgeT0iMTgiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSI0OCIgeT0iMTgiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSI2IiB5PSIyNCIgZmlsbD0iIzQyNDI0MiIgd2lkdGg9IjYiIGhlaWdodD0iNiIvPjxyZWN0IHg9IjQ2IiB5PSIyNCIgZmlsbD0iIzQyNDI0MiIgd2lkdGg9IjYiIGhlaWdodD0iNiIvPjxyZWN0IHg9IjIwIiB5PSIyNCIgZmlsbD0iIzQyNDI0MiIgd2lkdGg9IjE4IiBoZWlnaHQ9IjYiLz48cmVjdCB4PSIxNCIgeT0iMjQiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjYiLz48cmVjdCB4PSI0MCIgeT0iMjQiIGZpbGw9IiM0MjQyNDIiIHdpZHRoPSI0IiBoZWlnaHQ9IjYiLz48L3N2Zz4=) 50% no-repeat;border:4px solid #f6f6f6;border-radius:4px}.monaco-editor.vs-dark .iPadShowKeyboard{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1OCIgaGVpZ2h0PSIzNiI+PHBhdGggZmlsbD0iIzJCMjgyRSIgZD0iTTU0IDMydi0yOGgtNTB2MjhoNTB6bS0xNi0yaC0xOHYtNmgxOHY2em02IDBoLTR2LTZoNHY2em04IDBoLTZ2LTZoNnY2em0tNC0yNGg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bS02LTEyaDR2NGgtNHYtNHptMCA2aDR2NGgtNHYtNHptMCA2aDR2NGgtNHYtNHptLTYtMTJoNHY0aC00di00em0wIDZoNHY0aC00di00em0wIDZoNHY0aC00di00em0tNi0xMmg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bS02LTEyaDR2NGgtNHYtNHptMCA2aDR2NGgtNHYtNHptMCA2aDR2NGgtNHYtNHptLTYtMTJoNHY0aC00di00em0wIDZoNHY0aC00di00em0wIDZoNHY0aC00di00em0wIDEyaC00di02aDR2NnptLTYtMjRoNHY0aC00di00em0wIDZoNHY0aC00di00em0wIDZoNHY0aC00di00em0tNi0xMmg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bTAgNmg0djRoLTR2LTR6bTAgNmg2djZoLTZ2LTZ6Ii8+PHBhdGggZmlsbD0iI0M1QzVDNSIgZD0iTTU1LjMzNiAwaC01My4yODVjLTEuMzQ0IDAtMi4wNTEuNjU2LTIuMDUxIDJ2MzJjMCAxLjM0NC43MDcgMS45NjUgMi4wNTEgMS45NjVsNTMuOTQ5LjAzNWMxLjM0NCAwIDItLjY1NiAyLTJ2LTMyYzAtMS4zNDQtMS4zMi0yLTIuNjY0LTJ6bS0xLjMzNiAzMmgtNTB2LTI4aDUwdjI4eiIvPjxyZWN0IHg9IjYiIHk9IjEyIiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMTIiIHk9IjEyIiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMTgiIHk9IjEyIiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMjQiIHk9IjEyIiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMzAiIHk9IjEyIiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMzYiIHk9IjEyIiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iNDIiIHk9IjEyIiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iNDgiIHk9IjEyIiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iNiIgeT0iNiIgZmlsbD0iI0M1QzVDNSIgd2lkdGg9IjQiIGhlaWdodD0iNCIvPjxyZWN0IHg9IjEyIiB5PSI2IiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMTgiIHk9IjYiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIyNCIgeT0iNiIgZmlsbD0iI0M1QzVDNSIgd2lkdGg9IjQiIGhlaWdodD0iNCIvPjxyZWN0IHg9IjMwIiB5PSI2IiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iMzYiIHk9IjYiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSI0MiIgeT0iNiIgZmlsbD0iI0M1QzVDNSIgd2lkdGg9IjQiIGhlaWdodD0iNCIvPjxyZWN0IHg9IjQ4IiB5PSI2IiBmaWxsPSIjQzVDNUM1IiB3aWR0aD0iNCIgaGVpZ2h0PSI0Ii8+PHJlY3QgeD0iNiIgeT0iMTgiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIxMiIgeT0iMTgiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIxOCIgeT0iMTgiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIyNCIgeT0iMTgiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIzMCIgeT0iMTgiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSIzNiIgeT0iMTgiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSI0MiIgeT0iMTgiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSI0OCIgeT0iMTgiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiLz48cmVjdCB4PSI2IiB5PSIyNCIgZmlsbD0iI0M1QzVDNSIgd2lkdGg9IjYiIGhlaWdodD0iNiIvPjxyZWN0IHg9IjQ2IiB5PSIyNCIgZmlsbD0iI0M1QzVDNSIgd2lkdGg9IjYiIGhlaWdodD0iNiIvPjxyZWN0IHg9IjIwIiB5PSIyNCIgZmlsbD0iI0M1QzVDNSIgd2lkdGg9IjE4IiBoZWlnaHQ9IjYiLz48cmVjdCB4PSIxNCIgeT0iMjQiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjYiLz48cmVjdCB4PSI0MCIgeT0iMjQiIGZpbGw9IiNDNUM1QzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjYiLz48L3N2Zz4=) 50% no-repeat;border:4px solid #252526}.monaco-editor .tokens-inspect-widget{z-index:50;-webkit-user-select:text;-ms-user-select:text;-moz-user-select:text;-o-user-select:text;user-select:text;padding:10px}.tokens-inspect-separator{height:1px;border:0}.monaco-editor .tokens-inspect-widget .tm-token{font-family:monospace}.monaco-editor .tokens-inspect-widget .tm-token-length{font-weight:400;font-size:60%;float:right}.monaco-editor .tokens-inspect-widget .tm-metadata-table{width:100%}.monaco-editor .tokens-inspect-widget .tm-metadata-value{font-family:monospace;text-align:right}.monaco-editor .tokens-inspect-widget .tm-token-type{font-family:monospace}.monaco-quick-open-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight,.monaco-quick-open-widget .monaco-tree .monaco-tree-row .monaco-highlighted-label .highlight{color:#0066bf}.vs-dark .monaco-quick-open-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight,.vs-dark .monaco-quick-open-widget .monaco-tree .monaco-tree-row .monaco-highlighted-label .highlight{color:#0097fb}.hc-black .monaco-quick-open-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight,.hc-black .monaco-quick-open-widget .monaco-tree .monaco-tree-row .monaco-highlighted-label .highlight{color:#f38518}.monaco-quick-open-widget{font-size:13px}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon,.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iNDAiPjxwYXRoIGQ9Ik0yODguNDgzIDMzYy0uNzcyIDAtMS40OTctLjEyMy0yLjE1My0uMzY1LS42NzgtLjI1My0xLjI3LS42MTctMS43Ni0xLjA4NC0uNS0uNDc1LS44OTItMS4wNDktMS4xNjMtMS43MDQtLjI3LS42NDQtLjQwNy0xLjM3MS0uNDA3LTIuMTU4IDAtLjUxNy4wNjEtMS4wMTguMTc4LTEuNDkuMTE2LS40Ny4yOS0uOTI1LjUxNi0xLjM0OC4yMjUtLjQyMi41MDgtLjgxNS44NDQtMS4xNjcuMzM0LS4zNTIuNzE3LS42NTYgMS4xMzktLjkwNS40MTYtLjI0Ni44ODEtLjQ0IDEuMzgtLjU3Ni40OTMtLjEzNCAxLjAyNi0uMjAyIDEuNTg3LS4yMDIuNzA1IDAgMS4zODIuMTA5IDIuMDEzLjMyNC42NDIuMjE3IDEuMjE4LjUzOCAxLjcwOC45NTUuNTAxLjQyNS45MDMuOTQ4IDEuMTkzIDEuNTU2LjI5NC42MjMuNDQyIDEuMzE2LjQ0MiAyLjA2NCAwIC42MTktLjA5IDEuMTg1LS4yNjggMS42NzktLjE3OC40OTItLjQyLjkyLS43MjEgMS4yNzUtLjMzMS4zNzctLjY5OS42NTgtMS4xMDQuODQ3bC0uMDQ4LjAyMnYxLjUzbC0uNTg3LjI2NmMtLjEyOC4wNTktLjI4OC4xMTctLjQ3NC4xNzktLjE5My4wNjItLjQwNC4xMTQtLjY0NS4xNTktLjIyOS4wNC0uNDc3LjA3Ni0uNzUzLjEwMy0uMjcuMDI3LS41NzguMDQtLjkxNy4wNHoiIGZpbGw9IiMyRDJEMkQiLz48cGF0aCBkPSJNMjkxLjcxNiAyNC4wNDFjLS4zOTYtLjMzNi0uODU2LS41OTMtMS4zODQtLjc3MS0uNTI3LS4xOC0xLjA5LS4yNzEtMS42ODktLjI3MS0uNDczIDAtLjkxMi4wNTUtMS4zMjQuMTY3LS40MTQuMTEyLS43OTEuMjctMS4xMzUuNDczLS4zNDIuMjAyLS42NS40NDYtLjkyMi43MzMtLjI3My4yODYtLjUwMi42MDItLjY4Ni45NDktLjE4Ni4zNDctLjMzLjcyMi0uNDI4IDEuMTE5LS4xLjM5OS0uMTQ4LjgxNC0uMTQ4IDEuMjQ3IDAgLjY1Mi4xMDkgMS4yNDcuMzMyIDEuNzc2LjIxOS41MzEuNTMuOTg0LjkyOCAxLjM2MS4zOTYuMzc4Ljg3MS42NjcgMS40MTYuODcuNTQ4LjIwMiAxLjE1Mi4zMDQgMS44MDguMzA0LjMwMiAwIC41NzctLjAxMS44MjMtLjAzNS4yNDYtLjAyMy40NjgtLjA1Ni42NjQtLjA5MS4xOTUtLjAzNi4zNjYtLjA3OC41MTQtLjEyNWwuMzc1LS4xNHYtLjg1NGwtLjQ2My4xODRjLS4xNi4wNTYtLjMzNi4xMDQtLjUyMS4xNDMtLjE4OC4wMzctLjM4Ny4wNjktLjYwNC4wODktLjIxMy4wMjQtLjQ0OC4wMzQtLjcuMDM0LS41NjIgMC0xLjA2NC0uMDg4LTEuNTA5LS4yNjQtLjQ0Mi0uMTc2LS44MTYtLjQyMS0xLjEyNS0uNzMxLS4zMDktLjMxNC0uNTQ1LS42ODctLjcwOC0xLjEyNC0uMTYxLS40MzUtLjI0My0uOTEzLS4yNDMtMS40MzIgMC0uNTQ1LjA5LTEuMDUzLjI3My0xLjUyMi4xODItLjQ3MS40MzUtLjg3OS43NTgtMS4yMjUuMzI0LS4zNDUuNzA4LS42MTcgMS4xNTUtLjgxNS40NDYtLjE5Ni45MzQtLjI5NCAxLjQ1Ny0uMjk0LjQxOSAwIC43OTguMDQ0IDEuMTIyLjEzNi4zMjkuMDkxLjYyLjIxNS44NzEuMzY5LjI1NC4xNTguNDY1LjMzOS42NDMuNTQ3LjE3OS4yMDkuMzI0LjQzMi40MzguNjY3LjExMy4yMzcuMTkzLjQ4LjI0Ni43MzEuMDUxLjI1NC4wNzYuNS4wNzYuNzQxIDAgLjM0NC0uMDMzLjY1My0uMTAyLjkyNi0uMDY4LjI3NC0uMTU4LjUwMy0uMjY5LjY5NC0uMTEuMTg5LS4yMzkuMzM1LS4zODYuNDM0cy0uMjk1LjE0OC0uNDUzLjE0OGwtLjIxNS0uMDQ1Yy0uMDY2LS4wMjktLjExOS0uMDgtLjE2Ni0uMTU2LS4wNDYtLjA3NS0uMDgyLS4xNzctLjEwNy0uMzA2LS4wMjUtLjEyNi0uMDM5LS4yOTItLjAzOS0uNDkybC4wMTgtLjMyNS4wNDEtLjUzLjA1NS0uNjQ0LjA1OC0uNjQ3LjA0OC0uNTQ2LjAyNy0uMzQ0aC0uOTE5bC0uMDU0LjZoLS4wMjFjLS4wMjUtLjEwMy0uMDctLjE5NS0uMTM2LS4yODEtLjA2My0uMDgzLS4xNDEtLjE1NS0uMjMzLS4yMTYtLjA5MS0uMDYxLS4xOTMtLjEwNi0uMzA3LS4xNDEtLjExNS0uMDMzLS4yMzgtLjA0OC0uMzY5LS4wNDgtLjMzNyAwLS42NDYuMDctLjkyNC4yMTYtLjI4MS4xNDQtLjUxOC4zNDQtLjcyMS41OTktLjIwMS4yNTQtLjM1NS41NTYtLjQ2NS45MDUtLjExNS4zNS0uMTcuNzI2LS4xNyAxLjEzNCAwIC4zNDQuMDQ1LjY0NS4xMzUuOTAxLjA4OC4yNi4yMTEuNDczLjM1OS42NDYuMTUzLjE3MS4zMjkuMy41MzQuMzgyLjIuMDg2LjQxNS4xMjkuNjQxLjEyOS4xNzYgMCAuMzQyLS4wMjcuNDk5LS4wODEuMTU0LS4wNTIuMzAyLS4xMy40MzItLjIzMi4xMzQtLjEwNC4yNDgtLjIzLjM0OC0uMzguMTAyLS4xNDkuMTgyLS4zMjMuMjM2LS41MmguMDI3YzAgLjM3Ni4xMDEuNjc0LjMwNy44OTMuMjA3LjIyLjUwMi4zMy44ODkuMzMuMjkyIDAgLjU4LS4wNjQuODYzLS4xOTguMjgzLS4xMzIuNTM2LS4zMjguNzYyLS41ODYuMjIzLS4yNjIuNDA0LS41ODMuNTQzLS45NjYuMTM4LS4zODQuMjA4LS44My4yMDgtMS4zNCAwLS42MDUtLjExNy0xLjE1LS4zNDUtMS42MzQtLjIzMS0uNDgyLS41NDYtLjg5MS0uOTM5LTEuMjI1bS0yLjM2OCAzLjc3NGMtLjA1Ni4yNzctLjEzNi41MTctLjI0Ni43MTktLjEwOS4yMDMtLjI0Ni4zNjMtLjQwNy40ODEtLjE2My4xMTUtLjM1NC4xNzYtLjU3Mi4xNzYtLjEyIDAtLjIzNi0uMDI1LS4zNDQtLjA3OC0uMTA4LS4wNTItLjIwNi0uMTMtLjI4OS0uMjMyLS4wODEtLjEwMy0uMTQ4LS4yMzQtLjE5OC0uMzktLjA0Ni0uMTU2LS4wNy0uMzM3LS4wNy0uNTQ3IDAtLjIzNy4wMjctLjQ4MS4wOC0uNzI5LjA1Ni0uMjQ3LjEzNy0uNDczLjI1LS42NzcuMTA5LS4yLjI1LS4zNjMuNDE2LS40OTIuMTY1LS4xMjcuMzYxLS4xOTEuNTgyLS4xOTEuMTIzIDAgLjIzNC4wMjEuMzQuMDYzLjEwNy4wNDIuMTk4LjEwNy4yNzkuMTk2LjA4LjA4Ny4xNDUuMTk3LjE4OS4zMy4wNDMuMTM0LjA3LjI5NC4wNy40OCAwIC4zMTctLjAzMS42MTUtLjA4Ljg5MSIgZmlsbD0iI0M1QzVDNSIvPjxwYXRoIGQ9Ik0yODguNDgzIDEzYy0uNzcyIDAtMS40OTctLjEyMy0yLjE1My0uMzY1LS42NzgtLjI1My0xLjI3LS42MTctMS43Ni0xLjA4NC0uNS0uNDc1LS44OTItMS4wNDktMS4xNjMtMS43MDQtLjI2OS0uNjQ0LS40MDctMS4zNzEtLjQwNy0yLjE1OSAwLS41MTcuMDYxLTEuMDE4LjE3OC0xLjQ5LjExNi0uNDcuMjktLjkyNS41MTYtMS4zNDguMjI1LS40MjIuNTA4LS44MTUuODQ0LTEuMTY3LjMzNC0uMzUyLjcxNy0uNjU2IDEuMTM5LS45MDUuNDE2LS4yNDYuODgxLS40NCAxLjM4LS41NzYuNDkyLS4xMzQgMS4wMjUtLjIwMiAxLjU4Ni0uMjAyLjcwNSAwIDEuMzgyLjEwOSAyLjAxMy4zMjQuNjQyLjIxNyAxLjIxOC41MzggMS43MDguOTU1LjUwMS40MjUuOTAzLjk0OCAxLjE5MyAxLjU1Ni4yOTUuNjI0LjQ0MyAxLjMxNy40NDMgMi4wNjUgMCAuNjE5LS4wOSAxLjE4NS0uMjY4IDEuNjc5LS4xNzguNDkyLS40Mi45Mi0uNzIxIDEuMjc1LS4zMzEuMzc3LS42OTkuNjU4LTEuMTA0Ljg0N2wtLjA0OC4wMjJ2MS41M2wtLjU4Ny4yNjZjLS4xMjguMDU5LS4yODguMTE3LS40NzQuMTc5LS4xOTMuMDYyLS40MDQuMTE0LS42NDUuMTU5LS4yMjkuMDQtLjQ3Ny4wNzYtLjc1My4xMDMtLjI3LjAyNy0uNTc4LjA0LS45MTcuMDR6IiBmaWxsPSIjRjNGM0YzIi8+PHBhdGggZD0iTTI5MS43MTYgNC4wNDFjLS4zOTYtLjMzNi0uODU2LS41OTMtMS4zODQtLjc3MS0uNTI3LS4xNzktMS4wOS0uMjctMS42ODktLjI3LS40NzMgMC0uOTEyLjA1NS0xLjMyNC4xNjctLjQxNC4xMTItLjc5MS4yNy0xLjEzNS40NzMtLjM0Mi4yMDItLjY1LjQ0Ni0uOTIyLjczMy0uMjczLjI4Ni0uNTAyLjYwMi0uNjg2Ljk0OS0uMTg2LjM0Ny0uMzMuNzIyLS40MjggMS4xMTktLjA5OS40LS4xNDguODE1LS4xNDggMS4yNDcgMCAuNjUyLjEwOSAxLjI0Ny4zMzIgMS43NzYuMjE5LjUzMS41My45ODQuOTI4IDEuMzYxLjM5Ni4zNzguODcxLjY2NyAxLjQxNi44Ny41NDguMjAyIDEuMTUyLjMwNCAxLjgwOC4zMDQuMzAyIDAgLjU3Ny0uMDExLjgyMy0uMDM1LjI0Ni0uMDIzLjQ2OC0uMDU2LjY2NC0uMDkxLjE5NS0uMDM2LjM2Ni0uMDc4LjUxNC0uMTI1bC4zNzUtLjE0di0uODU0bC0uNDYzLjE4NGMtLjE2LjA1Ni0uMzM2LjEwNC0uNTIxLjE0My0uMTg4LjAzNy0uMzg3LjA2OS0uNjA0LjA4OS0uMjEzLjAyNC0uNDQ4LjAzNC0uNy4wMzQtLjU2MiAwLTEuMDY0LS4wODgtMS41MDktLjI2NC0uNDQyLS4xNzYtLjgxNi0uNDIxLTEuMTI1LS43MzEtLjMwOS0uMzE0LS41NDUtLjY4Ny0uNzA4LTEuMTI0LS4xNjEtLjQzNS0uMjQzLS45MTMtLjI0My0xLjQzMiAwLS41NDUuMDktMS4wNTMuMjczLTEuNTIyLjE4Mi0uNDcxLjQzNS0uODc5Ljc1OC0xLjIyNS4zMjQtLjM0NS43MDgtLjYxNyAxLjE1NS0uODE1LjQ0Ni0uMTk2LjkzNC0uMjk0IDEuNDU3LS4yOTQuNDE5IDAgLjc5OC4wNDQgMS4xMjIuMTM2LjMyOS4wOTEuNjIuMjE1Ljg3MS4zNjkuMjU0LjE1OC40NjUuMzM5LjY0My41NDcuMTc5LjIwOS4zMjQuNDMyLjQzOC42NjcuMTEzLjIzNy4xOTMuNDguMjQ2LjczMS4wNTEuMjU0LjA3Ni41LjA3Ni43NDEgMCAuMzQ0LS4wMzMuNjUzLS4xMDIuOTI2LS4wNjguMjc0LS4xNTguNTAzLS4yNjkuNjk0LS4xMS4xODktLjIzOS4zMzUtLjM4Ni40MzRzLS4yOTUuMTQ4LS40NTMuMTQ4bC0uMjE1LS4wNDVjLS4wNjYtLjAyOS0uMTE5LS4wOC0uMTY2LS4xNTYtLjA0Ni0uMDc1LS4wODItLjE3Ny0uMTA3LS4zMDYtLjAyNS0uMTI2LS4wMzktLjI5Mi0uMDM5LS40OTJsLjAxOC0uMzI1LjA0MS0uNTMuMDU1LS42NDQuMDU4LS42NDcuMDQ4LS41NDYuMDI3LS4zNDRoLS45MTlsLS4wNTQuNmgtLjAyMWMtLjAyNS0uMTAzLS4wNy0uMTk1LS4xMzYtLjI4MS0uMDYzLS4wODMtLjE0MS0uMTU1LS4yMzMtLjIxNi0uMDkxLS4wNjEtLjE5My0uMTA2LS4zMDctLjE0MS0uMTE1LS4wMzMtLjIzOC0uMDQ4LS4zNjktLjA0OC0uMzM3IDAtLjY0Ni4wNy0uOTI0LjIxNi0uMjgxLjE0NC0uNTE4LjM0NC0uNzIxLjU5OS0uMjAxLjI1NC0uMzU1LjU1Ni0uNDY1LjkwNS0uMTE1LjM1LS4xNy43MjYtLjE3IDEuMTM0IDAgLjM0NC4wNDUuNjQ1LjEzNS45MDEuMDg4LjI2LjIxMS40NzMuMzU5LjY0Ni4xNTMuMTcxLjMyOS4zLjUzNC4zODIuMi4wODYuNDE1LjEyOS42NDEuMTI5LjE3NiAwIC4zNDItLjAyNy40OTktLjA4MS4xNTQtLjA1Mi4zMDItLjEzLjQzMi0uMjMyLjEzNC0uMTA0LjI0OC0uMjMuMzQ4LS4zOC4xMDItLjE0OS4xODItLjMyMy4yMzYtLjUyaC4wMjdjMCAuMzc2LjEwMS42NzQuMzA3Ljg5My4yMDcuMjIuNTAyLjMzLjg4OS4zMy4yOTIgMCAuNTgtLjA2NC44NjMtLjE5OC4yODMtLjEzMi41MzYtLjMyOC43NjItLjU4Ni4yMjMtLjI2Mi40MDQtLjU4My41NDMtLjk2Ni4xMzgtLjM4NS4yMDgtLjgzMS4yMDgtMS4zNDEgMC0uNjA1LS4xMTctMS4xNS0uMzQ1LTEuNjM0LS4yMzEtLjQ4Mi0uNTQ2LS44OTEtLjkzOS0xLjIyNW0tMi4zNjggMy43NzRjLS4wNTYuMjc3LS4xMzYuNTE3LS4yNDYuNzE5LS4xMDkuMjAzLS4yNDYuMzYzLS40MDcuNDgxLS4xNjMuMTE1LS4zNTQuMTc2LS41NzIuMTc2LS4xMiAwLS4yMzYtLjAyNS0uMzQ0LS4wNzgtLjEwOC0uMDUyLS4yMDYtLjEzLS4yODktLjIzMi0uMDgxLS4xMDMtLjE0OC0uMjM0LS4xOTgtLjM5LS4wNDYtLjE1Ni0uMDctLjMzNy0uMDctLjU0NyAwLS4yMzcuMDI3LS40ODEuMDgtLjcyOS4wNTYtLjI0Ny4xMzctLjQ3My4yNS0uNjc3LjEwOS0uMi4yNS0uMzYzLjQxNi0uNDkyLjE2NS0uMTI3LjM2MS0uMTkxLjU4Mi0uMTkxLjEyMyAwIC4yMzQuMDIxLjM0LjA2My4xMDcuMDQyLjE5OC4xMDcuMjc5LjE5Ni4wOC4wODcuMTQ1LjE5Ny4xODkuMzMuMDQzLjEzNC4wNy4yOTQuMDcuNDggMCAuMzE3LS4wMzEuNjE1LS4wOC44OTEiIGZpbGw9IiM0MjQyNDIiLz48cGF0aCBkPSJNMjY0IDM3di0xNGg4LjYyNWwzLjM3NSAzLjU1NnYxMC40NDRoLTEyeiIgZmlsbD0iIzJEMkQyRCIvPjxwYXRoIGQ9Ik0yNzIgMjRoLTd2MTJoMTB2LTlsLTMtM3ptMiAxMWgtOHYtMTBoNXYzaDN2N3oiIGZpbGw9IiNDNUM1QzUiLz48cG9seWdvbiBwb2ludHM9IjI2NiwyNSAyNzEsMjUgMjcxLDI4IDI3NCwyOCAyNzQsMzUgMjY2LDM1IiBmaWxsPSIjMkQyRDJEIi8+PHBhdGggZD0iTTI2NCAxN3YtMTRoOC42MjVsMy4zNzUgMy41NTZ2MTAuNDQ0aC0xMnoiIGZpbGw9IiNGM0YzRjMiLz48cGF0aCBkPSJNMjcyIDRoLTd2MTJoMTB2LTlsLTMtM3ptMiAxMWgtOHYtMTBoNXYzaDN2N3oiIGZpbGw9IiM0MjQyNDIiLz48cG9seWdvbiBwb2ludHM9IjI2Niw1IDI3MSw1IDI3MSw4IDI3NCw4IDI3NCwxNSAyNjYsMTUiIGZpbGw9IiNGMEVGRjEiLz48cG9seWdvbiBwb2ludHM9IjI0NywzNCAyNDcsMzAgMjQ1LDMwIDI0NSwyNiAyNTUsMjYgMjU1LDM0IiBmaWxsPSIjMkQyRDJEIi8+PHBhdGggZD0iTTI1NCAyOWgtOHYtMmg4djJ6bTAgMWgtNnYxaDZ2LTF6bTAgMmgtNnYxaDZ2LTF6IiBmaWxsPSIjQzVDNUM1Ii8+PHBvbHlnb24gcG9pbnRzPSIyNDcsMTQgMjQ3LDEwIDI0NSwxMCAyNDUsNiAyNTUsNiAyNTUsMTQiIGZpbGw9IiNGM0YzRjMiLz48cGF0aCBkPSJNMjU0IDloLTh2LTJoOHYyem0wIDFoLTZ2MWg2di0xem0wIDJoLTZ2MWg2di0xeiIgZmlsbD0iIzQyNDI0MiIvPjxwYXRoIGQ9Ik0yMzAuNSAyMmMtNC4xNDMgMC03LjUgMy4zNTctNy41IDcuNXMzLjM1NyA3LjUgNy41IDcuNSA3LjUtMy4zNTcgNy41LTcuNS0zLjM1Ny03LjUtNy41LTcuNXptMCAxMWMtMS45MzMgMC0zLjUtMS41NjYtMy41LTMuNXMxLjU2Ny0zLjUgMy41LTMuNSAzLjUgMS41NjYgMy41IDMuNS0xLjU2NyAzLjUtMy41IDMuNXoiIGZpbGw9IiMyRDJEMkQiLz48cGF0aCBkPSJNMjI0LjAyNSAyOWMuMTA4LTEuNDE4LjY2OS0yLjcwOCAxLjU0Mi0zLjcyNmwxLjQzMSAxLjQzMWMtLjUxNi42NDYtLjg1MSAxLjQzLS45NDcgMi4yOTVoLTIuMDI2em0yLjk3MyAzLjI5NWMtLjUxNi0uNjQ2LS44NTEtMS40My0uOTQ3LTIuMjk1aC0yLjAyNWMuMTA4IDEuNDE4LjY2OSAyLjcwNyAxLjU0MiAzLjcyNmwxLjQzLTEuNDMxem00LjAwMi05LjI3djIuMDI1Yy44NjUuMDk3IDEuNjQ5LjQzMiAyLjI5NS45NDdsMS40MzEtMS40MzFjLTEuMDE4LS44NzItMi4zMDgtMS40MzItMy43MjYtMS41NDF6bS0zLjI5NSAyLjk3M2MuNjQ2LS41MTYgMS40My0uODUxIDIuMjk1LS45NDd2LTIuMDI1Yy0xLjQxOC4xMDgtMi43MDguNjY5LTMuNzI2IDEuNTQybDEuNDMxIDEuNDN6bTYuMjk3LjcwN2MuNTE2LjY0Ni44NTEgMS40My45NDcgMi4yOTVoMi4wMjVjLS4xMDgtMS40MTgtLjY2OS0yLjcwOC0xLjU0Mi0zLjcyNmwtMS40MyAxLjQzMXptLTQuMDAyIDcuMjQ0Yy0uODY1LS4wOTctMS42NDktLjQzMi0yLjI5NS0uOTQ3bC0xLjQzMSAxLjQzMWMxLjAxOC44NzMgMi4zMDcgMS40MzQgMy43MjYgMS41NDJ2LTIuMDI2em00Ljk0OS0zLjk0OWMtLjA5Ny44NjUtLjQzMiAxLjY0OC0uOTQ3IDIuMjk1bDEuNDMxIDEuNDMxYy44NzMtMS4wMTkgMS40MzQtMi4zMDggMS41NDItMy43MjZoLTIuMDI2em0tMS42NTQgMy4wMDJjLS42NDYuNTE2LTEuNDMuODUxLTIuMjk1Ljk0N3YyLjAyNWMxLjQxOS0uMTA4IDIuNzA4LS42NjkgMy43MjYtMS41NDJsLTEuNDMxLTEuNDN6IiBmaWxsPSIjQzVDNUM1Ii8+PHBhdGggZD0iTTIzMC41IDJjLTQuMTQzIDAtNy41IDMuMzU4LTcuNSA3LjUgMCA0LjE0MyAzLjM1NyA3LjUgNy41IDcuNXM3LjUtMy4zNTcgNy41LTcuNWMwLTQuMTQyLTMuMzU3LTcuNS03LjUtNy41em0wIDExYy0xLjkzMyAwLTMuNS0xLjU2Ni0zLjUtMy41IDAtMS45MzMgMS41NjctMy41IDMuNS0zLjVzMy41IDEuNTY3IDMuNSAzLjVjMCAxLjkzNC0xLjU2NyAzLjUtMy41IDMuNXoiIGZpbGw9IiNGM0YzRjMiLz48cGF0aCBkPSJNMjI0LjAyNSA5Yy4xMDgtMS40MTguNjY5LTIuNzA4IDEuNTQyLTMuNzI2bDEuNDMxIDEuNDMxYy0uNTE2LjY0Ni0uODUxIDEuNDMtLjk0NyAyLjI5NGgtMi4wMjZ6bTIuOTczIDMuMjk1Yy0uNTE2LS42NDYtLjg1MS0xLjQzLS45NDctMi4yOTVoLTIuMDI1Yy4xMDggMS40MTguNjY5IDIuNzA3IDEuNTQyIDMuNzI2bDEuNDMtMS40MzF6bTQuMDAyLTkuMjd2Mi4wMjVjLjg2NS4wOTcgMS42NDkuNDMyIDIuMjk1Ljk0OGwxLjQzMS0xLjQzMWMtMS4wMTgtLjg3My0yLjMwOC0xLjQzMy0zLjcyNi0xLjU0MnptLTMuMjk1IDIuOTc0Yy42NDYtLjUxNiAxLjQzLS44NTEgMi4yOTUtLjk0OHYtMi4wMjZjLTEuNDE4LjEwOC0yLjcwOC42NjktMy43MjYgMS41NDJsMS40MzEgMS40MzJ6bTYuMjk3LjcwN2MuNTE2LjY0Ni44NTEgMS40My45NDcgMi4yOTRoMi4wMjVjLS4xMDgtMS40MTgtLjY2OS0yLjcwOC0xLjU0Mi0zLjcyNmwtMS40MyAxLjQzMnptLTQuMDAyIDcuMjQzYy0uODY1LS4wOTctMS42NDktLjQzMi0yLjI5NS0uOTQ3bC0xLjQzMSAxLjQzMWMxLjAxOC44NzMgMi4zMDcgMS40MzQgMy43MjYgMS41NDJ2LTIuMDI2em00Ljk0OS0zLjk0OWMtLjA5Ny44NjUtLjQzMiAxLjY0OC0uOTQ3IDIuMjk1bDEuNDMxIDEuNDMxYy44NzMtMS4wMTkgMS40MzQtMi4zMDggMS41NDItMy43MjZoLTIuMDI2em0tMS42NTQgMy4wMDJjLS42NDYuNTE2LTEuNDMuODUxLTIuMjk1Ljk0N3YyLjAyNWMxLjQxOS0uMTA4IDIuNzA4LS42NjkgMy43MjYtMS41NDJsLTEuNDMxLTEuNDN6IiBmaWxsPSIjNDI0MjQyIi8+PHJlY3QgeD0iMjAyIiB5PSIyMyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE0IiBmaWxsPSIjMkQyRDJEIi8+PHBhdGggZD0iTTIwMyAyNHYxMmgxNHYtMTJoLTE0em0xMyAxMWgtMTJ2LTEwaDEydjEwem0tNi03di0xaC0xdjVoM3YtNGgtMnptMSAzaC0xdi0yaDF2MnptMy0ydjJoMXYxaC0ydi00aDJ2MWgtMXptLTYtMXY0aC0zdi0yaDF2MWgxdi0xaC0xdi0xaC0xdi0xaDN6IiBmaWxsPSIjQzVDNUM1Ii8+PHBhdGggZD0iTTIxMCAyOWgxdjJoLTF2LTJ6bS0zIDJ2LTFoLTF2MWgxem05LTZ2MTBoLTEydi0xMGgxMnptLTggM2gtM3YxaDF2MWgtMXYyaDN2LTR6bTQgMGgtMnYtMWgtMXY1aDN2LTR6bTMgMGgtMnY0aDJ2LTFoLTF2LTJoMXYtMXoiIGZpbGw9IiMyRDJEMkQiLz48cmVjdCB4PSIyMDIiIHk9IjMiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNCIgZmlsbD0iI0YzRjNGMyIvPjxwYXRoIGQ9Ik0yMDMgNHYxMmgxNHYtMTJoLTE0em0xMyAxMWgtMTJ2LTEwaDEydjEwem0tNi03di0xaC0xdjVoM3YtNGgtMnptMSAzaC0xdi0yaDF2MnptMy0ydjJoMXYxaC0ydi00aDJ2MWgtMXptLTYtMXY0aC0zdi0yaDF2MWgxdi0xaC0xdi0xaC0xdi0xaDN6IiBmaWxsPSIjNDI0MjQyIi8+PHBhdGggZD0iTTIxMCA5aDF2MmgtMXYtMnptLTMgMnYtMWgtMXYxaDF6bTktNnYxMGgtMTJ2LTEwaDEyem0tOCAzaC0zdjFoMXYxaC0xdjJoM3YtNHptNCAwaC0ydi0xaC0xdjVoM3YtNHptMyAwaC0ydjRoMnYtMWgtMXYtMmgxdi0xeiIgZmlsbD0iI0YwRUZGMSIvPjxwYXRoIGQ9Ik0xOTYuNjUyIDMyLjVjLjgxMS0uNTM3IDEuMzQ4LTEuNDU3IDEuMzQ4LTIuNSAwLTEuNjU0LTEuMzQ2LTMtMy0zLS43NzEgMC0xLjQ2OC4zMDEtMiAuNzc5di01Ljc3OWgtMTF2MTJoMy43NjRsLTEuNDUyLjcyNyAxLjQ4MSAxLjQ4Yy4zMjIuMzIyLjgwMy41IDEuMzU0LjUuNDM2IDAgLjg5Ny0uMTExIDEuMzAxLS4zMTNsMy4xNDQtMS41NzJjLjEzNC4wNTMuMjcxLjA5OC40MTQuMTI3bC0uMDA1LjA1MWMwIDEuNjU0IDEuMzQ2IDMgMyAzczMtMS4zNDYgMy0zYy0uMDAxLTEuMDQzLS41MzgtMS45NjMtMS4zNDktMi41eiIgZmlsbD0iIzJEMkQyRCIvPjxwYXRoIGQ9Ik0xOTUgMzNjLS4yOTMgMC0uNTY5LjA2Ni0uODIuMThsLS4yNS0uMjVjLjA0Mi0uMTM3LjA3LS4yNzkuMDctLjQzcy0uMDI4LS4yOTMtLjA3LS40M2wuMjUtLjI1Yy4yNTEuMTEzLjUyNy4xOC44Mi4xOCAxLjEwNCAwIDItLjg5NiAyLTIgMC0xLjEwNS0uODk2LTItMi0ycy0yIC44OTUtMiAyYzAgLjI5My4wNjYuNTY4LjE4LjgybC0uMjUuMjVjLS4xMzctLjA0My0uMjc5LS4wNy0uNDMtLjA3LS4zMzcgMC0uNjQ1LjExNS0uODk1LjMwM2wtMi42MDctMS4zMDUtLjk5OS0uNWMtLjU1Mi0uMjc1LTEuMjIzLS4yNzUtMS40OTkuMDAybC0uNS41IDUgMi41LTUgMi41LjUuNWMuMjc2LjI3NS45NDcuMjc1IDEuNSAwbDEtLjUgMi42MDUtMS4zMDNjLjI1LjE4OC41NTguMzAzLjg5NS4zMDMuMTUgMCAuMjkzLS4wMjkuNDMtLjA3bC4yNS4yNWMtLjExNC4yNS0uMTguNTI3LS4xOC44MiAwIDEuMTA0Ljg5NiAyIDIgMnMyLS44OTYgMi0yYzAtMS4xMDUtLjg5Ni0yLTItMnptMC00Yy41NTMgMCAxIC40NDcgMSAxIDAgLjU1MS0uNDQ3IDEtMSAxcy0xLS40NDktMS0xYzAtLjU1My40NDctMSAxLTF6bS0yLjUgNGMtLjI3NiAwLS41LS4yMjUtLjUtLjUgMC0uMjc3LjIyNC0uNS41LS41cy41LjIyMy41LjVjMCAuMjc1LS4yMjQuNS0uNS41em0yLjUgM2MtLjU1MyAwLTEtLjQ0OS0xLTEgMC0uNTUzLjQ0Ny0xIDEtMXMxIC40NDcgMSAxYzAgLjU1MS0uNDQ3IDEtMSAxem0tMy0xM3Y3LjA1MWMtLjE0Mi4wMjktLjI3OS4wNy0uNDEzLjEyM2wtLjU4Ny0uMTc0di02aC03djdoLTF2LThoOXptLTggMTBoLTF2LTFoMXYxem0yLTFoLTF2MWgxdi0xem0yIDBoLTF2MWgxdi0xeiIgZmlsbD0iI0M1QzVDNSIvPjxwYXRoIGQ9Ik0xODUuNzkzIDI4Ljc5M2wtMS43OTMgMS4yMDd2LTZoN3Y1LjM4MWwtMi41NTQtLjc3N2MtLjgxNi0uNDA5LTEuOTktLjQ3NS0yLjY1My4xODl6bS0uNzkzIDIuMjA3aC43NjRsLS43NjQtLjM4M3YuMzgzem0xMSA0YzAgLjU1MS0uNDQ3IDEtMSAxcy0xLS40NDktMS0xYzAtLjU1My40NDctMSAxLTFzMSAuNDQ3IDEgMXptLTMuNS0zYy0uMjc2IDAtLjUuMjIzLS41LjUgMCAuMjc1LjIyNC41LjUuNXMuNS0uMjI1LjUtLjVjMC0uMjc3LS4yMjQtLjUtLjUtLjV6bTIuNS0zYy0uNTUzIDAtMSAuNDQ3LTEgMSAwIC41NTEuNDQ3IDEgMSAxczEtLjQ0OSAxLTFjMC0uNTUzLS40NDctMS0xLTF6IiBmaWxsPSIjMkQyRDJEIi8+PHBhdGggZD0iTTE5Ni42NTIgMTIuNWMuODExLS41MzggMS4zNDgtMS40NTggMS4zNDgtMi41IDAtMS42NTQtMS4zNDYtMy0zLTMtLjc3MSAwLTEuNDY4LjMwMS0yIC43Nzl2LTUuNzc5aC0xMXYxMmgzLjc2NGwtMS40NTIuNzI3IDEuNDgxIDEuNDhjLjMyMi4zMjIuODAzLjUgMS4zNTQuNS40MzYgMCAuODk3LS4xMTEgMS4zMDEtLjMxM2wzLjE0NC0xLjU3MmMuMTM0LjA1My4yNzEuMDk4LjQxNC4xMjdsLS4wMDUuMDUxYzAgMS42NTQgMS4zNDYgMyAzIDNzMy0xLjM0NiAzLTNjLS4wMDEtMS4wNDMtLjUzOC0xLjk2My0xLjM0OS0yLjV6IiBmaWxsPSIjRjNGM0YzIi8+PHBhdGggZD0iTTE5NSAxM2MtLjI5MyAwLS41NjkuMDY2LS44Mi4xOGwtLjI1LS4yNWMuMDQyLS4xMzcuMDctLjI3OS4wNy0uNDNzLS4wMjgtLjI5My0uMDctLjQzbC4yNS0uMjVjLjI1MS4xMTMuNTI3LjE4LjgyLjE4IDEuMTA0IDAgMi0uODk2IDItMiAwLTEuMTA1LS44OTYtMi0yLTJzLTIgLjg5NS0yIDJjMCAuMjkzLjA2Ni41NjguMTguODJsLS4yNS4yNWMtLjEzNy0uMDQzLS4yNzktLjA3LS40My0uMDctLjMzNyAwLS42NDUuMTE1LS44OTUuMzAzbC0yLjYwNy0xLjMwNC0uOTk5LS41Yy0uNTUyLS4yNzUtMS4yMjMtLjI3NS0xLjQ5OS4wMDJsLS41LjQ5OSA1IDIuNS01IDIuNS41LjVjLjI3Ni4yNzUuOTQ3LjI3NSAxLjUgMGwxLS41IDIuNjA1LTEuMzAzYy4yNS4xODguNTU4LjMwMy44OTUuMzAzLjE1IDAgLjI5My0uMDI5LjQzLS4wN2wuMjUuMjVjLS4xMTMuMjUtLjE4LjUyNy0uMTguODIgMCAxLjEwNC44OTYgMiAyIDJzMi0uODk2IDItMmMwLTEuMTA2LS44OTYtMi0yLTJ6bTAtNGMuNTUzIDAgMSAuNDQ3IDEgMSAwIC41NTEtLjQ0NyAxLTEgMXMtMS0uNDQ5LTEtMWMwLS41NTMuNDQ3LTEgMS0xem0tMi41IDRjLS4yNzYgMC0uNS0uMjI1LS41LS41IDAtLjI3Ny4yMjQtLjUuNS0uNXMuNS4yMjMuNS41YzAgLjI3NS0uMjI0LjUtLjUuNXptMi41IDNjLS41NTMgMC0xLS40NDktMS0xIDAtLjU1My40NDctMSAxLTFzMSAuNDQ3IDEgMWMwIC41NS0uNDQ3IDEtMSAxem0tMy0xM3Y3LjA1MWMtLjE0Mi4wMjktLjI3OS4wNy0uNDEzLjEyM2wtLjU4Ny0uMTc0di02aC03djdoLTF2LThoOXptLTggMTBoLTF2LTFoMXYxem0yLTFoLTF2MWgxdi0xem0yIDBoLTF2MWgxdi0xeiIgZmlsbD0iIzQyNDI0MiIvPjxwYXRoIGQ9Ik0xODUuNzkzIDguNzkzbC0xLjc5MyAxLjIwN3YtNmg3djUuMzgxbC0yLjU1NC0uNzc3Yy0uODE2LS40MDktMS45OS0uNDc1LTIuNjUzLjE4OXptLS43OTMgMi4yMDdoLjc2NGwtLjc2NC0uMzgzdi4zODN6bTExIDRjMCAuNTUxLS40NDcgMS0xIDFzLTEtLjQ0OS0xLTFjMC0uNTUzLjQ0Ny0xIDEtMXMxIC40NDcgMSAxem0tMy41LTNjLS4yNzYgMC0uNS4yMjMtLjUuNSAwIC4yNzUuMjI0LjUuNS41cy41LS4yMjUuNS0uNWMwLS4yNzgtLjIyNC0uNS0uNS0uNXptMi41LTNjLS41NTMgMC0xIC40NDctMSAxIDAgLjU1MS40NDcgMSAxIDFzMS0uNDQ5IDEtMWMwLS41NTMtLjQ0Ny0xLTEtMXoiIGZpbGw9IiNGMEVGRjEiLz48cGF0aCBkPSJNMTc4IDI3di0zaC03di0xaC05djE0aDEzdi0zaDN2LTNoLTF2LTNoLTZ2LTFoN3ptLTggN3YtM2gxdjNoLTF6IiBmaWxsPSIjMkQyRDJEIi8+PHBhdGggZD0iTTE3NyAyNmgtNXYtMWg1djF6bS0xIDNoLTJ2MWgydi0xem0tNCAwaC05djFoOXYtMXptMiA2aC0xMXYxaDExdi0xem0tNS0zaC02djFoNnYtMXptOCAwaC01djFoNXYtMXptLTctOHYzaC03di0zaDd6bS0xIDFoLTV2MWg1di0xeiIgZmlsbD0iI0M1QzVDNSIvPjxyZWN0IHg9IjE2NCIgeT0iMjUiIHdpZHRoPSI1IiBoZWlnaHQ9IjEiIGZpbGw9IiMyRDJEMkQiLz48cGF0aCBkPSJNMTc4IDd2LTNoLTd2LTFoLTl2MTRoMTN2LTNoM3YtM2gtMXYtM2gtNnYtMWg3em0tOCA3di0zaDF2M2gtMXoiIGZpbGw9IiNGM0YzRjMiLz48cGF0aCBkPSJNMTc3IDZoLTV2LTFoNXYxem0tMSAzaC0ydjFoMnYtMXptLTQgMGgtOXYxaDl2LTF6bTIgNmgtMTF2MWgxMXYtMXptLTUtM2gtNnYxaDZ2LTF6bTggMGgtNXYxaDV2LTF6bS03LTh2M2gtN3YtM2g3em0tMSAxaC01djFoNXYtMXoiIGZpbGw9IiM0MjQyNDIiLz48cmVjdCB4PSIxNjQiIHk9IjUiIHdpZHRoPSI1IiBoZWlnaHQ9IjEiIGZpbGw9IiNGMEVGRjEiLz48cG9seWdvbiBwb2ludHM9IjE1NC40MTQsMjQgMTQ5LjU4NiwyNCAxNDgsMjUuNTg2IDE0OCwyOCAxNDQsMjggMTQ0LDM1IDE1MiwzNSAxNTIsMzEgMTU0LjQxNCwzMSAxNTYsMjkuNDE0IDE1NiwyNS41ODYiIGZpbGw9IiMyRDJEMkQiLz48ZyBmaWxsPSIjNzVCRUZGIj48cGF0aCBkPSJNMTU0IDI1aC00bC0xIDF2Mmg1djFoLTJ2MWgybDEtMXYtM2wtMS0xem0wIDJoLTR2LTFoNHYxek0xNDUgMzRoNnYtNWgtNnY1em0xLTNoNHYxaC00di0xeiIvPjwvZz48ZyBmaWxsPSIjMkQyRDJEIj48cmVjdCB4PSIxNDYiIHk9IjMxIiB3aWR0aD0iNCIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMTUwIiB5PSIyNiIgd2lkdGg9IjQiIGhlaWdodD0iMSIvPjxyZWN0IHg9IjE1MiIgeT0iMjgiIHdpZHRoPSIyIiBoZWlnaHQ9IjEiLz48L2c+PHBvbHlnb24gcG9pbnRzPSIxNTQuNDE0LDQgMTQ5LjU4Niw0IDE0OCw1LjU4NiAxNDgsOCAxNDQsOCAxNDQsMTUgMTUyLDE1IDE1MiwxMSAxNTQuNDE0LDExIDE1Niw5LjQxNCAxNTYsNS41ODYiIGZpbGw9IiNGM0YzRjMiLz48ZyBmaWxsPSIjMDA1MzlDIj48cGF0aCBkPSJNMTU0IDVoLTRsLTEgMXYyaDV2MWgtMnYxaDJsMS0xdi0zbC0xLTF6bTAgMmgtNHYtMWg0djF6TTE0NSAxNGg2di01aC02djV6bTEtM2g0djFoLTR2LTF6Ii8+PC9nPjxnIGZpbGw9IiNGMEVGRjEiPjxyZWN0IHg9IjE0NiIgeT0iMTEiIHdpZHRoPSI0IiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIxNTAiIHk9IjYiIHdpZHRoPSI0IiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIxNTIiIHk9IjgiIHdpZHRoPSIyIiBoZWlnaHQ9IjEiLz48L2c+PHBhdGggZD0iTTEzOCAyNGgtMTV2NGgtMXY4aDh2LTZoOHYtNnptLTExIDloLTJ2LTJoMnYyeiIgZmlsbD0iIzJEMkQyRCIvPjxwYXRoIGQ9Ik0xMzcgMjloLTd2LTFoLTZ2LTNoMXYyaDF2LTJoMXYyaDF2LTJoMXYyaDF2LTJoMXYyaDF2LTJoMXYyaDF2LTJoMXYyaDF2LTJoMXY0em0tMTIgMXYtMWgtMnY2aDJ2LTFoLTF2LTRoMXptMiA0djFoMnYtNmgtMnYxaDF2NGgtMXoiIGZpbGw9IiNDNUM1QzUiLz48cGF0aCBkPSJNMTI1IDI3di0yaDF2MmgtMXptMyAwdi0yaC0xdjJoMXptMiAwdi0yaC0xdjJoMXptMiAwdi0yaC0xdjJoMXptMiAwdi0yaC0xdjJoMXptMiAwdi0yaC0xdjJoMXoiIGZpbGw9IiMyRDJEMkQiLz48cGF0aCBkPSJNMTM4IDRoLTE1djRoLTF2OGg4di02aDh2LTZ6bS0xMSA5aC0ydi0yaDJ2MnoiIGZpbGw9IiNGM0YzRjMiLz48cGF0aCBkPSJNMTM3IDloLTd2LTFoLTZ2LTNoMXYyaDF2LTJoMXYyaDF2LTJoMXYyaDF2LTJoMXYyaDF2LTJoMXYyaDF2LTJoMXYyaDF2LTJoMXY0em0tMTIgMXYtMWgtMnY2aDJ2LTFoLTF2LTRoMXptMiA0djFoMnYtNmgtMnYxaDF2NGgtMXoiIGZpbGw9IiM0MjQyNDIiLz48cGF0aCBkPSJNMTI1IDd2LTJoMXYyaC0xem0zIDB2LTJoLTF2Mmgxem0yIDB2LTJoLTF2Mmgxem0yIDB2LTJoLTF2Mmgxem0yIDB2LTJoLTF2Mmgxem0yIDB2LTJoLTF2MmgxeiIgZmlsbD0iI0YwRUZGMSIvPjxwYXRoIGQ9Ik0xMTAuNDQ5IDIzYy0xLjYzNyAwLTMuMDc1Ljc5Ny0zLjk4NyAyLjAxMmwuMDAxLjAwMmMtLjYyOC44MzYtMS4wMTQgMS44NjMtMS4wMTQgMi45ODYgMCAuNDY5LjA2Ny45MzMuMiAxLjM4NWwtMi45MDcgMi45MDhjLS42ODcuNjg2LTEuMjUzIDIuMTYxIDAgMy40MTQuNjA5LjYwOSAxLjI0NC43MzYgMS42Ny43MzYuOTU4IDAgMS42MjEtLjYxMyAxLjc0NC0uNzM2bDIuOTA3LTIuOTA4Yy40NTMuMTMzLjkxNy4yMDEgMS4zODYuMjAxIDEuMTIzIDAgMi4xNDktLjM4NyAyLjk4NS0xLjAxNGwuMDAyLjAwMWMxLjIxNi0uOTEyIDIuMDEzLTIuMzUyIDIuMDEzLTMuOTg3IDAtMi43NjItMi4yMzgtNS01LTV6IiBmaWxsPSIjMkQyRDJEIi8+PHBhdGggZD0iTTExNC4wOSAyNi4zNTlsLTIuNjQxIDIuNjQxLTItMiAyLjY0MS0yLjY0MWMtLjUwMi0uMjI3LTEuMDU1LS4zNTktMS42NDEtLjM1OS0yLjIwOSAwLTQgMS43OTEtNCA0IDAgLjU4Ni4xMzMgMS4xMzkuMzU5IDEuNjRsLTMuMzU5IDMuMzZzLTEgMSAwIDJoMmwzLjM1OS0zLjM2Yy41MDIuMjI3IDEuMDU1LjM2IDEuNjQxLjM2IDIuMjA5IDAgNC0xLjc5MSA0LTQgMC0uNTg2LS4xMzMtMS4xMzktLjM1OS0xLjY0MXoiIGZpbGw9IiNDNUM1QzUiLz48cGF0aCBkPSJNMTEwLjQ0OSAzYy0xLjYzNyAwLTMuMDc1Ljc5Ny0zLjk4NyAyLjAxMmwuMDAxLjAwMmMtLjYyOC44MzYtMS4wMTQgMS44NjMtMS4wMTQgMi45ODYgMCAuNDY5LjA2Ny45MzMuMiAxLjM4NWwtMi45MDcgMi45MDhjLS42ODcuNjg2LTEuMjUzIDIuMTYxIDAgMy40MTQuNjA5LjYwOSAxLjI0NC43MzYgMS42Ny43MzYuOTU4IDAgMS42MjEtLjYxMyAxLjc0NC0uNzM2bDIuOTA3LTIuOTA4Yy40NTMuMTMzLjkxNy4yMDEgMS4zODYuMjAxIDEuMTIzIDAgMi4xNDktLjM4NyAyLjk4NS0xLjAxNGwuMDAyLjAwMWMxLjIxNi0uOTEyIDIuMDEzLTIuMzUyIDIuMDEzLTMuOTg3IDAtMi43NjItMi4yMzgtNS01LTV6IiBmaWxsPSIjRjNGM0YzIi8+PHBhdGggZD0iTTExNC4wOSA2LjM1OWwtMi42NDEgMi42NDEtMi0yIDIuNjQxLTIuNjQxYy0uNTAyLS4yMjYtMS4wNTUtLjM1OS0xLjY0MS0uMzU5LTIuMjA5IDAtNCAxLjc5MS00IDQgMCAuNTg2LjEzMyAxLjEzOS4zNTkgMS42NGwtMy4zNTkgMy4zNnMtMSAxIDAgMmgybDMuMzU5LTMuMzZjLjUwMi4yMjcgMS4wNTUuMzYgMS42NDEuMzYgMi4yMDkgMCA0LTEuNzkxIDQtNCAwLS41ODYtLjEzMy0xLjEzOS0uMzU5LTEuNjQxeiIgZmlsbD0iIzQyNDI0MiIvPjxwYXRoIGQ9Ik04OSAzM2gxdi0xYzAtLjUzNy43NDEtMS42MTMgMS0yLS4yNTktLjM4OS0xLTEuNDY3LTEtMnYtMWgtMXYtM2gxYzEuOTY5LjAyMSAzIDEuMjc3IDMgM3YxbDEgMXYybC0xIDF2MWMwIDEuNzA5LTEuMDMxIDIuOTc5LTMgM2gtMXYtM3ptLTIgMGgtMXYtMWMwLS41MzctLjc0MS0xLjYxMy0xLTIgLjI1OS0uMzg5IDEtMS40NjcgMS0ydi0xaDF2LTNoLTFjLTEuOTY5LjAyMS0zIDEuMjc3LTMgM3YxbC0xIDF2MmwxIDF2MWMwIDEuNzA5IDEuMzE3IDIuOTc5IDMuMjg2IDNoLjcxNHYtM3oiIGZpbGw9IiMyRDJEMkQiLz48cGF0aCBkPSJNOTEgMzN2LTFjMC0uODM0LjQ5Ni0xLjczOCAxLTItLjUwNC0uMjctMS0xLjE2OC0xLTJ2LTFjMC0uODQtLjU4NC0xLTEtMXYtMWMyLjA4MyAwIDIgMS4xNjYgMiAydjFjMCAuOTY5LjcwMy45OCAxIDF2MmMtLjMyMi4wMi0xIC4wNTMtMSAxdjFjMCAuODM0LjA4MyAyLTIgMnYtMWMuODMzIDAgMS0xIDEtMXptLTYgMHYtMWMwLS44MzQtLjQ5Ni0xLjczOC0xLTIgLjUwNC0uMjcgMS0xLjE2OCAxLTJ2LTFjMC0uODQuNTg0LTEgMS0xdi0xYy0yLjA4MyAwLTIgMS4xNjYtMiAydjFjMCAuOTY5LS43MDMuOTgtMSAxdjJjLjMyMi4wMiAxIC4wNTMgMSAxdjFjMCAuODM0LS4wODMgMiAyIDJ2LTFjLS44MzMgMC0xLTEtMS0xeiIgZmlsbD0iI0M1QzVDNSIvPjxwYXRoIGQ9Ik04OSAxM2gxdi0xYzAtLjUzNy43NDEtMS42MTMgMS0yLS4yNTktLjM4OS0xLTEuNDY3LTEtMnYtMWgtMXYtM2gxYzEuOTY5LjAyMSAzIDEuMjc3IDMgM3YxbDEgMXYybC0xIDF2MWMwIDEuNzA5LTEuMDMxIDIuOTc5LTMgM2gtMXYtM3ptLTIgMGgtMXYtMWMwLS41MzctLjc0MS0xLjYxMy0xLTIgLjI1OS0uMzg5IDEtMS40NjcgMS0ydi0xaDF2LTNoLTFjLTEuOTY5LjAyMS0zIDEuMjc3LTMgM3YxbC0xIDF2MmwxIDF2MWMwIDEuNzA5IDEuMzE3IDIuOTc5IDMuMjg2IDNoLjcxNHYtM3oiIGZpbGw9IiNGM0YzRjMiLz48cGF0aCBkPSJNOTEgMTN2LTFjMC0uODM0LjQ5Ni0xLjczOCAxLTItLjUwNC0uMjctMS0xLjE2OC0xLTJ2LTFjMC0uODQtLjU4NC0xLTEtMXYtMWMyLjA4MyAwIDIgMS4xNjYgMiAydjFjMCAuOTY5LjcwMy45OCAxIDF2MmMtLjMyMi4wMi0xIC4wNTMtMSAxdjFjMCAuODM0LjA4MyAyLTIgMnYtMWMuODMzIDAgMS0xIDEtMXptLTYgMHYtMWMwLS44MzQtLjQ5Ni0xLjczOC0xLTIgLjUwNC0uMjcgMS0xLjE2OCAxLTJ2LTFjMC0uODQuNTg0LTEgMS0xdi0xYy0yLjA4MyAwLTIgMS4xNjYtMiAydjFjMCAuOTY5LS43MDMuOTgtMSAxdjJjLjMyMi4wMiAxIC4wNTMgMSAxdjFjMCAuODM0LS4wODMgMiAyIDJ2LTFjLS44MzMgMC0xLTEtMS0xeiIgZmlsbD0iIzQyNDI0MiIvPjxwYXRoIGQ9Ik03My41IDM0Yy0xLjkxNCAwLTMuNjAxLTEuMjQyLTQuMjI3LTNoLTEuNjgzYy0uNTI0LjkxLTEuNTAzIDEuNS0yLjU5MSAxLjUtMS42NTQgMC0zLTEuMzQ2LTMtM3MxLjM0Ni0zIDMtM2MxLjA4OCAwIDIuMDY2LjU4OCAyLjU5MSAxLjVoMS42ODNjLjYyNi0xLjc2IDIuMzEzLTMgNC4yMjctMyAyLjQ4MSAwIDQuNSAyLjAxOCA0LjUgNC41IDAgMi40OC0yLjAxOSA0LjUtNC41IDQuNXoiIGZpbGw9IiMyRDJEMkQiLz48cGF0aCBkPSJNNzMuNSAyNmMtMS43NTkgMC0zLjIwNCAxLjMwOC0zLjQ0OSAzaC0zLjEyMmMtLjIyMy0uODYxLS45OTgtMS41LTEuOTI5LTEuNS0xLjEwNCAwLTIgLjg5NS0yIDIgMCAxLjEwNC44OTYgMiAyIDIgLjkzMSAwIDEuNzA2LS42MzkgMS45MjktMS41aDMuMTIyYy4yNDUgMS42OTEgMS42OSAzIDMuNDQ5IDMgMS45MyAwIDMuNS0xLjU3IDMuNS0zLjUgMC0xLjkzMS0xLjU3LTMuNS0zLjUtMy41em0wIDVjLS44MjcgMC0xLjUtLjY3NC0xLjUtMS41IDAtLjgyOC42NzMtMS41IDEuNS0xLjVzMS41LjY3MiAxLjUgMS41YzAgLjgyNi0uNjczIDEuNS0xLjUgMS41eiIgZmlsbD0iIzc1QkVGRiIvPjxjaXJjbGUgY3g9IjczLjUiIGN5PSIyOS41IiByPSIxLjUiIGZpbGw9IiMyRDJEMkQiLz48cGF0aCBkPSJNNzMuNSAxNGMtMS45MTQgMC0zLjYwMS0xLjI0Mi00LjIyNy0zaC0xLjY4M2MtLjUyNC45MS0xLjUwMyAxLjUtMi41OTEgMS41LTEuNjU0IDAtMy0xLjM0Ni0zLTNzMS4zNDYtMyAzLTNjMS4wODggMCAyLjA2Ni41ODggMi41OTEgMS41aDEuNjgzYy42MjYtMS43NiAyLjMxMy0zIDQuMjI3LTMgMi40ODEgMCA0LjUgMi4wMTggNC41IDQuNSAwIDIuNDgtMi4wMTkgNC41LTQuNSA0LjV6IiBmaWxsPSIjRjNGM0YzIi8+PHBhdGggZD0iTTczLjUgNmMtMS43NTkgMC0zLjIwNCAxLjMwOC0zLjQ0OSAzaC0zLjEyMmMtLjIyMy0uODYxLS45OTgtMS41LTEuOTI5LTEuNS0xLjEwNCAwLTIgLjg5NS0yIDIgMCAxLjEwNC44OTYgMiAyIDIgLjkzMSAwIDEuNzA2LS42MzkgMS45MjktMS41aDMuMTIyYy4yNDUgMS42OTEgMS42OSAzIDMuNDQ5IDMgMS45MyAwIDMuNS0xLjU3IDMuNS0zLjUgMC0xLjkzMS0xLjU3LTMuNS0zLjUtMy41em0wIDVjLS44MjcgMC0xLjUtLjY3NC0xLjUtMS41IDAtLjgyOC42NzMtMS41IDEuNS0xLjVzMS41LjY3MiAxLjUgMS41YzAgLjgyNi0uNjczIDEuNS0xLjUgMS41eiIgZmlsbD0iIzAwNTM5QyIvPjxjaXJjbGUgY3g9IjczLjUiIGN5PSI5LjUiIHI9IjEuNSIgZmlsbD0iI0YwRUZGMSIvPjxwYXRoIGQ9Ik01OCAyOC41ODZsLTMtMy0xLjQxNCAxLjQxNGgtMi4xNzJsMS0xLTQtNGgtLjgyOGwtNS41ODYgNS41ODZ2LjgyOGw0IDQgMi40MTQtMi40MTRoLjU4NnY1aDEuNTg2bDMgM2guODI4bDMuNTg2LTMuNTg2di0uODI4bC0yLjA4Ni0yLjA4NiAyLjA4Ni0yLjA4NnYtLjgyOHoiIGZpbGw9IiMyRDJEMkQiLz48cG9seWdvbiBwb2ludHM9IjUzLjk5OCwzMy4wMDIgNTEsMzMgNTEsMjkgNTMsMjkgNTIsMzAgNTQsMzIgNTcsMjkgNTUsMjcgNTQsMjggNDksMjggNTEsMjYgNDgsMjMgNDMsMjggNDYsMzEgNDgsMjkgNTAsMjkgNTAsMzQgNTMsMzQgNTIsMzUgNTQsMzcgNTcsMzQgNTUsMzIiIGZpbGw9IiNDMjdEMUEiLz48cGF0aCBkPSJNNTggOC41ODZsLTMtMy0xLjQxNCAxLjQxNGgtMi4xNzJsMS0xLTQtNGgtLjgyOGwtNS41ODYgNS41ODZ2LjgyOGw0IDQgMi40MTQtMi40MTRoLjU4NnY1aDEuNTg2bDMgM2guODI4bDMuNTg2LTMuNTg2di0uODI4bC0yLjA4Ni0yLjA4NiAyLjA4Ni0yLjA4NnYtLjgyOHoiIGZpbGw9IiNGM0YzRjMiLz48cG9seWdvbiBwb2ludHM9IjUzLjk5OCwxMy4wMDIgNTEsMTMgNTEsOSA1Myw5IDUyLDEwIDU0LDEyIDU3LDkgNTUsNyA1NCw4IDQ5LDggNTEsNiA0OCwzIDQzLDggNDYsMTEgNDgsOSA1MCw5IDUwLDE0IDUzLDE0IDUyLDE1IDU0LDE3IDU3LDE0IDU1LDEyIiBmaWxsPSIjQzI3RDFBIi8+PHBhdGggZD0iTTI5LjI2MyAyNGw0LjczNyAyLjM2OXY1LjIzNmwtNi43OTEgMy4zOTVoLS40MmwtNC43ODktMi4zOTV2LTUuMjM2bDYuNzM5LTMuMzY5aC41MjR6IiBmaWxsPSIjMkQyRDJEIi8+PHBhdGggZD0iTTIzIDI4djRsNCAyIDYtM3YtNGwtNC0yLTYgM3ptNCAxbC0yLTEgNC0yIDIgMS00IDJ6IiBmaWxsPSIjNzVCRUZGIi8+PHBhdGggZD0iTTI5IDI2bDIgMS00IDItMi0xIDQtMnoiIGZpbGw9IiMyRDJEMkQiLz48cGF0aCBkPSJNMjkuMjYzIDRsNC43MzcgMi4zNjl2NS4yMzZsLTYuNzkxIDMuMzk1aC0uNDJsLTQuNzg5LTIuMzk1di01LjIzNmw2LjczOS0zLjM2OWguNTI0eiIgZmlsbD0iI0YzRjNGMyIvPjxwYXRoIGQ9Ik0yMyA4djRsNCAyIDYtM3YtNGwtNC0yLTYgM3ptNCAxbC0yLTEgNC0yIDIgMS00IDJ6IiBmaWxsPSIjMDA1MzlDIi8+PHBhdGggZD0iTTI5IDZsMiAxLTQgMi0yLTEgNC0yeiIgZmlsbD0iI0YwRUZGMSIvPjxwb2x5Z29uIHBvaW50cz0iMiwyNy4zMDggMiwzMi42OTIgNy4yMDksMzYgNy43OTEsMzYgMTMsMzIuNjkyIDEzLDI3LjMwOCA3Ljc5MSwyNCA3LjIwOSwyNCIgZmlsbD0iIzJEMkQyRCIvPjxwYXRoIGQ9Ik03LjUgMjVsLTQuNSAyLjg1N3Y0LjI4NWw0LjUgMi44NTggNC41LTIuODU3di00LjI4NWwtNC41LTIuODU4em0tLjUgOC40OThsLTMtMS45MDV2LTIuODE1bDMgMS45MDV2Mi44MTV6bS0yLjM1OC01LjQ5OGwyLjg1OC0xLjgxNSAyLjg1OCAxLjgxNS0yLjg1OCAxLjgxNS0yLjg1OC0xLjgxNXptNi4zNTggMy41OTNsLTMgMS45MDV2LTIuODE1bDMtMS45MDV2Mi44MTV6IiBmaWxsPSIjQjE4MEQ3Ii8+PHBvbHlnb24gcG9pbnRzPSIxMC4zNTgsMjggNy41LDI5LjgxNSA0LjY0MiwyOCA3LjUsMjYuMTg1IiBmaWxsPSIjMkQyRDJEIi8+PHBvbHlnb24gcG9pbnRzPSI0LDI4Ljc3NyA3LDMwLjY4MyA3LDMzLjQ5OCA0LDMxLjU5MyIgZmlsbD0iIzJEMkQyRCIvPjxwb2x5Z29uIHBvaW50cz0iOCwzMy40OTggOCwzMC42ODMgMTEsMjguNzc3IDExLDMxLjU5MyIgZmlsbD0iIzJEMkQyRCIvPjxwb2x5Z29uIHBvaW50cz0iMiw3LjMwOCAyLDEyLjY5MiA3LjIwOSwxNiA3Ljc5MSwxNiAxMywxMi42OTIgMTMsNy4zMDggNy43OTEsNCA3LjIwOSw0IiBmaWxsPSIjRjNGM0YzIi8+PHBhdGggZD0iTTcuNSA1bC00LjUgMi44NTd2NC4yODVsNC41IDIuODU4IDQuNS0yLjg1N3YtNC4yODZsLTQuNS0yLjg1N3ptLS41IDguNDk4bC0zLTEuOTA1di0yLjgxNmwzIDEuOTA1djIuODE2em0tMi4zNTgtNS40OThsMi44NTgtMS44MTUgMi44NTggMS44MTUtMi44NTggMS44MTUtMi44NTgtMS44MTV6bTYuMzU4IDMuNTkzbC0zIDEuOTA1di0yLjgxNWwzLTEuOTA1djIuODE1eiIgZmlsbD0iIzY1MkQ5MCIvPjxwb2x5Z29uIHBvaW50cz0iMTAuMzU4LDggNy41LDkuODE1IDQuNjQyLDggNy41LDYuMTg1IiBmaWxsPSIjRjBFRkYxIi8+PHBvbHlnb24gcG9pbnRzPSI0LDguNzc3IDcsMTAuNjgzIDcsMTMuNDk4IDQsMTEuNTkzIiBmaWxsPSIjRjBFRkYxIi8+PHBvbHlnb24gcG9pbnRzPSI4LDEzLjQ5OCA4LDEwLjY4MyAxMSw4Ljc3NyAxMSwxMS41OTMiIGZpbGw9IiNGMEVGRjEiLz48L3N2Zz4=);background-repeat:no-repeat}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor,.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function,.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method{background-position:0 -4px}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field,.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable{background-position:-22px -4px}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class{background-position:-43px -3px}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface{background-position:-63px -4px}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module{background-position:-82px -4px}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property{background-position:-102px -3px}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum{background-position:-122px -3px}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule{background-position:-242px -4px}.monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file{background-position:-262px -4px}.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor,.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function,.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method{background-position:0 -24px}.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field,.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable{background-position:-22px -24px}.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class{background-position:-43px -23px}.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface{background-position:-63px -24px}.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module{background-position:-82px -24px}.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property{background-position:-102px -23px}.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum{background-position:-122px -23px}.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule{background-position:-242px -24px}.vs-dark .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file{background-position:-262px -24px}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon{background:none;display:inline}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon:before{height:16px;width:16px;display:inline-block}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor:before,.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function:before,.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method:before{content:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI0IxODBENyIgZD0iTTUuNSAzbC00LjUgMi44NTd2NC4yODVsNC41IDIuODU4IDQuNS0yLjg1N3YtNC4yODZsLTQuNS0yLjg1N3ptLS41IDguNDk4bC0zLTEuOTA1di0yLjgxNmwzIDEuOTA1djIuODE2em0tMi4zNTgtNS40OThsMi44NTgtMS44MTUgMi44NTggMS44MTUtMi44NTggMS44MTUtMi44NTgtMS44MTV6bTYuMzU4IDMuNTkzbC0zIDEuOTA1di0yLjgxNWwzLTEuOTA1djIuODE1eiIvPjwvc3ZnPg==);margin-left:2px}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field:before,.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable:before{content:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iIzc1QkVGRiIgZD0iTTEgNnY0bDQgMiA2LTN2LTRsLTQtMi02IDN6bTQgMWwtMi0xIDQtMiAyIDEtNCAyeiIvPjwvc3ZnPg==);margin-left:2px}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class:before{content:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBvbHlnb24gZmlsbD0iI0U4QUI1MyIgcG9pbnRzPSIxMS45OTgsMTEuMDAyIDksMTEgOSw3IDExLDcgMTAsOCAxMiwxMCAxNSw3IDEzLDUgMTIsNiA3LDYgOSw0IDYsMSAxLDYgNCw5IDYsNyA4LDcgOCwxMiAxMSwxMiAxMCwxMyAxMiwxNSAxNSwxMiAxMywxMCIvPjwvc3ZnPg==)}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface:before{content:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iIzc1QkVGRiIgZD0iTTExLjUgNGMtMS43NTkgMC0zLjIwNCAxLjMwOC0zLjQ0OSAzaC0zLjEyMmMtLjIyMy0uODYxLS45OTgtMS41LTEuOTI5LTEuNS0xLjEwNCAwLTIgLjg5NS0yIDIgMCAxLjEwNC44OTYgMiAyIDIgLjkzMSAwIDEuNzA2LS42MzkgMS45MjktMS41aDMuMTIyYy4yNDUgMS42OTEgMS42OSAzIDMuNDQ5IDMgMS45MyAwIDMuNS0xLjU3IDMuNS0zLjUgMC0xLjkzMS0xLjU3LTMuNS0zLjUtMy41em0wIDVjLS44MjcgMC0xLjUtLjY3NC0xLjUtMS41IDAtLjgyOC42NzMtMS41IDEuNS0xLjVzMS41LjY3MiAxLjUgMS41YzAgLjgyNi0uNjczIDEuNS0xLjUgMS41eiIvPjwvc3ZnPg==)}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module:before{content:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI0M1QzVDNSIgZD0iTTkgMTF2LTFjMC0uODM0LjQ5Ni0xLjczOCAxLTItLjUwNC0uMjctMS0xLjE2OC0xLTJ2LTFjMC0uODQtLjU4NC0xLTEtMXYtMWMyLjA4MyAwIDIgMS4xNjYgMiAydjFjMCAuOTY5LjcwMy45OCAxIDF2MmMtLjMyMi4wMi0xIC4wNTMtMSAxdjFjMCAuODM0LjA4MyAyLTIgMnYtMWMuODMzIDAgMS0xIDEtMXptLTYgMHYtMWMwLS44MzQtLjQ5Ni0xLjczOC0xLTIgLjUwNC0uMjcgMS0xLjE2OCAxLTJ2LTFjMC0uODQuNTg0LTEgMS0xdi0xYy0yLjA4MyAwLTIgMS4xNjYtMiAydjFjMCAuOTY5LS43MDMuOTgtMSAxdjJjLjMyMi4wMiAxIC4wNTMgMSAxdjFjMCAuODM0LS4wODMgMiAyIDJ2LTFjLS44MzMgMC0xLTEtMS0xeiIvPjwvc3ZnPg==);margin-left:2px}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property:before{content:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI0M1QzVDNSIgZD0iTTEyLjA5IDQuMzU5bC0yLjY0MSAyLjY0MS0yLTIgMi42NDEtMi42NDFjLS41MDItLjIyNi0xLjA1NS0uMzU5LTEuNjQxLS4zNTktMi4yMDkgMC00IDEuNzkxLTQgNCAwIC41ODYuMTMzIDEuMTM5LjM1OSAxLjY0bC0zLjM1OSAzLjM2cy0xIDEgMCAyaDJsMy4zNTktMy4zNmMuNTAzLjIyNiAxLjA1NS4zNiAxLjY0MS4zNiAyLjIwOSAwIDQtMS43OTEgNC00IDAtLjU4Ni0uMTMzLTEuMTM5LS4zNTktMS42NDF6Ii8+PC9zdmc+);margin-left:1px}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum:before,.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.value:before{content:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PGcgZmlsbD0iIzc1QkVGRiI+PHBhdGggZD0iTTEyIDNoLTRsLTEgMXYyaDV2MWgtMnYxaDJsMS0xdi0zbC0xLTF6bTAgMmgtNHYtMWg0djF6TTMgMTJoNnYtNWgtNnY1em0xLTNoNHYxaC00di0xeiIvPjwvZz48L3N2Zz4=)}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule:before{content:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiI+PHBhdGggZmlsbD0iI0M1QzVDNSIgZD0iTTEwIDVoLTh2LTJoOHYyem0wIDFoLTZ2MWg2di0xem0wIDJoLTZ2MWg2di0xeiIvPjwvc3ZnPg==)}.hc-black .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file:before{content:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZmlsbD0iI0M1QzVDNSIgZD0iTTkuNjc2IDJoLTYuNjc2djEyaDEwdi05bC0zLjMyNC0zem0yLjMyNCAxMWgtOHYtMTBoNXYzaDN2N3oiLz48L3N2Zz4=)}.monaco-editor{font-family:-apple-system,BlinkMacSystemFont,Segoe WPC,Segoe UI,HelveticaNeue-Light,Ubuntu,Droid Sans,sans-serif}.monaco-editor.hc-black .monaco-menu .monaco-action-bar.vertical .action-menu-item:focus .action-label,.monaco-editor.vs-dark .monaco-menu .monaco-action-bar.vertical .action-menu-item:focus .action-label,.monaco-menu .monaco-action-bar.vertical .action-item .action-menu-item:focus .action-label{stroke-width:1.2px}.monaco-editor-hover p{margin:0}.monaco-editor.hc-black{-ms-high-contrast-adjust:none}@media screen and (-ms-high-contrast:active){.monaco-editor.vs-dark .view-overlays .current-line,.monaco-editor.vs .view-overlays .current-line{border-color:windowtext!important;border-left:0;border-right:0}.monaco-editor.vs-dark .cursor,.monaco-editor.vs .cursor{background-color:windowtext!important}.monaco-editor.vs-dark .dnd-target,.monaco-editor.vs .dnd-target{border-color:windowtext!important}.monaco-editor.vs-dark .selected-text,.monaco-editor.vs .selected-text{background-color:highlight!important}.monaco-editor.vs-dark .view-line,.monaco-editor.vs .view-line{-ms-high-contrast-adjust:none}.monaco-editor.vs-dark .view-line span,.monaco-editor.vs .view-line span{color:windowtext!important}.monaco-editor.vs-dark .view-line span.inline-selected-text,.monaco-editor.vs .view-line span.inline-selected-text{color:highlighttext!important}.monaco-editor.vs-dark .view-overlays,.monaco-editor.vs .view-overlays{-ms-high-contrast-adjust:none}.monaco-editor.vs-dark .reference-decoration,.monaco-editor.vs-dark .selectionHighlight,.monaco-editor.vs-dark .wordHighlight,.monaco-editor.vs-dark .wordHighlightStrong,.monaco-editor.vs .reference-decoration,.monaco-editor.vs .selectionHighlight,.monaco-editor.vs .wordHighlight,.monaco-editor.vs .wordHighlightStrong{border:2px dotted highlight!important;background:transparent!important;box-sizing:border-box}.monaco-editor.vs-dark .rangeHighlight,.monaco-editor.vs .rangeHighlight{background:transparent!important;border:1px dotted activeborder!important;box-sizing:border-box}.monaco-editor.vs-dark .bracket-match,.monaco-editor.vs .bracket-match{border-color:windowtext!important;background:transparent!important}.monaco-editor.vs-dark .currentFindMatch,.monaco-editor.vs-dark .findMatch,.monaco-editor.vs .currentFindMatch,.monaco-editor.vs .findMatch{border:2px dotted activeborder!important;background:transparent!important;box-sizing:border-box}.monaco-editor.vs-dark .find-widget,.monaco-editor.vs .find-widget{border:1px solid windowtext}.monaco-editor.vs-dark .monaco-list .monaco-list-row,.monaco-editor.vs .monaco-list .monaco-list-row{-ms-high-contrast-adjust:none;color:windowtext!important}.monaco-editor.vs-dark .monaco-list .monaco-list-row.focused,.monaco-editor.vs .monaco-list .monaco-list-row.focused{color:highlighttext!important;background-color:highlight!important}.monaco-editor.vs-dark .monaco-list .monaco-list-row:hover,.monaco-editor.vs .monaco-list .monaco-list-row:hover{background:transparent!important;border:1px solid highlight;box-sizing:border-box}.monaco-editor.vs-dark .monaco-tree .monaco-tree-row,.monaco-editor.vs .monaco-tree .monaco-tree-row{-ms-high-contrast-adjust:none;color:windowtext!important}.monaco-editor.vs-dark .monaco-tree .monaco-tree-row.focused,.monaco-editor.vs-dark .monaco-tree .monaco-tree-row.selected,.monaco-editor.vs .monaco-tree .monaco-tree-row.focused,.monaco-editor.vs .monaco-tree .monaco-tree-row.selected{color:highlighttext!important;background-color:highlight!important}.monaco-editor.vs-dark .monaco-tree .monaco-tree-row:hover,.monaco-editor.vs .monaco-tree .monaco-tree-row:hover{background:transparent!important;border:1px solid highlight;box-sizing:border-box}.monaco-editor.vs-dark .monaco-scrollable-element>.scrollbar,.monaco-editor.vs .monaco-scrollable-element>.scrollbar{-ms-high-contrast-adjust:none;background:background!important;border:1px solid windowtext;box-sizing:border-box}.monaco-editor.vs-dark .monaco-scrollable-element>.scrollbar>.slider,.monaco-editor.vs .monaco-scrollable-element>.scrollbar>.slider{background:windowtext!important}.monaco-editor.vs-dark .monaco-scrollable-element>.scrollbar>.slider.active,.monaco-editor.vs-dark .monaco-scrollable-element>.scrollbar>.slider:hover,.monaco-editor.vs .monaco-scrollable-element>.scrollbar>.slider.active,.monaco-editor.vs .monaco-scrollable-element>.scrollbar>.slider:hover{background:highlight!important}.monaco-editor.vs-dark .decorationsOverviewRuler,.monaco-editor.vs .decorationsOverviewRuler{opacity:0}.monaco-editor.vs-dark .minimap,.monaco-editor.vs .minimap{display:none}.monaco-editor.vs-dark .squiggly-d-error,.monaco-editor.vs .squiggly-d-error{background:transparent!important;border-bottom:4px double #e47777}.monaco-editor.vs-dark .squiggly-b-info,.monaco-editor.vs-dark .squiggly-c-warning,.monaco-editor.vs .squiggly-b-info,.monaco-editor.vs .squiggly-c-warning{border-bottom:4px double #71b771}.monaco-editor.vs-dark .squiggly-a-hint,.monaco-editor.vs .squiggly-a-hint{border-bottom:4px double #6c6c6c}.monaco-editor.vs-dark .monaco-menu .monaco-action-bar.vertical .action-menu-item:focus .action-label,.monaco-editor.vs .monaco-menu .monaco-action-bar.vertical .action-menu-item:focus .action-label{-ms-high-contrast-adjust:none;color:highlighttext!important;background-color:highlight!important}.monaco-editor.vs-dark .monaco-menu .monaco-action-bar.vertical .action-menu-item:hover .action-label,.monaco-editor.vs .monaco-menu .monaco-action-bar.vertical .action-menu-item:hover .action-label{-ms-high-contrast-adjust:none;background:transparent!important;border:1px solid highlight;box-sizing:border-box}.monaco-diff-editor.vs-dark .diffOverviewRuler,.monaco-diff-editor.vs .diffOverviewRuler{display:none}.monaco-editor.vs-dark .line-delete,.monaco-editor.vs-dark .line-insert,.monaco-editor.vs .line-delete,.monaco-editor.vs .line-insert{background:transparent!important;border:1px solid highlight!important;box-sizing:border-box}.monaco-editor.vs-dark .char-delete,.monaco-editor.vs-dark .char-insert,.monaco-editor.vs .char-delete,.monaco-editor.vs .char-insert{background:transparent!important}}.context-view .monaco-menu{min-width:130px}.context-view-block{position:fixed;left:0;top:0;z-index:-1;width:100%;height:100%} \ No newline at end of file diff --git a/packages/rrweb-snapshot/test/css/style-with-import.css b/packages/rrweb-snapshot/test/css/style-with-import.css new file mode 100644 index 00000000..61058d7b --- /dev/null +++ b/packages/rrweb-snapshot/test/css/style-with-import.css @@ -0,0 +1 @@ +@import "./style.css"; diff --git a/packages/rrweb-snapshot/test/css/style.css b/packages/rrweb-snapshot/test/css/style.css new file mode 100644 index 00000000..2b3faf2a --- /dev/null +++ b/packages/rrweb-snapshot/test/css/style.css @@ -0,0 +1,12 @@ +body { + margin: 0; + background: url('../a.jpg'); + border-image: url('data:image/svg+xml;utf8,'); +} +p { + color: red; + background: url('./b.jpg'); +} +body > p { + color: yellow; +} diff --git a/packages/rrweb-snapshot/test/html/about-mozilla.html b/packages/rrweb-snapshot/test/html/about-mozilla.html new file mode 100644 index 00000000..f353c482 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/about-mozilla.html @@ -0,0 +1,57 @@ + + + + + The Book of Mozilla, 11:9 + + + + + +

+ Mammon slept. And the beast reborn spread over the earth and its numbers + grew legion. And they proclaimed the times and sacrificed crops unto the + fire, with the cunning of foxes. And they built a new world in their own + image as promised by the + sacred words, and spoke + of the beast with their children. Mammon awoke, and lo! it was + naught but a follower. +

+ +

+ from The Book of Mozilla, 11:9
(10th Edition) +

+ + + + \ No newline at end of file diff --git a/packages/rrweb-snapshot/test/html/basic.html b/packages/rrweb-snapshot/test/html/basic.html new file mode 100644 index 00000000..44d38cba --- /dev/null +++ b/packages/rrweb-snapshot/test/html/basic.html @@ -0,0 +1,15 @@ + + + + + + + + Document + + + +

Title

+ + + diff --git a/packages/rrweb-snapshot/test/html/block-element.html b/packages/rrweb-snapshot/test/html/block-element.html new file mode 100644 index 00000000..573c4fa2 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/block-element.html @@ -0,0 +1,27 @@ + + + + + + + Document + + + + +
block 1
+
record 2
+
block 3
+
block 3
+ + diff --git a/packages/rrweb-snapshot/test/html/compat-mode.html b/packages/rrweb-snapshot/test/html/compat-mode.html new file mode 100644 index 00000000..61b1544d --- /dev/null +++ b/packages/rrweb-snapshot/test/html/compat-mode.html @@ -0,0 +1,14 @@ + + + + Compat Mode; image resizing + + +
+ + + +
+
+ + diff --git a/packages/rrweb-snapshot/test/html/cors-style-sheet.html b/packages/rrweb-snapshot/test/html/cors-style-sheet.html new file mode 100644 index 00000000..741a1366 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/cors-style-sheet.html @@ -0,0 +1,15 @@ + + + + + + + with style sheet + + + + + diff --git a/packages/rrweb-snapshot/test/html/dynamic-stylesheet.html b/packages/rrweb-snapshot/test/html/dynamic-stylesheet.html new file mode 100644 index 00000000..83a57ccf --- /dev/null +++ b/packages/rrweb-snapshot/test/html/dynamic-stylesheet.html @@ -0,0 +1,20 @@ + + + + + + + dynamic stylesheet + + + + +

p tag

+ + diff --git a/packages/rrweb-snapshot/test/html/form-fields.html b/packages/rrweb-snapshot/test/html/form-fields.html new file mode 100644 index 00000000..31a35afa --- /dev/null +++ b/packages/rrweb-snapshot/test/html/form-fields.html @@ -0,0 +1,42 @@ + + + + + + + form fields + + + +
+ + + + + + +
+ + + diff --git a/packages/rrweb-snapshot/test/html/hover.html b/packages/rrweb-snapshot/test/html/hover.html new file mode 100644 index 00000000..e6df2126 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/hover.html @@ -0,0 +1,31 @@ + + + + + + + + hover selector + + + + +
hover me
+ + + \ No newline at end of file diff --git a/packages/rrweb-snapshot/test/html/iframe-inner.html b/packages/rrweb-snapshot/test/html/iframe-inner.html new file mode 100644 index 00000000..2ef778d9 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/iframe-inner.html @@ -0,0 +1 @@ + diff --git a/packages/rrweb-snapshot/test/html/iframe.html b/packages/rrweb-snapshot/test/html/iframe.html new file mode 100644 index 00000000..8b45139e --- /dev/null +++ b/packages/rrweb-snapshot/test/html/iframe.html @@ -0,0 +1,12 @@ + + + + + + + iframe + + + + + diff --git a/packages/rrweb-snapshot/test/html/invalid-attribute.html b/packages/rrweb-snapshot/test/html/invalid-attribute.html new file mode 100644 index 00000000..e2428e28 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/invalid-attribute.html @@ -0,0 +1,3 @@ + + + diff --git a/packages/rrweb-snapshot/test/html/invalid-doctype.html b/packages/rrweb-snapshot/test/html/invalid-doctype.html new file mode 100644 index 00000000..395c916d --- /dev/null +++ b/packages/rrweb-snapshot/test/html/invalid-doctype.html @@ -0,0 +1,9 @@ + + + + + + Invalid Doctype + + + diff --git a/packages/rrweb-snapshot/test/html/invalid-tagname.html b/packages/rrweb-snapshot/test/html/invalid-tagname.html new file mode 100644 index 00000000..e28dd710 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/invalid-tagname.html @@ -0,0 +1,14 @@ + + + + + + + Document + + + Hello + Hello + + + diff --git a/packages/rrweb-snapshot/test/html/mask-text.html b/packages/rrweb-snapshot/test/html/mask-text.html new file mode 100644 index 00000000..e31eab8f --- /dev/null +++ b/packages/rrweb-snapshot/test/html/mask-text.html @@ -0,0 +1,17 @@ + + + + + + + Document + + + +

mask 1

+
+ mask 2 +
+
mask 3
+ + diff --git a/packages/rrweb-snapshot/test/html/picture-blob-in-frame.html b/packages/rrweb-snapshot/test/html/picture-blob-in-frame.html new file mode 100644 index 00000000..f6237f22 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/picture-blob-in-frame.html @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/rrweb-snapshot/test/html/picture-blob.html b/packages/rrweb-snapshot/test/html/picture-blob.html new file mode 100644 index 00000000..d2a32658 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/picture-blob.html @@ -0,0 +1,16 @@ + + + This is a robot + + + diff --git a/packages/rrweb-snapshot/test/html/picture-in-frame.html b/packages/rrweb-snapshot/test/html/picture-in-frame.html new file mode 100644 index 00000000..31684d2c --- /dev/null +++ b/packages/rrweb-snapshot/test/html/picture-in-frame.html @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/rrweb-snapshot/test/html/picture.html b/packages/rrweb-snapshot/test/html/picture.html new file mode 100644 index 00000000..e005310b --- /dev/null +++ b/packages/rrweb-snapshot/test/html/picture.html @@ -0,0 +1,9 @@ + + + + + + + This is a robot + + diff --git a/packages/rrweb-snapshot/test/html/preload.html b/packages/rrweb-snapshot/test/html/preload.html new file mode 100644 index 00000000..32e84a26 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/preload.html @@ -0,0 +1,11 @@ + + + + + + Document + + + + + diff --git a/packages/rrweb-snapshot/test/html/shadow-dom.html b/packages/rrweb-snapshot/test/html/shadow-dom.html new file mode 100644 index 00000000..0050bede --- /dev/null +++ b/packages/rrweb-snapshot/test/html/shadow-dom.html @@ -0,0 +1,209 @@ + + + + + + shadow DOM + + + + + + +
content panel 1
+
content panel 2
+
content panel 3
+
+ + + diff --git a/packages/rrweb-snapshot/test/html/svg.html b/packages/rrweb-snapshot/test/html/svg.html new file mode 100644 index 00000000..3035cd34 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/svg.html @@ -0,0 +1,54 @@ + + + + IcoMoon - SVG Icons + + + + +
+

Grid Size: 0

+
+
+ + Icon-behance +
+
+
+
+ + Icon-linkedin +
+
+
+ + + + + + + + + + diff --git a/packages/rrweb-snapshot/test/html/video.html b/packages/rrweb-snapshot/test/html/video.html new file mode 100644 index 00000000..653f7172 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/video.html @@ -0,0 +1,19 @@ + + + + + + + video + + + + + diff --git a/packages/rrweb-snapshot/test/html/with-relative-res.html b/packages/rrweb-snapshot/test/html/with-relative-res.html new file mode 100644 index 00000000..c390dc53 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/with-relative-res.html @@ -0,0 +1,21 @@ + + + + + + + Document + + + + Hello + Hello + Hello + + + + + + + + \ No newline at end of file diff --git a/packages/rrweb-snapshot/test/html/with-script.html b/packages/rrweb-snapshot/test/html/with-script.html new file mode 100644 index 00000000..b4812e96 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/with-script.html @@ -0,0 +1,18 @@ + + + + + + + + with script + + + + + + + + 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 new file mode 100644 index 00000000..6b45f65b --- /dev/null +++ b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html @@ -0,0 +1,16 @@ + + + + + + + + with style sheet with import + + + + + + + + diff --git a/packages/rrweb-snapshot/test/html/with-style-sheet.html b/packages/rrweb-snapshot/test/html/with-style-sheet.html new file mode 100644 index 00000000..2083dae9 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/with-style-sheet.html @@ -0,0 +1,16 @@ + + + + + + + + with style sheet + + + + + + + + diff --git a/packages/rrweb-snapshot/test/iframe-html/frame1.html b/packages/rrweb-snapshot/test/iframe-html/frame1.html new file mode 100644 index 00000000..8810af46 --- /dev/null +++ b/packages/rrweb-snapshot/test/iframe-html/frame1.html @@ -0,0 +1,13 @@ + + + + + + Frame 1 + + + frame 1 + + + + diff --git a/packages/rrweb-snapshot/test/iframe-html/frame2.html b/packages/rrweb-snapshot/test/iframe-html/frame2.html new file mode 100644 index 00000000..34324085 --- /dev/null +++ b/packages/rrweb-snapshot/test/iframe-html/frame2.html @@ -0,0 +1,11 @@ + + + + + + Frame 2 + + + frame 2 + + diff --git a/packages/rrweb-snapshot/test/iframe-html/main.html b/packages/rrweb-snapshot/test/iframe-html/main.html new file mode 100644 index 00000000..d8e712bc --- /dev/null +++ b/packages/rrweb-snapshot/test/iframe-html/main.html @@ -0,0 +1,12 @@ + + + + + + Main + + + + + + diff --git a/packages/rrweb-snapshot/test/images/compat-bottom.png b/packages/rrweb-snapshot/test/images/compat-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..bc0267a61964622717d410067075a758e0fc0487 GIT binary patch literal 2503 zcmb_e`BxGM7j;ffOqSR(Wuu{!HZrYFD{^H>27RfW@KPs zVC-?$&D+3WlgPl}i&n!;dgQ<}&KCXHl6W>S#lYa0^}qcErVNVKgGVo2@Hw4VbzP^^ z@n^Xz^*Xhkv>*|#%2zu38vD7O&l_-~yq?$ZD<4ymN?U$w>MZ^n9~Yt*``N?oln-WN ziPw+XhXetp*keiflY7M9c@|Xq)_R{RvkEh$SnYVUuZ>Nz@`VihUqpNCHv5R05G^<&3navd;j8uwC|0y$H@+D-?RE`Mmf=kg{ql$ zt9{N=8CGxkzpnn9)KDy4w_q-{L};oI8&mQTma`?#bajmW`5IIq5t4_C>umj3OhkhLN1Y7A8q1o+@W~z;V zD9v2&WAPFuU>CL;pRzH+;1YUF-)U#BizYP-in3@%FCPNJzQk8&9kA>|9+j~>CI_Lk zp%@0JL=y4orre$FuPEnO(jSuWT3XfyvK8Dq8N(RiL%z{3SqVytl_SMEWy{9ZwKp7v ziY-Ozv}tQn8fA2{-g_7Jr7H>5b30PFI*dxSk+Bro!mn_yE(3@Ud^*eXo*bQ_@DWw1 zdY9dnq~qxurPgHIECn8aE@l4!-GsRppVlx8b)d9_E3zE@cVJ%@DI`!3u5Bbwt4R_; zwPK;j#eS!1bbYP1%2jZU!QgA0cay;jO62lC10=g~l0v70>)5Cq*1crWji519LDrF< zZkNl^i$xe??VtL*+U>M4p0$>vNy10xZWYCc@V|jlL2=D~C~7*?x$s{?6a5o5?CiAn zJRIuh^05#2&H+)=!lj4Se5Fs@c3&M5usZK#iM7Y9_$6g7QAQUiAKqn2D&Srtw54wL zO50ptF4H^iN#KwM9V_?y*gb)ClKJ4uhhNdkv&zI|$(bw@|6y{R=95?nM87z)1gRFPm z^$35y1u`ktT#Xw-pS(HHXpyla=P>*0&G|VIv`(+fhh(WZ#m(?bX6#RI3C-0UYjtb= z#f#$8v4nvK8`jC>=Je2oGPu{Mb*^b{;@g!)0-pBq$a^!?5H?P9WSdYHqxnlDDN-h% z7ImpIO9M&fSqhloP-s}uR$^MgXbE6~WzsX^)$|mkakG-8Y9eN5vwrqex24c5xGom3s!c_p8+N7X^^#ZeoiNxc9}pvv!E8uL_Nn%eK8{`yX{EJnXd0( zUfgh8ALWk=MO8+S$+ACC1twmM!qM)Eg>dH;=MjbNA_yB_3a3^g-Ax5|_q~lvb&?T2 zeEA!DiV=$-?sX2f1@*kA-`*IEcahyyRpiA&iGO#(f7z;Wo82Z%UFM&lxZeT`=Z4lA zc;!l%EeLyBgPxsT`IUpTCKFECQ`~3ai71H!eW8su?#N@OihSiYc^p)F%re9bTYDHt zUo4kD(XiIWE@jp#EIi-dxgFw5Oq(mV=$O+YkI1gnD~^9fiG~P=`4Q~44xbtyDZZHm zWK=UjTQue{;{8BEj^9P`<)D%~F)a)8K@k8ocaZEqYTZc~`G)|5Hgcfm5*h8!7|-IU z?C#_BoWo*RaS>;AW$k`ub80V}xHR<)x(6Hii|Ao4lL?&~Y%Jn?L0x+hRqa&AMMgN; zpBqlLyti#E^|&nM1lhi_LcOT{T-?Cd+?(LzP^B(Av1t%{m5r=oh@sfiKx9uC+I`>B z8EUzO@wzUf_$5b?U_&*x>GGVVR=}xMTX6+20lmIesI7Qf{|SBP#**b$oC<(M-+#Ov zLsm^%+}Erni%z8F*j=&PMi+-5v}0t!I6SJ(qyqYazbx z7sU)|7Z4|&mP(>>54KSbt_KfWNKXKH@fU}ZHidJb!xrrhK%VjMSCVRy6TL~Hk@U5+K<^a?o|r zx99IZM^P+}BQR0r!n@d2&aU%!c}rN4dpl-|k2|WYt?DY|#r1j=u{VK@8I3*jB?XZ` zn*`Vvj?<}g)2z{NTri@tNSNu-U9Y)65K~W0)#hpj_){LenPX?jLEG02&SwFfKb6Zd zKXra<^F+x%A`=Y?`qNxxGgcafku-P+>#=UE0RH597T|_FLU~7<49g zWp`h%*S-j_`T#k7H7v7J6Tlk3Hff#_CuAp2#$B>)FL(ida&_eC8FKM42s)(-EVIV{ zB|%R!AO8b5*@%6>p(}6lRGfQWdu^YebfA#T_`I3EY1K5rRls(SCfXGF-3n1~)aCM@ zm1e5vUG$G&BgT4OCbgmr$uz5Bc&m0_6cZk*>c@YqP&K6-D`@1%%?6b?;G*GCo+HrRau`rHWr| zfm@~T>|znrlQD&}BYHSF`^IHlzZ= literal 0 HcmV?d00001 diff --git a/packages/rrweb-snapshot/test/images/compat-top-left.png b/packages/rrweb-snapshot/test/images/compat-top-left.png new file mode 100644 index 0000000000000000000000000000000000000000..40dc35b6d2c7b51ba827bc307fa6e79bc5dff5e9 GIT binary patch literal 2693 zcmdT``!^Ge8~5hx?V>16$nD#!7o$jZLNU=;qEVtUr1A2WHn$nW_@ch<^O8`++nA9= z$lT@kRyjq9+Huf zIbweud|O6l-+zAMfb1S~<+%B=y>u|>`u$KD86%ya_$7j1h2A4CIXm95)hrgg>01}9 zN$ymxh_F%^Zyt}(gpW4DU32_~(ZQ~*%g21olNA1wY)w1O;bK5AkA! z_F(Hf5fe*nR{Yybs=qp$8%DH?V#mGeF4*lqtpQFA@T{M<13eWVJhk|<;Xfl*+OkGz zt&Xesp5eNdLjDu-O>O=vy|h0dM2Rf|i>61iZNpsdxVjRUGd11ye5@DSKHm|=J<3K{ zO)OO54c_84)n1r4xIo>-NY-+|DER|} z#lLr_#|)Asrwl0G2P$L6PAls%5WuFv)iO(?a7W=814=)F@5xMd*zTSCz{?40b|dD> zW!TrG-uPiry}pgs^2f8FDC^AVomYu+srV3;TsUateskE`AfDxjdi}?m61PU750?iB z^nVnoA(m$#bep_20vK*HB*CBPV4$5wCkz;Rlx>ISpYh3(+x@tvnmhGlEX}u*0M%(q z2QxpaI~=|{7Ov+3P9a$Vb&M_&c=g9T=dOqZkH}l5o9Q z0WZJ1W)f$m(Mg~)ZPc2t6r$>VDqUZcPjs<)rFPuzk~aG!G-C(js8yKUE6O}Fuoc*7 zRREva!W43HqaxGuy<}ppq4*otQt~F3HY2s{^l9B*?tDr^?(IkEY)BVEY{`7r)+aAz zdZ7Y?V`J7p9Cw8Wt^GL8ckdKxPyXzBK{A4z+c-I8_RULR3Km5&t8L1PEFQ+44hk9_7L6zj%5P9z7xJ+NTx#Qk6ei8@%+-Ik|2S#!w{m{gS$rP^J4E^I^oe)o6vU;zXpjPB+UzC!mgTliaRiZVuy7Z06mDj0i(uY0=V^wNt7Z}*jiq+v3 zi=VlFjnG5fkr%^WQjIqlSbZ{TCc z?*zrNhCiKd1_E)d#a;u=Z!ispU;B!AoXYfo3?vc8d2Q2}pFOn@n(`^UeMXN#_HN2M zIlOH!#Eg$l_)^-1C~c81ZT-DsR+3<8p-0|^^H#Yw)P21t#Z1ZIxZRaj%8?ozewYl%zr`OI)Gz6;=9^$#IaO(bkA1mX;s{2CI4sKfs$Z>mR?6vhi&Z@%pflA)-O-%;beZ>6yj~7nbtMxA? zPc`he+M8ML>NnBL`x*w}iKfZkWbm^C}^v?dt>W+e$tn9V5!5g_M=$S<&bpJj6Yc zN46R3E`~Xn-7LY@e;WKENpdg|CtD0CH2iCoqt}`#d=wZiI2p&nAuX+8ch;&*+|oeT zGl3kZY(*pGaCy2@bM98l=bbAc{U%dI${p8WQXhQfD1Gxqi;l}-Sk~LO6RW4LctOLS zyNxco`A*^>o&%OQSm{VtW|F$~G^8KyqT%6-5zfqZbhP?)XZW%hl^(we645?=UuitR zl^g`-Ui9M$GqRlPxzviRnsu_+VdrG zCC7lEs8NSe=o9d(yqENKF&vw~AQOx|HVQjwb!^q=Q<5?Y`{ZMnWuLL&bng-RT0cJO zuD{vRK#&uCbhfJ39>qPT`OkQV{(nlbwpcbo)4AvOu+WyD NioGoq{NBbp{(oAF>iqx! literal 0 HcmV?d00001 diff --git a/packages/rrweb-snapshot/test/images/compat-top-right.png b/packages/rrweb-snapshot/test/images/compat-top-right.png new file mode 100644 index 0000000000000000000000000000000000000000..0e7a6d2866345fdee552e08f311c5cc752b3c17d GIT binary patch literal 3049 zcmb`J`7;}e7RQs?uKnKj7PWR$Qbo1)eW|9_v}h4ZO0}_Xv4%)#DZMDQv=l)RRoaV4 zs%m*HvC}3JK_snRYAvD?@!a>z`v>lsIdf({=R0T4oSE;;Nx$w078j8f0RRBvHrK8= z0RTtP005A{&v!^vPTM&g7U8gK?vVh1#+iQ-7+Yl)eF)EAw|B9o_--E@9BhnnnT5MN z?g5w0n@HFh58Cx5S#{K>|EN{U#5$*g$?UPE!EsEBv;SufpjvO(`ka^5Y-@4`*`A;{Z#ek?& z>{&=&NI&w4>LVPE-=XGh;|-ZB7U{6m~j9b370Z#N~%%8@=h+9 zA@}r_@Gyh2=9L7E%RU4rFHv0-$|^2?G;?MU#=5xsJG5&)=lZzEnz>T^QJ196Q414f5h(>g5Yc^ih>J|x@bVD4-ihJ;z>b@ zO1sxSzY$dB{d6lWG?ri_Kl6<|yzNF=T%6Y>rD%no(5V}=V_J1U4O>PyqMVbx@E^fl zbF$o_XjAxo+?9%B#uC#4UOVyQ{*W3$RaqP7)j#dv%A5j#!(2tu`&Wnz3!~>8lL{fRyfr3r0ctaF=`QrT zN^aAzRGaJzKJ5bh98Y-#Rw|B?2#*#S^8(lMt2zdfmepj>y`<)~SuZSOkF7N4$Q&xy zs&6{4{%tut)~!FD2yLYxJ(N4|w;sgyeQ8j*TbNhxN{qTSEM?VLvPW;eJx@<``AQbR)`l#QZ9(dj zIDa+m%uFWJj3kUf*aqnHVY8qK)ATQip-`fH`vGVHX3<^e-< zMVCD%_>T;0mz@}to8BX<_0hqQ<}$yx4Ap#qbbg58N=;4h7zkx@P?LB%uAH$b1}qwg z0#_I?bAq;y#dY8-I5;aSQB#T__LpTHr97^^f6Nyr$U5zf=Z9-hagSO!;v?E(+?n;p zu?H;zBUdK?*3#7)62w5t%@T_m{E8R%s5*iukE!z9>KUo4E3@gWy=J7)oOmX38-=oo zS)QT_x}}eqz7LU9j+waW*uQtanBta*F4$ayJKhIBaOEdyoQk=lpA!fP9uMvXX58pf z7}~t@2{jEVO1)G5`tl-i3kdE1uycu%1qNPTIK(Kxc~TjPXT#8R z!;6~pKkI6w*E%9}9KswLv?6nqJIAo&FmHrh@&4uUD5U7LObfw=G@O!F0GM;niYi42 zru1-%u)pOa^q;Y)(wD7n8*~=Hw|M)^9-k+#FCD=FSBj28r@veX%sURN)RQSa zI(Ei`$f7-X7K2U?yc@IEb>%6ZK10^AOAA!j6WmQ80653PRP_3LH8g|nBb!eGs}o6E z??6V-^+Ut$Y-$PWtBKby{8ZnK9|v^HN6pudo2ZxchRA*4hifYNqB%~II-WNg79>vF zqnFNaX&M)t&Tf?aUlWY}Xu9sUS}u5sK5~0oEaK>iAYzn+fA=)TbqHwKPYgdwyRJ8u z-mp|h>u8WktDXqQ7D9=_MDrTmyNf; z)!UqGN~u-PbZRTyQkeOGIJ>xe@@8#e9$!ShT;Z10s>U*Wf*EG@(5-$7F92`FZduMM zmJujUYc@}AN0>K6%ve{QWm;GHIiu53G(!5MVBZFcUZl=KhZwnDV2R$FrtJeo;zLL= zJ~C35B=9HUU8y@?m=|Q?QU`LNCgFlKWh7GuR>P!Z+4l@b4)p>lJviK_>4)_|DY|D~ zEq<7BJ5!!Ht}Fcla(Wmb8WK7nhWJkBp36RZM4q_Q(i$Z*BeCArTGh6~wJsxQ;PD+OoWQgWp=h~iS!PaxY&4G? zxXbE!=2P?eJCHIr+QpLEV04njpG(C=R|2eML2?DJNHJpI% z08Np{u8ORCwLin=ZKSX`UneytBrbPCculzB`!R#pfLSvt2Rd|0R@4Ci|AdaHC_u&% zKbi%;0@U<`lhRFw<|`Bq0D&~tv(?Gc*^xF=BYv>(#r@P=uj3b%T2xe)cXVgK(CDey z1hwkQ-W(J$=ZNl*u6rAfqKMxDcn$zw5!r&a=XHy(0 zzhel0#KcPvR!9CqGNQbM6=3YEQ~qn7-!lo7c*t4-YGzgCAdX^PvUO`c=}GCK2l2^f gS^w@&`u}Np%p@6}Scu;k^5Z|y#@g{pgC#8azu-~MsQ>@~ literal 0 HcmV?d00001 diff --git a/packages/rrweb-snapshot/test/images/robot.png b/packages/rrweb-snapshot/test/images/robot.png new file mode 100644 index 0000000000000000000000000000000000000000..cc486cc8b70680b0dfd72828b6530e0a4e9183ad GIT binary patch literal 11004 zcmVMG57cTg)be7&LF-a&Iq(N9eY0ckxvbk2cx7z2w0ZIEpA(uQbq_V zgpyh-t=ipA&dZOK*Vj7>&F&&&7QkiMEq$Y4yw)0&#%Xl*rW^mo2S33m7?6cr==-j1 zb4saYao4dBQE8KABF%(lS=}V!j9xXiCm-}FEeIi(^#E@alGhMpjAOYUe)q4~mPILW z#xtde!O-Fqpxut7Q~;pSAfn7QkB|3Sp`tS z>qYtIXiyw$;%0_Z-%)t${gEON)tu^KuKlL5P&#I<2WS%TI(oIwAPsrNt#AccVS_n zR-H#QanvCg5<<|x>sIEQVXkCZ3rH!YKn#FmSxK5DX$AmNX-a7i03s@-;v}h7=Y>cJ z0ijb=oDgz3)xecCR|3Qs<2qh3ECGPUIS@iAwQSyvQ$*BSAsWUgrPSh_QA#LDlcd{i z10n)2N}3Cc6c}Dc(Q}1;l7KNf=NEE*h=7Qwl_rFAI&qq2T4|$^QNkGUUDt7JN+e0HYNSjE?waJChM1uU{W;Qh`1Lg z9HcQilc^sBo?|(#ZQEA8z7&Q5W&GUKtmFC{H*AYqb)zK#5NVV~4H37s7Jz8G7IpRI zwLSo@w7K@$0Xfpcmvb zsg%~OC=t>iAzEwK=fn|c-hh*WD+>|!c6sF{Q7Tg z+OqMb0|&KM8#isKH`)u;TAFl?)>3OfR{+3vbBTaTqhxTrKR0UiHlLUO3|Btz5C8*9 zSUW#IO%3dO?%DdnRI6DZ9b4u2q33#9%Y3nrDX3NF2&kl+I<7Ogp)5r5h9HDD=fKBe zcC%BTS(th8M+dIHX6NsG@>4+&e)qc%ojrd}ir5&HC2_l1%d%8y4O(v8wSfi(_3Ph2 z{)Aj{kd)9Qi(JR^Ah&w;#_^$Xq0|k#Z@%~5_jJ3_;bX_+R5Tj3c56w=EN!)h*9`Y> z=@W^d^bLY|MM3r$0dX3wU$bRJZr$vmsfC%De4!}PwB6}cD&;%xyldj@nR62-*RNhz zE|ud}yE<20v2IOR$stj%zXSOXTceYM7fBe+GItyfp44iUR$d4z2ly@f97+4B!o`Wm{7W~uz2tN53FCmZQ|frNBZ36Zxk`& z=LSg#rIb<1C|&+r0z@hymB>WWj#`ZI_uT#q|LRvhYa;WRPyZeOXstJF*~V?_$blCq z<>kKeQnO*Z-tBk2Ey(8yL25K5&}z4f<;tGhZoBrH>z0nx^H$lwWi}fsbGoHfnxxA&pYYvbJ4)&DzbIo`33b zB~=_nW807!e3FPTPgjhM&dx6isU6F*IC<-xZ##1MK)FyncIs&V>fy!3S&Lu! zMi!+E(PSbKN`_v3&Dh43L#qbMLzO~bCEph%@i%_(-+uF#e!J7^2EJb|RgxsCFV!lQ zzNvHPrB-j*dsm!v_doqqqgLY|+DIw2RwvG! zb%h_;PGxmDj^g>nIjyy2Sy2?-c+*YSU4Q-a&peSUl#NoJ?};pxDw&&^q9FI~y}QwB zFU&8*NxEv)$}krY#55C9s+(@T{r~>2e|+xz8A{y9N(VLsSt{ve^mzWe(UlOAiIg(y zrX9C_=>CuV(mnsn4V$m(E0!oDX_g9^8Dap*L*IM2RPb-V?RJAE7Y0EPAb>$kv&8qq z```WEBuO5B>`~jblPJ!Tbmi)`<-UsVx?8sG48y?p{cfkzX}A0!EEV&VF+xylER_1c z|Lwm$aq1ZAAaqN9iD{#ZL4em2aeq2ULTDzk_2ZjA_v@d1?``i`Jvi!FOca9ngedHJKfB!(GugBnUUAwK z_w3re=lr?zStk9!cReplv$JPUGj1`?ils7u86F)!di2PFr=KFO+ieJ&6dd37LW?le z$^ia+E%l!cl2RgM_G|C^jWq+~&mBH=^!UlyYOPr53miLD3K0z&&-1_a;Dha@#Sgys z1B=ZzXN*#65RKNpZ54gDzf=eVUmLw)^VWOc^`7;cHpOwY>y}&MBq>+QjvKUEEs+UmF=8Us$Zhai>*l#!b?m@2EuQ`|`}8 z2&Tt-`B^mk(`jdj#&+Cw-j?rweloC=VS(aoX z&V)oX2trERaTez0{*Qn9vBibC(a{xQm={9Ss*5|Xxqj>RZ8z?^pTwzL({?zT+t3VP$lQi16 zdfSe5JLc;1-MHgJ=!#%yeBBmJe1eCR`wR2E~(Am5I?rdjbjOa(PNblru^xWt=gKJGSlEHf1c!gj5O;5K##^U!5OW zF}hHldujhuhY!7Q_RQ(I`I&mXRw?)Gefv9396!=*)%Ne(chCLr;kKP7$@uu%iE|V4 z3$uVG$Q7IO)5ZQl&Mn{1_4W4`^CcI`Ij@*uOX`dg_ESI-AjW9l4L|vjPgjB<(Q2?* zzT>((YMtf}zVqEBxxCB- zsN||^clVV`#bU8oDoswFxiEE#v!K7Qx+CV5d6__Z5hS5$mcHZG`**C_s1XUFJ?e}> z+_n4WBPY&QYxU6M>o#mYedgSb9XmH~*~~d-JvM_uPKW`t80Cx+N(`dXMr#wr38mDv z?M#R)6+Imasg%-$kcIjAVj=JO!ImxCc3yMi-0aNcxihwFA3pHX<}Fv<|DFe`iwjrn z*m>t&Z%;ERr9wl3z#ZuCFBVEj>B7{R>u=gqES3g`h7Z25@0(xvPZLKE9em+OOZ8@D ztYWCRR4y$0s6k^{_UPaW0Ky0X00fZQP)eUabalmY-H zu`wDE5Gep7#OBO(tSl2~kWwm@avjI9Ev>aOMjKo> ze&}P4Ye!N17hm|3R$Qa>{aE*h{DcfFLpeq0OolV;eVYlu}NdJ%dJj zo)>q!QT*}|1tA0o2!pZGLTiGE2HTyOb5<?Uj|7CF~)d~U&s{@Km*|b4k0dQ zQmN5WY0LUGKX~|?M_$^`7`1InNR_3j)+QCQXO@Ua2@pyZf-oqQ=A8MSqqPA5+qQcI z70Q?~Af(QujMG%4!sa{(eb;p?#(PbZT{rLf+~@zxj%%;}o3DN;jWbFqV@!(7_dUn4 z8Kb!%%;j=P99?t$?lY&3H|mS))~@=@=l|X6wd>MUG&`M~)i)5XR7PG(nU_)uQpQ}{ zd0CSH(ik8_01S)S(Kzv zoWx0rXc%XV(H;RbNwaP@CX`yXO@ZWcq3zm)P|LDKniNarPyFtuvn)Gu_#h(szMmwK zZ86t%EZZW43=Iwho;Nf+HZVL=2&{YWxhGB2PP?rQYGo#-CKg+>oLQIlq01n((V?FU z{Qvytgdk#; ziEcO6+PJO*0NP-d$uv!+6y-25Xn+vT83OivSP1F+;r;J^|LM~w(j@EU+$`w^p6l4Q zWpmE0p~1n4iPP_S-!E+6zSU?=7&C~7P+w{vo7$hMCIxn>nq4+mrPW}0Ftja=*u#wg z2t-JY2tbHq+n(ofZW)AbxBbkMk1y5g%}$g^g$AWiTInRqx=Gp*GEPu2Xa@M1zP*X>E)#hB79lSh;dlK3{4!n`xTm^SO4bX& z22pFHwH8t(N!n<2s?{20jB!?vQk!!rWRhgP-KIeVLOAE#;;!SyQ55FFa=FAU9!G88 z^M#PcwhTgF|G>HFg@OKx)?kb=2-VusGe7=eZoGm_DI=v^&LWq5l9cmhxEPNN006N$ zV~k3vmDXtzTbvohB#AZZx!LI-{_y+H?R)OZR z94EbQ4}d9voz}; z7-}rlZHrr+F~%I*$^}8ElboBHaa~&}ZGcWrPQ3W|le`hG39o5PfNT4`{tyQM1SEu5 zmb07@01N`BL?I%gF-8gz#ZjqTQQE{&*J!P^>2|uEPMa|rN3m_!EY2C{w(Z!C!x$%& zFh-3*%9z$hNtvWsqtR-#+DR1Ijw6+{ZAu6MK&7-nBZbVQ)Jmh)#u!8}TDMy*tw!P` z?(6I0-0pUoz846os7)NLE0yx8iRr#_iBUobnV+34kLUXPOVg9n)tP4hnjruLAj{^8 zfc0h-E_&mo-a;>yq-h#=y96Lfvp7irz!-zttX{L$5BwkqIcGqK6hbK_gdiY|F-l1( zWhRxBDoZn^wU7#d-J_R;wbKP<1EY4Bn1GGB}&R}w=+LCOBq|YVS|z? zNs_^#;q&Ltu2@yDEuLh;_g$%s?*+$BO(%l@25ZW*-~G@fPC2} zdsdnWp)STF0)U8@NYQbUG?(g}kvJ7m904H|NZgHrFbKmiU&y4T2^NaPBD_8n~SMiH;v-6Zt$Ftz#snN2z zQcA0h1$B`xj+de@A^_=hI`eZE05MHP-0c!T0BCnw7mFU!+c6SK9LKQ{6CjjQ%d$AP zT-PH6j6noYO6S5LP7W9QDDS-q}gTUL~$wqv_OmWrigXU>nU7#kTG;Y4L3vu)S*na1oD zAQ2g5jdml=RN(MlI5NwIi-=HNTx>2ZdajqGnGmVc${=*wZLPI2XtXwH2qjt>DJ3Er zqdm`cY&(kMG!?$@NhvLhD{TP5vUw&9XEY20V-OJ>$MpiKwUSauk)=sotIpLHX0tSI zG}?y_ym;H5JEW9x6qicH$w}UAH(WOqQc9(Qz!Oq6n~kFf59$`DpvNTOstQ?=6nf4vq;tUZfrH*6w_w`LoP9s8XZoXVD6$<%Y zr{ZOw>}j$Sx0;>iXsKWTFUDAOZGL{P{{6puX!F%O!d$K!b)=LAi4Y==BOx+0%R(tZ zBoqke964i@5+M1UU+^48NThYM6B7VxQ{B~CYh#pUTbxq>V2t{{2Vj(xS}Q3s&ksk( zR#C>JkjIZ6Idl5d=FMA_lu;BH3I)!srKPIx7c!v?VlMQuOgOevU1}Bz1&cA)aoh1c z%`eB4Wld5_(=4eqYviIgA{q_=jDPxH{eE?B`pB~f4?h3=@bFN#+s@KRYm+2#k|c<| zL~MHP?MzAlAaGnaOH(wcq)L;x zw`m4|Bu#zxr$6`m4XREK4a>9vS%Y<3C`+ zwm56Hnpu_sKy`6p&ALrmD>P=gRzyStoG}8RmCEHq)bPk-KYsB4{nq|{&&4WFw&OdFQ!4kLJ9FyUXP&(M&bvEG zhX~zHcjfr1qlXXf*xAPz%YPP?XZ@vZqQc3`L-~I1@*L&ZKNEqk$zHRT~+EYn&m-JAS02!Lp^ER&Rd<*Q%$?C*basaC(V1Yv;s(huPIj*>xCO`-qSbSU#Sdk-@ffP z|H-dvV;qW(!<9B#Yh{Fy#^RJS&luf{JO)uKZP1`WrPW*CddHqUZ;7IAbz!kuU945B ztyW{|{P}vdy12M#IZTLTes*!Gx}^Q4=6?TcTiEzZAn?>nTFN-JYDC4^ZP5R^hc z^7r2v9a}jtFx=}!3=EH!%6(@~pIo_mL!8tNnk-sf5|7&;`V%Prb_0Eg>u$b0EoaCMMTr9dl3l$qR~nt$cr@wP(lLF z4SjE*zvBDO$3FH?e)PyAoUrBYtIX6;x1+t-dA zIr853zrWp$J=ddzW?2?>y90wGM-INUaqG_B(F zLO`LUiml(UDen{uZ@5uOO&JqXBEl`V-!(Nm zQyN%l<)qCSA;kCngD*YjI991|AW34S6%sPHF#qgRKO%&jJbrl1Eh}Yj3l6Uq0$_1_ zetPoGx7;x>Jbdo_1xji2dH6J9ld>y`VTdUl&=DBw4!~r%85Uc5d?A z`LBQDZyK${^Biv5xqRX5@q`!QtXl;bb zcHOjlaA;U+%@_qj(DYz|Ha%nP%|c$hKd1NJpg{xJt9UP7tJ7n~^g6SbtOYK8NYI$I z8@F!QyuH)ue)-E^xG;4d4ZisN(?<_Jcl(|9`9Ua@j^m`+YR=Bh?tA>l=g*$ne(R01 zbFKD5!w>zrsY&MN>aEsLf8XtU_OeO^wcNOM>%^%u`Juel>Q%+SD}g@n)?e7U?V5JC z%?ay;5CA}9bni_Mu`y;@gRj8($~%LWTf3z9by7l*EaT&xGtPTo86dPP@^$*QYpD*Ua+uw3)rGM~0{n5WJjug_1j9LB)bVMmx&i9x3;DG}#I<9A17Gu2E zdNx{@<-iFg?rHwH<%hGRt^-G;byBB8}rIzgkVQ%ZyJEmvO z42_MRK7Fdw=|oXiDCN0MF<;IVijM060Hqe^*2?h}FCExl8Z12U%O702P$jV*8((?! z#OWP7uQ_&p_VFj5%=ZOe$%(t&mx*z&CPolrl<(%&_HQYBmB0>c<|ru%p^US7y(VO4 zl+rz8MKov-kpOCgXbhz^%LL;L!APw|nx;u2mCUkKWSJ1D$TBJA>eXx4uit2KO9+vr z=~BIRVd{K0YImZ}*%PO1%iVL=+Xse6YV}$ehMY3jwo8@%a%I4Aea86t>FI~Q@t2ic z@aw<%d#yxtyX`a+2lnqfarDK-YL%kC;oZ9gw#h4KgFoSu$S9Aq?t!VNDdjHrLMQL> zz@Z*B)MgGf6cuW%l~h8c1`Q$@L`s>~s?n(B`ihf>&s66ZI?blm(hqF{&Gr4m{s!bQocbkWPEaXw~g@azPlUsxvh)Jv~`0l!}$X z$G-c`i6aNfyLSD>Uw`S;>Eo0DB`ixiNvAcwX?@??oFZLf=qp)1KN)xskP^l?LqwsH znv8mdA|Z?t$_QhGqXCEeW4V<^sVIt;7G|4HBM1w}4;;SXAMKv}{+a1h=e@vPzh$Fk z+W^2QWt<}-XMB#$C24x{)UgxC4{y8b+KubCc&?|kO4D?3Xt-D?X%AVy^Xzw@Sify+ zxm?jk6QGo`IPSJujfu&#XHOonIPa$E%;9G(&VbVT+^iQCuf1cJH%zlEG5>(P{#vBo z9>0MRMtfI102rgSkrFeYW_|zWk--tpS(aoCsHRb>V%5K)Z}#|1+=%Xe=e2eT%wfqlQl0U&vB!m#4 zfWU}l4EVn9#=hfv9Ie)T#||yRC}nKh_8r@{Ut6s%aPBb9(=<^+_(7gf!Y!Lpk_&RJ zAXHk-%uc3hylQ-{?*~dL*Y^v9l|%dY_d@#2JYR&Cgz zwPBV+2(#^4p+CRk>NSeSN~lYb=H~H*7Rp7U#Jz7eb*L)q}pkXq_wM zuiv)YbzO^FewYJ9Av4!={UFRzVcV`z3IHr_5keJ_wfdqEVq|26W!bIP(u$$AkOCqy z!jNE?%W=jg&YUi-FRi?3%w$Li>%5>(s9wJc6v@vO#WFk{WyPhAl+mtca3z9en0LFNd#gkL# zEN*YwuxaPDH_o4)ItB*-01?AnP8-7<*QTz>QYADH06;>hGzlg^mI)58^~gUziIe~$ z`cBTa9L_nRyih8gKXAIa&DCwL13xJD4=v2jZ{K;1=lfb~04Nj+QX69+6Qai{p@dSx2@nF55=wrC z@sZavGJ${;D5H!rP6+XX&}bFKDK!?Sb~#sR%{FR{s#cP5?s=i@*xa@mw^ET2!h}ep zZYNEWW}}{^DFF<_f*np8*Fvd8yY-YJg8DSiOHX3z6Xt!HouHbqhvt7b$g9Hcz!je>U qTg~2LrAnpQZV>`Mi|_sS + + +behance + + + +linkedin + + + + \ No newline at end of file diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts new file mode 100644 index 00000000..6ee32f94 --- /dev/null +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -0,0 +1,375 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as http from 'http'; +import * as url from 'url'; +import * as puppeteer from 'puppeteer'; +import * as rollup from 'rollup'; +import * as typescript from 'rollup-plugin-typescript2'; +import * as assert from 'assert'; +import { waitForRAF } from './utils'; + +const _typescript = (typescript as unknown) as () => rollup.Plugin; + +const htmlFolder = path.join(__dirname, 'html'); +const htmls = fs.readdirSync(htmlFolder).map((filePath) => { + const raw = fs.readFileSync(path.resolve(htmlFolder, filePath), 'utf-8'); + return { + filePath, + src: raw, + }; +}); + +interface IMimeType { + [key: string]: string; +} + +const startServer = () => + new Promise((resolve) => { + const mimeType: IMimeType = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.png': 'image/png', + }; + const s = http.createServer((req, res) => { + const parsedUrl = url.parse(req.url!); + const sanitizePath = path + .normalize(parsedUrl.pathname!) + .replace(/^(\.\.[\/\\])+/, ''); + let pathname = path.join(__dirname, sanitizePath); + try { + const data = fs.readFileSync(pathname); + const ext = path.parse(pathname).ext; + res.setHeader('Content-type', mimeType[ext] || 'text/plain'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET'); + res.setHeader('Access-Control-Allow-Headers', 'Content-type'); + res.end(data); + } catch (error) { + res.end(); + } + }); + s.listen(3030).on('listening', () => { + resolve(s); + }); + }); + +interface ISuite { + server: http.Server; + browser: puppeteer.Browser; + code: string; +} + +describe('integration tests', function (this: ISuite) { + jest.setTimeout(30_000); + let server: ISuite['server']; + let browser: ISuite['browser']; + let code: ISuite['code']; + + beforeAll(async () => { + server = await startServer(); + browser = await puppeteer.launch({ + // headless: false, + }); + + const bundle = await rollup.rollup({ + input: path.resolve(__dirname, '../src/index.ts'), + plugins: [_typescript()], + }); + const { + output: [{ code: _code }], + } = await bundle.generate({ + name: 'rrweb', + format: 'iife', + }); + code = _code; + }); + + afterAll(async () => { + await browser.close(); + await server.close(); + }); + + for (const html of htmls) { + if (html.filePath.substring(html.filePath.length - 1) === '~') { + continue; + } + const title = '[html file]: ' + html.filePath; + it(title, async () => { + const page: puppeteer.Page = await browser.newPage(); + // console for debug + page.on('console', (msg) => console.log(msg.text())); + if (html.filePath === 'iframe.html') { + // loading directly is needed to ensure we don't trigger compatMode='BackCompat' + // which happens before setContent can be called + await page.goto(`http://localhost:3030/html/${html.filePath}`, { + waitUntil: 'load', + }); + const outerCompatMode = await page.evaluate('document.compatMode'); + const innerCompatMode = await page.evaluate( + 'document.querySelector("iframe").contentDocument.compatMode', + ); + assert( + outerCompatMode === 'CSS1Compat', + outerCompatMode + + ' for outer iframe.html should be CSS1Compat as it has ""', + ); + // inner omits a doctype so gets rendered in backwards compat mode + // although this was originally accidental, we'll add a synthetic doctype to the rebuild to recreate this + assert( + innerCompatMode === 'BackCompat', + innerCompatMode + + ' for iframe-inner.html should be BackCompat as it lacks ""', + ); + } else { + // loading indirectly is improtant for relative path testing + await page.goto(`http://localhost:3030/html`); + await page.setContent(html.src, { + waitUntil: 'load', + }); + } + const rebuildHtml = ( + await page.evaluate(`${code} + const x = new XMLSerializer(); + const snap = rrweb.snapshot(document); + let out = x.serializeToString(rrweb.rebuild(snap, { doc: document })); + if (document.querySelector('html').getAttribute('xmlns') !== 'http://www.w3.org/1999/xhtml') { + // this is just an artefact of serializeToString + out = out.replace(' xmlns=\"http://www.w3.org/1999/xhtml\"', ''); + } + out; // return + `) + ).replace(/\n\n/g, ''); + expect(rebuildHtml).toMatchSnapshot(); + }); + } + + it('correctly triggers backCompat mode and rendering', async () => { + const page: puppeteer.Page = await browser.newPage(); + // console for debug + page.on('console', (msg) => console.log(msg.text())); + + await page.goto('http://localhost:3030/html/compat-mode.html', { + waitUntil: 'load', + }); + const compatMode = await page.evaluate('document.compatMode'); + assert( + compatMode === 'BackCompat', + compatMode + + ' for compat-mode.html should be BackCompat as DOCTYPE is deliberately omitted', + ); + const renderedHeight = await page.evaluate( + 'document.querySelector("center").clientHeight', + ); + // can remove following assertion if dimensions of page change + assert( + renderedHeight < 400, + `pre-check: images will be rendered ~326px high in BackCompat mode, and ~588px in CSS1Compat mode; getting: ${renderedHeight}px`, + ); + const rebuildRenderedHeight = await page.evaluate(`${code} +const snap = rrweb.snapshot(document); +const iframe = document.createElement('iframe'); +iframe.setAttribute('width', document.body.clientWidth) +iframe.setAttribute('height', document.body.clientHeight) +iframe.style.transform = 'scale(0.3)'; // mini-me +document.body.appendChild(iframe); +// magic here! rebuild in a new iframe +const rebuildNode = rrweb.rebuild(snap, { doc: iframe.contentDocument })[0]; +iframe.contentDocument.querySelector('center').clientHeight +`); + const rebuildCompatMode = await page.evaluate( + 'document.querySelector("iframe").contentDocument.compatMode', + ); + assert( + rebuildCompatMode === 'BackCompat', + "rebuilt compatMode should match source compatMode, but doesn't: " + + rebuildCompatMode, + ); + assert( + rebuildRenderedHeight === renderedHeight, + 'rebuilt height (${rebuildRenderedHeight}) should equal original height (${renderedHeight})', + ); + }); + + it('correctly saves images offline', async () => { + const page: puppeteer.Page = await browser.newPage(); + + await page.goto('http://localhost:3030/html/picture.html', { + waitUntil: 'load', + }); + await page.waitForSelector('img', { timeout: 1000 }); + await page.evaluate(`${code}var snapshot = rrweb.snapshot(document, { + dataURLOptions: { type: "image/webp", quality: 0.8 }, + inlineImages: true, + inlineStylesheet: false + })`); + await page.waitFor(100); + const snapshot = await page.evaluate('JSON.stringify(snapshot, null, 2);'); + assert(snapshot.includes('"rr_dataURL"')); + assert(snapshot.includes('data:image/webp;base64,')); + }); + + it('correctly saves blob:images offline', async () => { + const page: puppeteer.Page = await browser.newPage(); + + await page.goto('http://localhost:3030/html/picture-blob.html', { + waitUntil: 'load', + }); + await page.waitForSelector('img', { timeout: 1000 }); + await page.evaluate(`${code}var snapshot = rrweb.snapshot(document, { + dataURLOptions: { type: "image/webp", quality: 0.8 }, + inlineImages: true, + inlineStylesheet: false + })`); + await page.waitFor(100); + const snapshot = await page.evaluate('JSON.stringify(snapshot, null, 2);'); + assert(snapshot.includes('"rr_dataURL"')); + assert(snapshot.includes('data:image/webp;base64,')); + }); + + it('correctly saves images in iframes offline', async () => { + const page: puppeteer.Page = await browser.newPage(); + + await page.goto('http://localhost:3030/html/picture-in-frame.html', { + waitUntil: 'load', + }); + await page.waitForSelector('iframe', { timeout: 1000 }); + await waitForRAF(page); // wait for page to render + await page.evaluate(`${code} + rrweb.snapshot(document, { + dataURLOptions: { type: "image/webp", quality: 0.8 }, + inlineImages: true, + inlineStylesheet: false, + onIframeLoad: function(iframe, sn) { + window.snapshot = sn; + } + })`); + await page.waitFor(100); + const snapshot = await page.evaluate( + 'JSON.stringify(window.snapshot, null, 2);', + ); + assert(snapshot.includes('"rr_dataURL"')); + assert(snapshot.includes('data:image/webp;base64,')); + }); + + it('correctly saves blob:images in iframes offline', async () => { + const page: puppeteer.Page = await browser.newPage(); + + await page.goto('http://localhost:3030/html/picture-blob-in-frame.html', { + waitUntil: 'load', + }); + await page.waitForSelector('iframe', { timeout: 1000 }); + await waitForRAF(page); // wait for page to render + await page.evaluate(`${code} + rrweb.snapshot(document, { + dataURLOptions: { type: "image/webp", quality: 0.8 }, + inlineImages: true, + inlineStylesheet: false, + onIframeLoad: function(iframe, sn) { + window.snapshot = sn; + } + })`); + await page.waitFor(100); + const snapshot = await page.evaluate( + 'JSON.stringify(window.snapshot, null, 2);', + ); + assert(snapshot.includes('"rr_dataURL"')); + assert(snapshot.includes('data:image/webp;base64,')); + }); +}); + +describe('iframe integration tests', function (this: ISuite) { + jest.setTimeout(30_000); + let server: ISuite['server']; + let browser: ISuite['browser']; + let code: ISuite['code']; + + beforeAll(async () => { + server = await startServer(); + browser = await puppeteer.launch({ + // headless: false, + }); + + const bundle = await rollup.rollup({ + input: path.resolve(__dirname, '../src/index.ts'), + plugins: [_typescript()], + }); + const { + output: [{ code: _code }], + } = await bundle.generate({ + name: 'rrweb', + format: 'iife', + }); + code = _code; + }); + + afterAll(async () => { + await browser.close(); + await server.close(); + }); + + it('snapshot async iframes', async () => { + const page: puppeteer.Page = await browser.newPage(); + // console for debug + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`http://localhost:3030/iframe-html/main.html`, { + waitUntil: 'load', + }); + const snapshotResult = JSON.stringify( + await page.evaluate(`${code}; + rrweb.snapshot(document); + `), + null, + 2, + ); + expect(snapshotResult).toMatchSnapshot(); + }); +}); + +describe('shadow DOM integration tests', function (this: ISuite) { + jest.setTimeout(30_000); + let server: ISuite['server']; + let browser: ISuite['browser']; + let code: ISuite['code']; + + beforeAll(async () => { + server = await startServer(); + browser = await puppeteer.launch({ + // headless: false, + }); + + const bundle = await rollup.rollup({ + input: path.resolve(__dirname, '../src/index.ts'), + plugins: [_typescript()], + }); + const { + output: [{ code: _code }], + } = await bundle.generate({ + name: 'rrweb', + format: 'iife', + }); + code = _code; + }); + + afterAll(async () => { + await browser.close(); + await server.close(); + }); + + it('snapshot shadow DOM', async () => { + const page: puppeteer.Page = await browser.newPage(); + // console for debug + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`http://localhost:3030/html/shadow-dom.html`, { + waitUntil: 'load', + }); + const snapshotResult = JSON.stringify( + await page.evaluate(`${code}; + rrweb.snapshot(document); + `), + null, + 2, + ); + expect(snapshotResult).toMatchSnapshot(); + }); +}); diff --git a/packages/rrweb-snapshot/test/js/a.js b/packages/rrweb-snapshot/test/js/a.js new file mode 100644 index 00000000..7a776f91 --- /dev/null +++ b/packages/rrweb-snapshot/test/js/a.js @@ -0,0 +1 @@ +var a = 1 + 1; diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts new file mode 100644 index 00000000..75a9ed64 --- /dev/null +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -0,0 +1,127 @@ +/** + * @jest-environment jsdom + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { addHoverClass, buildNodeWithSN, createCache } from '../src/rebuild'; +import { NodeType } from '../src/types'; +import { createMirror, Mirror } from '../src/utils'; + +function getDuration(hrtime: [number, number]) { + const [seconds, nanoseconds] = hrtime; + return seconds * 1000 + nanoseconds / 1000000; +} + +describe('rebuild', function () { + let cache: ReturnType; + let mirror: Mirror; + + beforeEach(() => { + mirror = createMirror(); + cache = createCache(); + }); + + describe('rr_dataURL', function () { + it('should rebuild dataURL', function () { + const dataURI = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + const node = buildNodeWithSN( + { + id: 1, + tagName: 'img', + type: NodeType.Element, + attributes: { + rr_dataURL: dataURI, + src: 'http://example.com/image.png', + }, + childNodes: [], + }, + { + doc: document, + mirror, + hackCss: false, + cache, + }, + ) as HTMLImageElement; + expect(node?.src).toBe(dataURI); + }); + }); + + 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(addHoverClass(cssText, cache)).toEqual(cssText); + }); + + it('can add hover class to css text', () => { + const cssText = '.a:hover { color: white }'; + expect(addHoverClass(cssText, cache)).toEqual( + '.a:hover, .a.\\:hover { color: white }', + ); + }); + + it('can add hover class when there is multi selector', () => { + const cssText = '.a, .b:hover, .c { color: white }'; + expect(addHoverClass(cssText, cache)).toEqual( + '.a, .b:hover, .b.\\:hover, .c { 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(addHoverClass(cssText, cache)).toEqual( + '.a:hover, .a.\\:hover, .a:hover::after, .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(addHoverClass(cssText, cache)).toEqual( + '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(addHoverClass(cssText, cache)).toEqual( + 'a:hover b:hover, a.\\:hover b.\\:hover { color: white }', + ); + }); + + it('will ignore :hover in css value', () => { + const cssText = '.a::after { content: ":hover" }'; + expect(addHoverClass(cssText, cache)).toEqual(cssText); + }); + + it('benchmark', () => { + const cssText = fs.readFileSync( + path.resolve(__dirname, './css/benchmark.css'), + 'utf8', + ); + const start = process.hrtime(); + addHoverClass(cssText, cache); + const end = process.hrtime(start); + const duration = getDuration(end); + expect(duration).toBeLessThan(100); + }); + + it('should be a lot faster to add a hover class to a previously processed css string', () => { + const factor = 100; + + let cssText = fs.readFileSync( + path.resolve(__dirname, './css/benchmark.css'), + 'utf8', + ); + + const start = process.hrtime(); + addHoverClass(cssText, cache); + const end = process.hrtime(start); + + const cachedStart = process.hrtime(); + addHoverClass(cssText, cache); + const cachedEnd = process.hrtime(cachedStart); + + expect(getDuration(cachedEnd) * factor).toBeLessThan(getDuration(end)); + }); + }); +}); diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts new file mode 100644 index 00000000..3042d290 --- /dev/null +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -0,0 +1,224 @@ +/** + * @jest-environment jsdom + */ +import { JSDOM } from 'jsdom'; +import { + absoluteToStylesheet, + serializeNodeWithId, + _isBlockedElement, +} from '../src/snapshot'; +import { serializedNodeWithId } from '../src/types'; +import { Mirror } from '../src/utils'; + +describe('absolute url to stylesheet', () => { + const href = 'http://localhost/css/style.css'; + + it('can handle relative path', () => { + expect(absoluteToStylesheet('url(a.jpg)', href)).toEqual( + `url(http://localhost/css/a.jpg)`, + ); + }); + + it('can handle same level path', () => { + expect(absoluteToStylesheet('url("./a.jpg")', href)).toEqual( + `url("http://localhost/css/a.jpg")`, + ); + }); + + it('can handle parent level path', () => { + expect(absoluteToStylesheet('url("../a.jpg")', href)).toEqual( + `url("http://localhost/a.jpg")`, + ); + }); + + it('can handle absolute path', () => { + expect(absoluteToStylesheet('url("/a.jpg")', href)).toEqual( + `url("http://localhost/a.jpg")`, + ); + }); + + it('can handle external path', () => { + expect(absoluteToStylesheet('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( + `url('http://localhost/css/a.jpg')`, + ); + }); + + it('can handle no quote path', () => { + expect(absoluteToStylesheet('url(./a.jpg)', href)).toEqual( + `url(http://localhost/css/a.jpg)`, + ); + }); + + it('can handle multiple no quote paths', () => { + expect( + absoluteToStylesheet( + 'background-image: url(images/b.jpg);background: #aabbcc url(images/a.jpg) 50% 50% repeat;', + href, + ), + ).toEqual( + `background-image: url(http://localhost/css/images/b.jpg);` + + `background: #aabbcc url(http://localhost/css/images/a.jpg) 50% 50% repeat;`, + ); + }); + + it('can handle data url image', () => { + expect( + absoluteToStylesheet('url(data:image/gif;base64,ABC)', href), + ).toEqual('url(data:image/gif;base64,ABC)'); + expect( + absoluteToStylesheet( + 'url(data:application/font-woff;base64,d09GMgABAAAAAAm)', + href, + ), + ).toEqual('url(data:application/font-woff;base64,d09GMgABAAAAAAm)'); + }); + + it('preserves quotes around inline svgs with spaces', () => { + expect( + absoluteToStylesheet( + "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, + ), + ).toEqual( + "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( + 'url(\'data:image/svg+xml;utf8,\')', + href, + ), + ).toEqual( + 'url(\'data:image/svg+xml;utf8,\')', + ); + expect( + absoluteToStylesheet( + 'url("data:image/svg+xml;utf8,")', + href, + ), + ).toEqual( + 'url("data:image/svg+xml;utf8,")', + ); + }); + it('can handle empty path', () => { + expect(absoluteToStylesheet(`url('')`, href)).toEqual(`url('')`); + }); +}); + +describe('isBlockedElement()', () => { + const subject = (html: string, opt: any = {}) => + _isBlockedElement(render(html), 'highlight-block', opt.blockSelector); + + const render = (html: string): HTMLElement => + JSDOM.fragment(html).querySelector('div')!; + + it('can handle empty elements', () => { + expect(subject('
')).toEqual(false); + }); + + it('blocks prohibited className', () => { + expect(subject('
')).toEqual(true); + }); + + it('does not block random data selector', () => { + expect(subject('
')).toEqual(false); + }); + + it('blocks blocked selector', () => { + expect( + subject('
', { + blockSelector: '[data-highlight-block]', + }), + ).toEqual(true); + }); +}); + +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: {}, + enableStrictPrivacy: true, + }); + }; + + const render = (html: string): HTMLStyleElement => { + document.write(html); + return document.querySelector('style')!; + }; + + 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, + rootId: undefined, + textContent: 'section {color: blue;}body {color: red;}', + type: 3, + }); + }); + + it('should serialize individual text nodes on stylesheets with multiple child nodes', () => { + const styleEl = render(``); + styleEl.append(document.createTextNode('section { color: blue; }')); + expect(serializeNode(styleEl.childNodes[1])).toMatchObject({ + isStyle: true, + rootId: undefined, + textContent: 'section { color: blue; }', + type: 3, + }); + }); +}); + +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, + enableStrictPrivacy: true, + }); + }; + + const render = (html: string): HTMLDivElement => { + document.write(html); + return document.querySelector('div')!; + }; + + it('should serialize scroll positions', () => { + const el = render(`
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
`); + el.scrollTop = 10; + el.scrollLeft = 20; + expect(serializeNode(el)).toMatchObject({ + attributes: { + rr_scrollTop: 10, + rr_scrollLeft: 20, + }, + }); + }); +}); diff --git a/packages/rrweb-snapshot/test/utils.ts b/packages/rrweb-snapshot/test/utils.ts new file mode 100644 index 00000000..43d4484b --- /dev/null +++ b/packages/rrweb-snapshot/test/utils.ts @@ -0,0 +1,11 @@ +import * as puppeteer from 'puppeteer'; + +export async function waitForRAF(page: puppeteer.Page) { + return await page.evaluate(() => { + return new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }); + }); + }); +} diff --git a/packages/rrweb-snapshot/tsconfig.json b/packages/rrweb-snapshot/tsconfig.json new file mode 100644 index 00000000..2b6d7032 --- /dev/null +++ b/packages/rrweb-snapshot/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node", + "noImplicitAny": true, + "strictNullChecks": true, + "removeComments": true, + "preserveConstEnums": true, + "rootDir": "src", + "outDir": "build", + "lib": ["es6", "dom"] + }, + "exclude": ["test"], + "include": ["src"] +} diff --git a/packages/rrweb/.gitignore b/packages/rrweb/.gitignore new file mode 100644 index 00000000..42374e6e --- /dev/null +++ b/packages/rrweb/.gitignore @@ -0,0 +1,17 @@ +.vscode +.idea +node_modules +package-lock.json +# yarn.lock +build +dist +es +lib +typings + +temp + +*.log + +.env +__diff_output__ \ No newline at end of file diff --git a/packages/rrweb/.release-it.json b/packages/rrweb/.release-it.json new file mode 100644 index 00000000..413a96b8 --- /dev/null +++ b/packages/rrweb/.release-it.json @@ -0,0 +1,12 @@ +{ + "non-interactive": true, + "hooks": { + "before:init": ["npm run bundle", "npm run typings"] + }, + "git": { + "requireCleanWorkingDir": false + }, + "github": { + "release": true + } +} \ No newline at end of file diff --git a/packages/rrweb/LICENSE b/packages/rrweb/LICENSE new file mode 100644 index 00000000..fce28eb8 --- /dev/null +++ b/packages/rrweb/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Contributors (https://github.com/rrweb-io/rrweb/graphs/contributors) and SmartX Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/rrweb/jest.config.js b/packages/rrweb/jest.config.js new file mode 100644 index 00000000..29db4e7f --- /dev/null +++ b/packages/rrweb/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/**.test.ts'], + moduleNameMapper: { + '\\.css$': 'identity-obj-proxy', + }, +}; diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json new file mode 100644 index 00000000..a8ff8381 --- /dev/null +++ b/packages/rrweb/package.json @@ -0,0 +1,87 @@ +{ + "name": "@highlight-run/rrweb", + "version": "2.1.10", + "description": "record and replay the web", + "scripts": { + "prepare": "npm run prepack", + "prepack": "npm run bundle", + "test": "npm run bundle:browser && jest --testPathIgnorePatterns test/benchmark", + "test:headless": "PUPPETEER_HEADLESS=true npm run test", + "test:watch": "PUPPETEER_HEADLESS=true npm run test -- --watch", + "repl": "npm run bundle:browser && node scripts/repl.js", + "dev": "yarn bundle:browser --watch", + "bundle:browser": "cross-env BROWSER_ONLY=true rollup --config", + "bundle": "rollup --config", + "typings": "tsc -d --declarationDir typings", + "check-types": "tsc -noEmit", + "prepublish": "npm run typings && npm run bundle", + "lint": "yarn eslint src", + "benchmark": "jest test/benchmark" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/rrweb-io/rrweb.git" + }, + "keywords": [ + "rrweb" + ], + "main": "lib/rrweb-all.js", + "module": "es/rrweb/packages/rrweb/src/entries/all.js", + "unpkg": "dist/rrweb.js", + "sideEffects": false, + "typings": "typings/entries/all.d.ts", + "files": [ + "dist", + "lib", + "es", + "typings" + ], + "author": "yanzhen@smartx.com", + "license": "MIT", + "bugs": { + "url": "https://github.com/rrweb-io/rrweb/issues" + }, + "homepage": "https://github.com/rrweb-io/rrweb#readme", + "devDependencies": { + "@rollup/plugin-node-resolve": "^13.1.3", + "@types/chai": "^4.1.6", + "@types/inquirer": "0.0.43", + "@types/jest": "^27.4.1", + "@types/jest-image-snapshot": "^4.3.1", + "@types/node": "^17.0.21", + "@types/offscreencanvas": "^2019.6.4", + "@types/prettier": "^2.3.2", + "@types/puppeteer": "^5.4.4", + "cross-env": "^5.2.0", + "esbuild": "^0.14.38", + "fast-mhtml": "^1.1.9", + "identity-obj-proxy": "^3.0.0", + "ignore-styles": "^5.0.1", + "inquirer": "^6.2.1", + "jest": "^27.5.1", + "jest-image-snapshot": "^4.5.1", + "jest-snapshot": "^23.6.0", + "prettier": "2.2.1", + "puppeteer": "^9.1.1", + "rollup": "^2.68.0", + "rollup-plugin-esbuild": "^4.9.1", + "rollup-plugin-postcss": "^3.1.1", + "rollup-plugin-rename-node-modules": "^1.3.1", + "rollup-plugin-typescript2": "^0.31.2", + "rollup-plugin-web-worker-loader": "^1.6.1", + "ts-jest": "^27.1.3", + "ts-node": "^10.7.0", + "tslib": "^2.3.1", + "typescript": "^4.7.3" + }, + "dependencies": { + "@highlight-run/rrdom": "0.1.17", + "@highlight-run/rrweb-snapshot": "1.1.31", + "@types/css-font-loading-module": "0.0.7", + "@xstate/fsm": "^1.4.0", + "base64-arraybuffer": "^1.0.1", + "fflate": "^0.4.4", + "mitt": "^3.0.0" + }, + "gitHead": "d5751f9e6c52a7734597c8595caa763d0f4dd4ad" +} diff --git a/packages/rrweb/rollup.config.js b/packages/rrweb/rollup.config.js new file mode 100644 index 00000000..a82a60ee --- /dev/null +++ b/packages/rrweb/rollup.config.js @@ -0,0 +1,251 @@ +import typescript from 'rollup-plugin-typescript2'; +import esbuild from 'rollup-plugin-esbuild'; +import resolve from '@rollup/plugin-node-resolve'; +import postcss from 'rollup-plugin-postcss'; +import renameNodeModules from 'rollup-plugin-rename-node-modules'; +import webWorkerLoader from 'rollup-plugin-web-worker-loader'; +import pkg from './package.json'; + +function toRecordPath(path) { + return path + .replace(/^([\w]+)\//, '$1/record/') + .replace('rrweb', 'rrweb-record'); +} + +function toRecordPackPath(path) { + return path + .replace(/^([\w]+)\//, '$1/record/') + .replace('rrweb', 'rrweb-record-pack'); +} + +function toReplayPath(path) { + return path + .replace(/^([\w]+)\//, '$1/replay/') + .replace('rrweb', 'rrweb-replay'); +} + +function toReplayUnpackPath(path) { + return path + .replace(/^([\w]+)\//, '$1/replay/') + .replace('rrweb', 'rrweb-replay-unpack'); +} + +function toAllPath(path) { + return path.replace('rrweb', 'rrweb-all'); +} + +function toPluginPath(pluginName, stage) { + return (path) => + path + .replace(/^([\w]+)\//, '$1/plugins/') + .replace('rrweb', `${pluginName}-${stage}`); +} + +function toMinPath(path) { + return path.replace(/\.js$/, '.min.js'); +} + +const baseConfigs = [ + // all in one + { + input: './src/entries/all.ts', + name: 'rrweb', + pathFn: toAllPath, + esm: true, + }, + // record only + { + input: './src/record/index.ts', + name: 'rrwebRecord', + pathFn: toRecordPath, + }, + // record and pack + { + input: './src/entries/record-pack.ts', + name: 'rrwebRecord', + pathFn: toRecordPackPath, + }, + // replay only + { + input: './src/replay/index.ts', + name: 'rrwebReplay', + pathFn: toReplayPath, + }, + // replay and unpack + { + input: './src/entries/replay-unpack.ts', + name: 'rrwebReplay', + pathFn: toReplayUnpackPath, + }, + // record and replay + { + input: './src/index.ts', + name: 'rrweb', + pathFn: (p) => p, + }, + // plugins + { + input: './src/plugins/console/record/index.ts', + name: 'rrwebConsoleRecord', + pathFn: toPluginPath('console', 'record'), + }, + { + input: './src/plugins/console/replay/index.ts', + name: 'rrwebConsoleReplay', + pathFn: toPluginPath('console', 'replay'), + }, + { + input: './src/plugins/sequential-id/record/index.ts', + name: 'rrwebSequentialIdRecord', + pathFn: toPluginPath('sequential-id', 'record'), + }, + { + input: './src/plugins/sequential-id/replay/index.ts', + name: 'rrwebSequentialIdReplay', + pathFn: toPluginPath('sequential-id', 'replay'), + }, +]; + +let configs = []; + +function getPlugins(options = {}) { + const { minify = true, sourceMap = false } = options; + return [ + resolve({ browser: true }), + webWorkerLoader({ + targetPlatform: 'browser', + inline: true, + sourceMap, + }), + esbuild({ + minify, + }), + postcss({ + extract: true, + inject: false, + minimize: minify, + sourceMap, + }), + ]; +} + +for (const c of baseConfigs) { + const basePlugins = [ + resolve({ browser: true }), + + // supports bundling `web-worker:..filename` + webWorkerLoader(), + + typescript(), + ]; + const plugins = basePlugins.concat( + postcss({ + extract: true, + inject: false, + minimize: true, + }), + ); + // browser + configs.push({ + input: c.input, + plugins: getPlugins(), + output: [ + { + name: c.name, + format: 'iife', + file: c.pathFn(pkg.unpkg), + }, + ], + }); + // browser + minify + configs.push({ + input: c.input, + plugins: getPlugins({ minify: true, sourceMap: true }), + output: [ + { + name: c.name, + format: 'iife', + file: toMinPath(c.pathFn(pkg.unpkg)), + sourcemap: true, + }, + ], + }); + // CommonJS + configs.push({ + input: c.input, + plugins, + output: [ + { + format: 'cjs', + file: c.pathFn('lib/rrweb.js'), + }, + ], + }); + if (c.esm) { + // ES module + configs.push({ + input: c.input, + plugins, + preserveModules: true, + output: [ + { + format: 'esm', + dir: 'es/rrweb', + plugins: [renameNodeModules('ext')], + }, + ], + }); + } +} + +if (process.env.BROWSER_ONLY) { + const browserOnlyBaseConfigs = [ + { + input: './src/index.ts', + name: 'rrweb', + pathFn: (p) => p, + }, + { + input: './src/plugins/console/record/index.ts', + name: 'rrwebConsoleRecord', + pathFn: toPluginPath('console', 'record'), + }, + { + input: './src/plugins/sequential-id/record/index.ts', + name: 'rrwebSequentialIdRecord', + pathFn: toPluginPath('sequential-id', 'record'), + }, + ]; + + configs = []; + + // browser record + replay, unminified (for profiling and performance testing) + configs.push({ + input: './src/index.ts', + plugins: getPlugins(), + output: [ + { + name: 'rrweb', + format: 'iife', + file: pkg.unpkg, + }, + ], + }); + + for (const c of browserOnlyBaseConfigs) { + configs.push({ + input: c.input, + plugins: getPlugins({ sourceMap: true, minify: true }), + output: [ + { + name: c.name, + format: 'iife', + file: toMinPath(c.pathFn(pkg.unpkg)), + sourcemap: true, + }, + ], + }); + } +} + +export default configs; diff --git a/scripts/repl.ts b/packages/rrweb/scripts/repl.js similarity index 64% rename from scripts/repl.ts rename to packages/rrweb/scripts/repl.js index 41755254..217b709a 100644 --- a/scripts/repl.ts +++ b/packages/rrweb/scripts/repl.js @@ -1,28 +1,42 @@ -/* tslint:disable: no-console */ +/* eslint:disable: no-console */ -import * as fs from 'fs'; -import * as path from 'path'; -import * as EventEmitter from 'events'; -import * as inquirer from 'inquirer'; -import * as puppeteer from 'puppeteer'; -import { eventWithTime } from '../src/types'; +const fs = require('fs'); +const path = require('path'); +const EventEmitter = require('events'); +const inquirer = require('inquirer'); +const puppeteer = require('puppeteer'); const emitter = new EventEmitter(); -function getCode(): string { +function getCode() { const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); return fs.readFileSync(bundlePath, 'utf8'); } -(async () => { +void (async () => { const code = getCode(); - let events: eventWithTime[] = []; + let events = []; - start(); + await start(); + + const fakeGoto = async (page, url) => { + const intercept = async (request) => { + await request.respond({ + status: 200, + contentType: 'text/html', + body: ' ', // non-empty string or page will load indefinitely + }); + }; + await page.setRequestInterception(true); + page.on('request', intercept); + await page.goto(url); + await page.setRequestInterception(false); + page.off('request', intercept); + }; async function start() { events = []; - const { url } = await inquirer.prompt<{ url: string }>([ + const { url } = await inquirer.prompt([ { type: 'input', name: 'url', @@ -35,17 +49,25 @@ function getCode(): string { await record(url); console.log('Ready to record. You can do any interaction on the page.'); - const { shouldReplay } = await inquirer.prompt<{ shouldReplay: boolean }>([ + const { shouldReplay } = await inquirer.prompt([ { - type: 'confirm', + type: 'list', + choices: [ + { name: 'Start replay (default)', value: 'default' }, + { + name: `Start replay on original url (helps when experiencing CORS issues)`, + value: 'replayWithFakeURL', + }, + { name: 'Skip replay', value: false }, + ], name: 'shouldReplay', - message: `Once you want to finish the recording, enter 'y' to start replay: `, + message: `Once you want to finish the recording, choose the following to start replay: `, }, ]); emitter.emit('done', shouldReplay); - const { shouldStore } = await inquirer.prompt<{ shouldStore: boolean }>([ + const { shouldStore } = await inquirer.prompt([ { type: 'confirm', name: 'shouldStore', @@ -57,9 +79,7 @@ function getCode(): string { saveEvents(); } - const { shouldRecordAnother } = await inquirer.prompt<{ - shouldRecordAnother: boolean; - }>([ + const { shouldRecordAnother } = await inquirer.prompt([ { type: 'confirm', name: 'shouldRecordAnother', @@ -74,27 +94,32 @@ function getCode(): string { } } - async function record(url: string) { + async function record(url) { const browser = await puppeteer.launch({ headless: false, defaultViewport: { width: 1600, height: 900, }, - args: ['--start-maximized', '--ignore-certificate-errors'], + args: [ + '--start-maximized', + '--ignore-certificate-errors', + '--no-sandbox', + ], }); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'domcontentloaded', + timeout: 300000, }); - await page.exposeFunction('_replLog', (event: eventWithTime) => { + await page.exposeFunction('_replLog', (event) => { events.push(event); }); await page.evaluate(`;${code} window.__IS_RECORDING__ = true rrweb.record({ - emit: event => window._replLog(event), + emit: event => console.log(event), recordCanvas: true, collectFonts: true }); @@ -114,30 +139,39 @@ function getCode(): string { }); emitter.once('done', async (shouldReplay) => { + const pages = await browser.pages(); + await Promise.all(pages.map((page) => page.close())); await browser.close(); if (shouldReplay) { - await replay(); + await replay(url, shouldReplay === 'replayWithFakeURL'); } }); } - async function replay() { + async function replay(url, useSpoofedUrl) { const browser = await puppeteer.launch({ headless: false, defaultViewport: { width: 1600, height: 900, }, - args: ['--start-maximized'], + args: ['--start-maximized', '--no-sandbox'], }); const page = await browser.newPage(); - await page.goto('about:blank'); + if (useSpoofedUrl) { + await fakeGoto(page, url); + } else { + await page.goto('about:blank'); + } + await page.addStyleTag({ path: path.resolve(__dirname, '../dist/rrweb.min.css'), }); await page.evaluate(`${code} const events = ${JSON.stringify(events)}; - const replayer = new rrweb.Replayer(events); + const replayer = new rrweb.Replayer(events, { + UNSAFE_replayCanvas: true + }); replayer.play(); `); } diff --git a/packages/rrweb/src/entries/all.ts b/packages/rrweb/src/entries/all.ts new file mode 100644 index 00000000..43668271 --- /dev/null +++ b/packages/rrweb/src/entries/all.ts @@ -0,0 +1,6 @@ +export * from '../index'; +export * from '../packer'; +export * from '../plugins/console/record'; +export * from '../plugins/console/replay'; +export { getRecordSequentialIdPlugin } from '../plugins/sequential-id/record'; +export { getReplaySequentialIdPlugin } from '../plugins/sequential-id/replay'; diff --git a/src/entries/record-pack.ts b/packages/rrweb/src/entries/record-pack.ts similarity index 100% rename from src/entries/record-pack.ts rename to packages/rrweb/src/entries/record-pack.ts diff --git a/src/entries/replay-unpack.ts b/packages/rrweb/src/entries/replay-unpack.ts similarity index 100% rename from src/entries/replay-unpack.ts rename to packages/rrweb/src/entries/replay-unpack.ts diff --git a/src/index.ts b/packages/rrweb/src/index.ts similarity index 67% rename from src/index.ts rename to packages/rrweb/src/index.ts index e57b8e42..d8e8dad3 100644 --- a/src/index.ts +++ b/packages/rrweb/src/index.ts @@ -1,6 +1,6 @@ import record from './record'; import { Replayer } from './replay'; -import { mirror } from './utils'; +import { _mirror } from './utils'; import * as utils from './utils'; export { @@ -13,4 +13,11 @@ export { const { addCustomEvent } = record; const { freezePage } = record; -export { record, addCustomEvent, freezePage, Replayer, mirror, utils }; +export { + record, + addCustomEvent, + freezePage, + Replayer, + _mirror as mirror, + utils, +}; diff --git a/src/packer/base.ts b/packages/rrweb/src/packer/base.ts similarity index 81% rename from src/packer/base.ts rename to packages/rrweb/src/packer/base.ts index ef15efb6..00cf3748 100644 --- a/src/packer/base.ts +++ b/packages/rrweb/src/packer/base.ts @@ -1,4 +1,4 @@ -import { eventWithTime } from '../types'; +import type { eventWithTime } from '../types'; export type PackFn = (event: eventWithTime) => string; export type UnpackFn = (raw: string) => eventWithTime; diff --git a/src/packer/index.ts b/packages/rrweb/src/packer/index.ts similarity index 100% rename from src/packer/index.ts rename to packages/rrweb/src/packer/index.ts diff --git a/src/packer/pack.ts b/packages/rrweb/src/packer/pack.ts similarity index 59% rename from src/packer/pack.ts rename to packages/rrweb/src/packer/pack.ts index f62bb925..5fce47cc 100644 --- a/src/packer/pack.ts +++ b/packages/rrweb/src/packer/pack.ts @@ -1,4 +1,4 @@ -import { deflate } from 'pako/dist/pako_deflate'; +import { strFromU8, strToU8, zlibSync } from 'fflate'; import { PackFn, MARK, eventWithTimeAndPacker } from './base'; export const pack: PackFn = (event) => { @@ -6,5 +6,5 @@ export const pack: PackFn = (event) => { ...event, v: MARK, }; - return deflate(JSON.stringify(_e), { to: 'string' }); + return strFromU8(zlibSync(strToU8(JSON.stringify(_e))), true); }; diff --git a/src/packer/unpack.ts b/packages/rrweb/src/packer/unpack.ts similarity index 70% rename from src/packer/unpack.ts rename to packages/rrweb/src/packer/unpack.ts index 4d46f124..47a0f4f4 100644 --- a/src/packer/unpack.ts +++ b/packages/rrweb/src/packer/unpack.ts @@ -1,13 +1,13 @@ -import { inflate } from 'pako/dist/pako_inflate'; +import { strFromU8, strToU8, unzlibSync } from 'fflate'; import { UnpackFn, eventWithTimeAndPacker, MARK } from './base'; -import { eventWithTime } from '../types'; +import type { eventWithTime } from '../types'; export const unpack: UnpackFn = (raw: string) => { if (typeof raw !== 'string') { return raw; } try { - const e: eventWithTime = JSON.parse(raw); + const e: eventWithTime = JSON.parse(raw) as eventWithTime; if (e.timestamp) { return e; } @@ -16,8 +16,8 @@ export const unpack: UnpackFn = (raw: string) => { } try { const e: eventWithTimeAndPacker = JSON.parse( - inflate(raw, { to: 'string' }), - ); + strFromU8(unzlibSync(strToU8(raw, true))), + ) as eventWithTimeAndPacker; if (e.v === MARK) { return e; } diff --git a/packages/rrweb/src/plugins/console/record/error-stack-parser.ts b/packages/rrweb/src/plugins/console/record/error-stack-parser.ts new file mode 100644 index 00000000..4c0e49c3 --- /dev/null +++ b/packages/rrweb/src/plugins/console/record/error-stack-parser.ts @@ -0,0 +1,248 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */ +/** + * Class StackFrame is a fork of https://github.com/stacktracejs/stackframe/blob/master/stackframe.js + * I fork it because: + * 1. There are some build issues when importing this package. + * 2. Rewrites into typescript give us a better type interface. + * 3. StackFrame contains some functions we don't need. + */ +export class StackFrame { + private fileName: string; + private functionName: string; + private lineNumber?: number; + private columnNumber?: number; + + constructor(obj: { + fileName?: string; + functionName?: string; + lineNumber?: number; + columnNumber?: number; + }) { + this.fileName = obj.fileName || ''; + this.functionName = obj.functionName || ''; + this.lineNumber = obj.lineNumber; + this.columnNumber = obj.columnNumber; + } + + toString() { + const lineNumber = this.lineNumber || ''; + const columnNumber = this.columnNumber || ''; + if (this.functionName) + return `${this.functionName} (${this.fileName}:${lineNumber}:${columnNumber})`; + return `${this.fileName}:${lineNumber}:${columnNumber}`; + } +} + +/** + * ErrorStackParser is a fork of https://github.com/stacktracejs/error-stack-parser/blob/master/error-stack-parser.js + * I fork it because: + * 1. There are some build issues when importing this package. + * 2. Rewrites into typescript give us a better type interface. + */ +const FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/; +const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; +const SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/; +export const ErrorStackParser = { + /** + * Given an Error object, extract the most information from it. + */ + parse: function (error: Error): StackFrame[] { + // https://github.com/rrweb-io/rrweb/issues/782 + if (!error) { + return []; + } + if ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + typeof error.stacktrace !== 'undefined' || + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + typeof error['opera#sourceloc'] !== 'undefined' + ) { + return this.parseOpera( + error as { + stacktrace?: string; + message: string; + stack?: string; + }, + ); + } else if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) { + return this.parseV8OrIE(error as { stack: string }); + } else if (error.stack) { + return this.parseFFOrSafari(error as { stack: string }); + } else { + throw new Error('Cannot parse given Error object'); + } + }, + // Separate line and column numbers from a string of the form: (URI:Line:Column) + extractLocation: function (urlLike: string) { + // Fail-fast but return locations like "(native)" + if (urlLike.indexOf(':') === -1) { + return [urlLike]; + } + + const regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/; + const parts = regExp.exec(urlLike.replace(/[()]/g, '')); + if (!parts) throw new Error(`Cannot parse given url: ${urlLike}`); + return [parts[1], parts[2] || undefined, parts[3] || undefined]; + }, + parseV8OrIE: function (error: { stack: string }) { + const filtered = error.stack.split('\n').filter(function (line) { + return !!line.match(CHROME_IE_STACK_REGEXP); + }, this); + + return filtered.map(function (line) { + if (line.indexOf('(eval ') > -1) { + // Throw away eval information until we implement stacktrace.js/stackframe#8 + line = line + .replace(/eval code/g, 'eval') + .replace(/(\(eval at [^()]*)|(\),.*$)/g, ''); + } + let sanitizedLine = line.replace(/^\s+/, '').replace(/\(eval code/g, '('); + + // capture and preseve the parenthesized location "(/foo/my bar.js:12:87)" in + // case it has spaces in it, as the string is split on \s+ later on + const location = sanitizedLine.match(/ (\((.+):(\d+):(\d+)\)$)/); + + // remove the parenthesized location from the line, if it was matched + sanitizedLine = location + ? sanitizedLine.replace(location[0], '') + : sanitizedLine; + + const tokens = sanitizedLine.split(/\s+/).slice(1); + // if a location was matched, pass it to extractLocation() otherwise pop the last token + const locationParts = this.extractLocation( + location ? location[1] : tokens.pop(), + ); + const functionName = tokens.join(' ') || undefined; + const fileName = + ['eval', ''].indexOf(locationParts[0]) > -1 + ? undefined + : locationParts[0]; + + return new StackFrame({ + functionName, + fileName, + lineNumber: locationParts[1], + columnNumber: locationParts[2], + }); + }, this); + }, + parseFFOrSafari: function (error: { stack: string }) { + const filtered = error.stack.split('\n').filter(function (line) { + return !line.match(SAFARI_NATIVE_CODE_REGEXP); + }, this); + + return filtered.map(function (line) { + // Throw away eval information until we implement stacktrace.js/stackframe#8 + if (line.indexOf(' > eval') > -1) { + line = line.replace( + / line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, + ':$1', + ); + } + + if (line.indexOf('@') === -1 && line.indexOf(':') === -1) { + // Safari eval frames only have function names and nothing else + return new StackFrame({ + functionName: line, + }); + } else { + const functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/; + const matches = line.match(functionNameRegex); + const functionName = matches && matches[1] ? matches[1] : undefined; + const locationParts = this.extractLocation( + line.replace(functionNameRegex, ''), + ); + + return new StackFrame({ + functionName, + fileName: locationParts[0], + lineNumber: locationParts[1], + columnNumber: locationParts[2], + }); + } + }, this); + }, + parseOpera: function (e: { + stacktrace?: string; + message: string; + stack?: string; + }): StackFrame[] { + if ( + !e.stacktrace || + (e.message.indexOf('\n') > -1 && + e.message.split('\n').length > e.stacktrace.split('\n').length) + ) { + return this.parseOpera9(e as { message: string }); + } else if (!e.stack) { + return this.parseOpera10(e as { stacktrace: string }); + } else { + return this.parseOpera11(e as { stack: string }); + } + }, + parseOpera9: function (e: { message: string }) { + const lineRE = /Line (\d+).*script (?:in )?(\S+)/i; + const lines = e.message.split('\n'); + const result = []; + + for (let i = 2, len = lines.length; i < len; i += 2) { + const match = lineRE.exec(lines[i]); + if (match) { + result.push( + new StackFrame({ + fileName: match[2], + lineNumber: parseFloat(match[1]), + }), + ); + } + } + + return result; + }, + parseOpera10: function (e: { stacktrace: string }) { + const lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; + const lines = e.stacktrace.split('\n'); + const result = []; + + for (let i = 0, len = lines.length; i < len; i += 2) { + const match = lineRE.exec(lines[i]); + if (match) { + result.push( + new StackFrame({ + functionName: match[3] || undefined, + fileName: match[2], + lineNumber: parseFloat(match[1]), + }), + ); + } + } + + return result; + }, + // Opera 10.65+ Error.stack very similar to FF/Safari + parseOpera11: function (error: { stack: string }) { + const filtered = error.stack.split('\n').filter(function (line) { + return ( + !!line.match(FIREFOX_SAFARI_STACK_REGEXP) && + !line.match(/^Error created at/) + ); + }, this); + + return filtered.map(function (line: string) { + const tokens = line.split('@'); + const locationParts = this.extractLocation(tokens.pop()); + const functionCall = tokens.shift() || ''; + const functionName = + functionCall + .replace(//, '$2') + .replace(/\([^)]*\)/g, '') || undefined; + return new StackFrame({ + functionName, + fileName: locationParts[0], + lineNumber: locationParts[1], + columnNumber: locationParts[2], + }); + }, this); + }, +}; diff --git a/packages/rrweb/src/plugins/console/record/index.ts b/packages/rrweb/src/plugins/console/record/index.ts new file mode 100644 index 00000000..cb50ee89 --- /dev/null +++ b/packages/rrweb/src/plugins/console/record/index.ts @@ -0,0 +1,203 @@ +import type { listenerHandler, RecordPlugin, IWindow } from '../../../types'; +import { patch } from '../../../utils'; +import { ErrorStackParser, StackFrame } from './error-stack-parser'; +import { stringify } from './stringify'; + +export type StringifyOptions = { + // limit of string length + stringLengthLimit?: number; + /** + * limit of number of keys in an object + * if an object contains more keys than this limit, we would call its toString function directly + */ + numOfKeysLimit: number; + /** + * limit number of depth in an object + * if an object is too deep, toString process may cause browser OOM + */ + depthOfLimit: number; +}; + +type LogRecordOptions = { + level?: LogLevel[]; + lengthThreshold?: number; + stringifyOptions?: StringifyOptions; + logger?: Logger | 'console'; +}; + +const defaultLogOptions: LogRecordOptions = { + level: [ + 'assert', + 'clear', + 'count', + 'countReset', + 'debug', + 'dir', + 'dirxml', + 'error', + 'group', + 'groupCollapsed', + 'groupEnd', + 'info', + 'log', + 'table', + 'time', + 'timeEnd', + 'timeLog', + 'trace', + 'warn', + ], + lengthThreshold: 1000, + logger: 'console', +}; + +export type LogData = { + level: LogLevel; + trace: string[]; + payload: string[]; +}; + +type logCallback = (p: LogData) => void; + +/* fork from interface Console */ +// all kinds of console functions +export type Logger = { + assert?: typeof console.assert; + clear?: typeof console.clear; + count?: typeof console.count; + countReset?: typeof console.countReset; + debug?: typeof console.debug; + dir?: typeof console.dir; + dirxml?: typeof console.dirxml; + error?: typeof console.error; + group?: typeof console.group; + groupCollapsed?: typeof console.groupCollapsed; + groupEnd?: () => void; + info?: typeof console.info; + log?: typeof console.log; + table?: typeof console.table; + time?: typeof console.time; + timeEnd?: typeof console.timeEnd; + timeLog?: typeof console.timeLog; + trace?: typeof console.trace; + warn?: typeof console.warn; +}; + +export type LogLevel = keyof Logger; + +function initLogObserver( + cb: logCallback, + win: IWindow, // top window or in an iframe + options: LogRecordOptions, +): listenerHandler { + const logOptions = (options + ? Object.assign({}, defaultLogOptions, options) + : defaultLogOptions) as { + level: LogLevel[]; + lengthThreshold: number; + stringifyOptions?: StringifyOptions; + logger: Logger | 'console'; + }; + const loggerType = logOptions.logger; + if (!loggerType) { + return () => { + // + }; + } + let logger: Logger; + if (typeof loggerType === 'string') { + logger = win[loggerType]; + } else { + logger = loggerType; + } + let logCount = 0; + const cancelHandlers: listenerHandler[] = []; + // add listener to thrown errors + if (logOptions.level.includes('error')) { + if (window) { + const errorHandler = (event: ErrorEvent) => { + const message = event.message, + error = event.error as Error; + const trace: string[] = ErrorStackParser.parse( + error, + ).map((stackFrame: StackFrame) => stackFrame.toString()); + const payload = [stringify(message, logOptions.stringifyOptions)]; + cb({ + level: 'error', + trace, + payload, + }); + }; + window.addEventListener('error', errorHandler); + cancelHandlers.push(() => { + if (window) window.removeEventListener('error', errorHandler); + }); + } + } + for (const levelType of logOptions.level) { + cancelHandlers.push(replace(logger, levelType)); + } + return () => { + cancelHandlers.forEach((h) => h()); + }; + + /** + * replace the original console function and record logs + * @param logger - the logger object such as Console + * @param level - the name of log function to be replaced + */ + function replace(_logger: Logger, level: LogLevel) { + if (!_logger[level]) { + return () => { + // + }; + } + // replace the logger.{level}. return a restore function + return patch( + _logger, + level, + (original: (...args: Array) => void) => { + return (...args: Array) => { + original.apply(this, args); + try { + 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) => + stringify(s, logOptions.stringifyOptions), + ); + logCount++; + if (logCount < logOptions.lengthThreshold) { + cb({ + level, + trace, + payload, + }); + } else if (logCount === logOptions.lengthThreshold) { + // notify the user + cb({ + level: 'warn', + trace: [], + payload: [ + stringify('The number of log records reached the threshold.'), + ], + }); + } + } catch (error) { + original('rrweb logger error:', error, ...args); + } + }; + }, + ); + } +} + +export const PLUGIN_NAME = 'rrweb/console@1'; + +export const getRecordConsolePlugin: ( + options?: LogRecordOptions, +) => RecordPlugin = (options) => ({ + name: PLUGIN_NAME, + observer: initLogObserver, + options: options, +}); diff --git a/packages/rrweb/src/plugins/console/record/stringify.ts b/packages/rrweb/src/plugins/console/record/stringify.ts new file mode 100644 index 00000000..eef8c38a --- /dev/null +++ b/packages/rrweb/src/plugins/console/record/stringify.ts @@ -0,0 +1,191 @@ +/** + * this file is used to serialize log message to string + * + */ + +import type { StringifyOptions } from './index'; + +/** + * transfer the node path in Event to string + * @param node - the first node in a node path array + */ +function pathToSelector(node: HTMLElement): string | '' { + if (!node || !node.outerHTML) { + return ''; + } + + let path = ''; + while (node.parentElement) { + let name = node.localName; + if (!name) { + break; + } + name = name.toLowerCase(); + const parent = node.parentElement; + + const domSiblings = []; + + if (parent.children && parent.children.length > 0) { + for (let i = 0; i < parent.children.length; i++) { + const sibling = parent.children[i]; + if (sibling.localName && sibling.localName.toLowerCase) { + if (sibling.localName.toLowerCase() === name) { + domSiblings.push(sibling); + } + } + } + } + + if (domSiblings.length > 1) { + name += `:eq(${domSiblings.indexOf(node)})`; + } + path = name + (path ? '>' + path : ''); + node = parent; + } + + return path; +} + +/** + * judge is object + */ +function isObject(obj: unknown): boolean { + return Object.prototype.toString.call(obj) === '[object Object]'; +} + +/** + * judge the object's depth + */ +function isObjTooDeep(obj: Record, limit: number): boolean { + if (limit === 0) { + return true; + } + + const keys = Object.keys(obj); + for (const key of keys) { + if ( + isObject(obj[key]) && + isObjTooDeep(obj[key] as Record, limit - 1) + ) { + return true; + } + } + + return false; +} + +/** + * stringify any js object + * @param obj - the object to stringify + */ +export function stringify( + obj: unknown, + stringifyOptions?: StringifyOptions, +): string { + const options: StringifyOptions = { + numOfKeysLimit: 50, + depthOfLimit: 4, + }; + Object.assign(options, stringifyOptions); + const stack: unknown[] = []; + const keys: unknown[] = []; + return JSON.stringify( + obj, + function (key, value: string | object | null | undefined) { + /** + * forked from https://github.com/moll/json-stringify-safe/blob/master/stringify.js + * to deCycle the object + */ + if (stack.length > 0) { + const thisPos = stack.indexOf(this); + ~thisPos ? stack.splice(thisPos + 1) : stack.push(this); + ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key); + if (~stack.indexOf(value)) { + if (stack[0] === value) { + value = '[Circular ~]'; + } else { + value = + '[Circular ~.' + + keys.slice(0, stack.indexOf(value)).join('.') + + ']'; + } + } + } else { + stack.push(value); + } + /* END of the FORK */ + + if (value === null) return value; + if (value === undefined) return 'undefined'; + if (shouldIgnore(value as object)) { + return toString(value as object); + } + if (value instanceof Event) { + const eventResult: Record = {}; + for (const eventKey in value) { + const eventValue = ((value as unknown) as Record)[ + eventKey + ]; + if (Array.isArray(eventValue)) { + eventResult[eventKey] = pathToSelector( + (eventValue.length ? eventValue[0] : null) as HTMLElement, + ); + } else { + eventResult[eventKey] = eventValue; + } + } + return eventResult; + } else if (value instanceof Node) { + if (value instanceof HTMLElement) { + return value ? value.outerHTML : ''; + } + return value.nodeName; + } else if (value instanceof Error) { + return value.stack + ? value.stack + '\nEnd of stack for Error object' + : value.name + ': ' + value.message; + } + return value; + }, + ); + + /** + * whether we should ignore obj's info and call toString() function instead + */ + function shouldIgnore(_obj: object): boolean { + // outof keys limit + if (isObject(_obj) && Object.keys(_obj).length > options.numOfKeysLimit) { + return true; + } + + // is function + if (typeof _obj === 'function') { + return true; + } + + /** + * judge object's depth to avoid browser's OOM + * + * issues: https://github.com/rrweb-io/rrweb/issues/653 + */ + if ( + isObject(_obj) && + isObjTooDeep(_obj as Record, options.depthOfLimit) + ) { + return true; + } + + return false; + } + + /** + * limit the toString() result according to option + */ + function toString(_obj: object): string { + let str = _obj.toString(); + if (options.stringLengthLimit && str.length > options.stringLengthLimit) { + str = `${str.slice(0, options.stringLengthLimit)}...`; + } + return str; + } +} diff --git a/packages/rrweb/src/plugins/console/replay/index.ts b/packages/rrweb/src/plugins/console/replay/index.ts new file mode 100644 index 00000000..75d8997b --- /dev/null +++ b/packages/rrweb/src/plugins/console/replay/index.ts @@ -0,0 +1,144 @@ +import { LogLevel, LogData, PLUGIN_NAME } from '../record'; +import { + eventWithTime, + EventType, + IncrementalSource, + ReplayPlugin, +} from '../../../types'; + +/** + * define an interface to replay log records + * (data: logData) => void> function to display the log data + */ +type ReplayLogger = Partial void>>; + +type LogReplayConfig = { + level?: LogLevel[]; + replayLogger?: ReplayLogger; +}; + +const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; +type PatchedConsoleLog = { + [ORIGINAL_ATTRIBUTE_NAME]: typeof console.log; +}; + +const defaultLogConfig: LogReplayConfig = { + level: [ + 'assert', + 'clear', + 'count', + 'countReset', + 'debug', + 'dir', + 'dirxml', + 'error', + 'group', + 'groupCollapsed', + 'groupEnd', + 'info', + 'log', + 'table', + 'time', + 'timeEnd', + 'timeLog', + 'trace', + 'warn', + ], + replayLogger: undefined, +}; + +class LogReplayPlugin { + private config: LogReplayConfig; + + constructor(config?: LogReplayConfig) { + this.config = Object.assign(defaultLogConfig, config); + } + + /** + * generate a console log replayer which implement the interface ReplayLogger + */ + public getConsoleLogger(): ReplayLogger { + const replayLogger: ReplayLogger = {}; + for (const level of this.config.level!) { + if (level === 'trace') { + replayLogger[level] = (data: LogData) => { + const logger = ((console.log as unknown) as PatchedConsoleLog)[ + ORIGINAL_ATTRIBUTE_NAME + ] + ? ((console.log as unknown) as PatchedConsoleLog)[ + ORIGINAL_ATTRIBUTE_NAME + ] + : console.log; + logger( + ...data.payload.map((s) => JSON.parse(s) as object), + this.formatMessage(data), + ); + }; + } else { + replayLogger[level] = (data: LogData) => { + const logger = ((console[level] as unknown) as PatchedConsoleLog)[ + ORIGINAL_ATTRIBUTE_NAME + ] + ? ((console[level] as unknown) as PatchedConsoleLog)[ + ORIGINAL_ATTRIBUTE_NAME + ] + : console[level]; + logger( + ...data.payload.map((s) => JSON.parse(s) as object), + this.formatMessage(data), + ); + }; + } + } + return replayLogger; + } + + /** + * format the trace data to a string + * @param data - the log data + */ + private formatMessage(data: LogData): string { + if (data.trace.length === 0) { + return ''; + } + const stackPrefix = '\n\tat '; + let result = stackPrefix; + result += data.trace.join(stackPrefix); + return result; + } +} + +export const getReplayConsolePlugin: ( + options?: LogReplayConfig, +) => ReplayPlugin = (options) => { + const replayLogger = + options?.replayLogger || new LogReplayPlugin(options).getConsoleLogger(); + + return { + handler(event: eventWithTime, _isSync, context) { + let logData: LogData | null = null; + if ( + event.type === EventType.IncrementalSnapshot && + event.data.source === (IncrementalSource.Log as IncrementalSource) + ) { + logData = (event.data as unknown) as LogData; + } else if ( + event.type === EventType.Plugin && + event.data.plugin === PLUGIN_NAME + ) { + logData = event.data.payload as LogData; + } + if (logData) { + try { + if (typeof replayLogger[logData.level] === 'function') { + replayLogger[logData.level]!(logData); + } + } catch (error) { + if (context.replayer.config.showWarning) { + console.warn(error); + } + } + } + }, + }; +}; diff --git a/packages/rrweb/src/plugins/sequential-id/record/index.ts b/packages/rrweb/src/plugins/sequential-id/record/index.ts new file mode 100644 index 00000000..fa5a616c --- /dev/null +++ b/packages/rrweb/src/plugins/sequential-id/record/index.ts @@ -0,0 +1,31 @@ +import type { RecordPlugin } from '../../../types'; + +export type SequentialIdOptions = { + key: string; +}; + +const defaultOptions: SequentialIdOptions = { + key: '_sid', +}; + +export const PLUGIN_NAME = 'rrweb/sequential-id@1'; + +export const getRecordSequentialIdPlugin: ( + options?: Partial, +) => RecordPlugin = (options) => { + const _options = options + ? Object.assign({}, defaultOptions, options) + : defaultOptions; + let id = 0; + + return { + name: PLUGIN_NAME, + eventProcessor(event) { + Object.assign(event, { + [_options.key]: ++id, + }); + return event; + }, + options: _options, + }; +}; diff --git a/packages/rrweb/src/plugins/sequential-id/replay/index.ts b/packages/rrweb/src/plugins/sequential-id/replay/index.ts new file mode 100644 index 00000000..7e3644af --- /dev/null +++ b/packages/rrweb/src/plugins/sequential-id/replay/index.ts @@ -0,0 +1,39 @@ +import type { SequentialIdOptions } from '../record'; +import type { ReplayPlugin, eventWithTime } from '../../../types'; + +type Options = SequentialIdOptions & { + warnOnMissingId: boolean; +}; + +const defaultOptions: Options = { + key: '_sid', + warnOnMissingId: true, +}; + +export const getReplaySequentialIdPlugin: ( + options?: Partial, +) => ReplayPlugin = (options) => { + const { key, warnOnMissingId } = options + ? Object.assign({}, defaultOptions, options) + : defaultOptions; + let currentId = 1; + + return { + handler(event: eventWithTime) { + if (key in event) { + const id = ((event as unknown) as Record)[key]; + if (id !== currentId) { + console.error( + `[sequential-id-plugin]: expect to get an id with value "${currentId}", but got "${id}"`, + ); + } else { + currentId++; + } + } else if (warnOnMissingId) { + console.warn( + `[sequential-id-plugin]: failed to get id in key: "${key}"`, + ); + } + }, + }; +}; diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts new file mode 100644 index 00000000..8e1480ca --- /dev/null +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -0,0 +1,41 @@ +import type { Mirror, serializedNodeWithId } from '@highlight-run/rrweb-snapshot'; +import type { mutationCallBack } from '../types'; + +export class IframeManager { + private iframes: WeakMap = new WeakMap(); + private mutationCb: mutationCallBack; + private loadListener?: (iframeEl: HTMLIFrameElement) => unknown; + + constructor(options: { mutationCb: mutationCallBack }) { + this.mutationCb = options.mutationCb; + } + + public addIframe(iframeEl: HTMLIFrameElement) { + this.iframes.set(iframeEl, true); + } + + public addLoadListener(cb: (iframeEl: HTMLIFrameElement) => unknown) { + this.loadListener = cb; + } + + public attachIframe( + iframeEl: HTMLIFrameElement, + childSn: serializedNodeWithId, + mirror: Mirror, + ) { + this.mutationCb({ + adds: [ + { + parentId: mirror.getId(iframeEl), + nextId: null, + node: childSn, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }); + this.loadListener?.(iframeEl); + } +} diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts new file mode 100644 index 00000000..055622c5 --- /dev/null +++ b/packages/rrweb/src/record/index.ts @@ -0,0 +1,555 @@ +import { + snapshot, + MaskInputOptions, + SlimDOMOptions, + createMirror, +} from '@highlight-run/rrweb-snapshot'; +import { initObservers, mutationBuffers } from './observer'; +import { + on, + getWindowWidth, + getWindowHeight, + polyfill, + hasShadowRoot, + isSerializedIframe, + isSerializedStylesheet, +} from '../utils'; +import { + EventType, + event, + eventWithTime, + recordOptions, + IncrementalSource, + listenerHandler, + mutationCallbackParam, + scrollCallback, + canvasMutationParam, +} from '../types'; +import { IframeManager } from './iframe-manager'; +import { ShadowDomManager } from './shadow-dom-manager'; +import { CanvasManager } from './observers/canvas/canvas-manager'; +import { StylesheetManager } from './stylesheet-manager'; +import { obfuscateText } from '@highlight-run/rrweb-snapshot'; + +function wrapEvent(e: event): eventWithTime { + return { + ...e, + timestamp: Date.now(), + }; +} + +let wrappedEmit!: (e: eventWithTime, isCheckout?: boolean) => void; + +let takeFullSnapshot!: (isCheckout?: boolean) => void; + +const mirror = createMirror(); +function record( + options: recordOptions = {}, +): listenerHandler | undefined { + const { + emit, + checkoutEveryNms, + checkoutEveryNth, + blockClass = 'highlight-block', + blockSelector = null, + ignoreClass = 'highlight-ignore', + maskTextClass = 'highlight-mask', + maskTextSelector = null, + inlineStylesheet = true, + maskAllInputs, + maskInputOptions: _maskInputOptions, + slimDOMOptions: _slimDOMOptions, + maskInputFn, + maskTextFn = obfuscateText, + hooks, + packFn, + sampling = {}, + mousemoveWait, + recordCanvas = false, + userTriggeredOnInput = false, + collectFonts = false, + inlineImages = false, + plugins, + keepIframeSrcFn = () => false, + enableStrictPrivacy = false, + } = options; + // runtime checks for user options + if (!emit) { + throw new Error('emit function is required'); + } + // move departed options to new options + if (mousemoveWait !== undefined && sampling.mousemove === undefined) { + sampling.mousemove = mousemoveWait; + } + + // reset mirror in case `record` this was called earlier + mirror.reset(); + + const maskInputOptions: MaskInputOptions = + maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : _maskInputOptions !== undefined + ? _maskInputOptions + : { password: true }; + + const slimDOMOptions: SlimDOMOptions = + _slimDOMOptions === true || _slimDOMOptions === 'all' + ? { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaVerification: true, + // the following are off for slimDOMOptions === true, + // as they destroy some (hidden) info: + headMetaAuthorship: _slimDOMOptions === 'all', + headMetaDescKeywords: _slimDOMOptions === 'all', + } + : _slimDOMOptions + ? _slimDOMOptions + : {}; + + polyfill(); + + let lastFullSnapshotEvent: eventWithTime; + let incrementalSnapshotCount = 0; + const eventProcessor = (e: eventWithTime): T => { + for (const plugin of plugins || []) { + if (plugin.eventProcessor) { + e = plugin.eventProcessor(e); + } + } + if (packFn) { + e = (packFn(e) as unknown) as eventWithTime; + } + return (e as unknown) as T; + }; + wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => { + if ( + mutationBuffers[0]?.isFrozen() && + e.type !== EventType.FullSnapshot && + !( + e.type === EventType.IncrementalSnapshot && + e.data.source === IncrementalSource.Mutation + ) + ) { + // we've got a user initiated event so first we need to apply + // all DOM changes that have been buffering during paused state + mutationBuffers.forEach((buf) => buf.unfreeze()); + } + + emit(eventProcessor(e), isCheckout); + if (e.type === EventType.FullSnapshot) { + lastFullSnapshotEvent = e; + incrementalSnapshotCount = 0; + } else if (e.type === EventType.IncrementalSnapshot) { + // attach iframe should be considered as full snapshot + if ( + e.data.source === IncrementalSource.Mutation && + e.data.isAttachIframe + ) { + return; + } + + incrementalSnapshotCount++; + const exceedCount = + checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth; + const exceedTime = + checkoutEveryNms && + e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms; + if (exceedCount || exceedTime) { + takeFullSnapshot(true); + } + } + }; + + const wrappedMutationEmit = (m: mutationCallbackParam) => { + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + ...m, + }, + }), + ); + }; + const wrappedScrollEmit: scrollCallback = (p) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Scroll, + ...p, + }, + }), + ); + const wrappedCanvasMutationEmit = (p: canvasMutationParam) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.CanvasMutation, + ...p, + }, + }), + ); + + const iframeManager = new IframeManager({ + mutationCb: wrappedMutationEmit, + }); + + const stylesheetManager = new StylesheetManager({ + mutationCb: wrappedMutationEmit, + }); + + const canvasManager = new CanvasManager({ + recordCanvas, + mutationCb: wrappedCanvasMutationEmit, + win: window, + blockClass, + mirror, + sampling: sampling?.canvas?.fps, + resizeQuality: sampling?.canvas?.resizeQuality, + resizeFactor: sampling?.canvas?.resizeFactor, + maxSnapshotDimension: sampling?.canvas?.maxSnapshotDimension, + }); + + const shadowDomManager = new ShadowDomManager({ + mutationCb: wrappedMutationEmit, + scrollCb: wrappedScrollEmit, + bypassOptions: { + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + recordCanvas, + inlineImages, + sampling, + slimDOMOptions, + iframeManager, + stylesheetManager, + canvasManager, + keepIframeSrcFn, + enableStrictPrivacy, + }, + mirror, + }); + + takeFullSnapshot = (isCheckout = false) => { + wrappedEmit( + wrapEvent({ + type: EventType.Meta, + data: { + href: window.location.href, + width: getWindowWidth(), + height: getWindowHeight(), + }, + }), + isCheckout, + ); + + mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting + const node = snapshot(document, { + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskAllInputs: maskInputOptions, + maskTextFn, + slimDOM: slimDOMOptions, + recordCanvas, + inlineImages, + enableStrictPrivacy, + onSerialize: (n) => { + if (isSerializedIframe(n, mirror)) { + iframeManager.addIframe(n as HTMLIFrameElement); + } + if (isSerializedStylesheet(n, mirror)) { + stylesheetManager.addStylesheet(n as HTMLLinkElement); + } + if (hasShadowRoot(n)) { + shadowDomManager.addShadowRoot(n.shadowRoot, document); + } + }, + onIframeLoad: (iframe, childSn) => { + iframeManager.attachIframe(iframe, childSn, mirror); + shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (linkEl, childSn) => { + stylesheetManager.attachStylesheet(linkEl, childSn, mirror); + }, + keepIframeSrcFn, + }); + + if (!node) { + return console.warn('Failed to snapshot the document'); + } + + wrappedEmit( + wrapEvent({ + type: EventType.FullSnapshot, + data: { + node, + initialOffset: { + left: + window.pageXOffset !== undefined + ? window.pageXOffset + : document?.documentElement.scrollLeft || + document?.body?.parentElement?.scrollLeft || + document?.body.scrollLeft || + 0, + top: + window.pageYOffset !== undefined + ? window.pageYOffset + : document?.documentElement.scrollTop || + document?.body?.parentElement?.scrollTop || + document?.body.scrollTop || + 0, + }, + }, + }), + ); + mutationBuffers.forEach((buf) => buf.unlock()); // generate & emit any mutations that happened during snapshotting, as can now apply against the newly built mirror + }; + + try { + const handlers: listenerHandler[] = []; + handlers.push( + on('DOMContentLoaded', () => { + wrappedEmit( + wrapEvent({ + type: EventType.DomContentLoaded, + data: {}, + }), + ); + }), + ); + + const observe = (doc: Document) => { + return initObservers( + { + mutationCb: wrappedMutationEmit, + mousemoveCb: (positions, source) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source, + positions, + }, + }), + ), + mouseInteractionCb: (d) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.MouseInteraction, + ...d, + }, + }), + ), + scrollCb: wrappedScrollEmit, + viewportResizeCb: (d) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.ViewportResize, + ...d, + }, + }), + ), + inputCb: (v) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Input, + ...v, + }, + }), + ), + mediaInteractionCb: (p) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.MediaInteraction, + ...p, + }, + }), + ), + styleSheetRuleCb: (r) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleSheetRule, + ...r, + }, + }), + ), + styleDeclarationCb: (r) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleDeclaration, + ...r, + }, + }), + ), + canvasMutationCb: wrappedCanvasMutationEmit, + fontCb: (p) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Font, + ...p, + }, + }), + ), + blockClass, + ignoreClass, + maskTextClass, + maskTextSelector, + maskInputOptions, + inlineStylesheet, + sampling, + recordCanvas, + inlineImages, + userTriggeredOnInput, + collectFonts, + doc, + maskInputFn, + maskTextFn, + keepIframeSrcFn, + blockSelector, + slimDOMOptions, + mirror, + iframeManager, + stylesheetManager, + shadowDomManager, + canvasManager, + enableStrictPrivacy, + plugins: + plugins + ?.filter((p) => p.observer) + ?.map((p) => ({ + observer: p.observer!, + options: p.options, + callback: (payload: object) => + wrappedEmit( + wrapEvent({ + type: EventType.Plugin, + data: { + plugin: p.name, + payload, + }, + }), + ), + })) || [], + }, + hooks, + ); + }; + + iframeManager.addLoadListener((iframeEl) => { + handlers.push(observe(iframeEl.contentDocument!)); + }); + + const init = () => { + takeFullSnapshot(); + handlers.push(observe(document)); + }; + if ( + document.readyState === 'interactive' || + document.readyState === 'complete' + ) { + init(); + } else { + handlers.push( + on( + 'load', + () => { + wrappedEmit( + wrapEvent({ + type: EventType.Load, + data: {}, + }), + ); + init(); + }, + window, + ), + ); + } + return () => { + handlers.forEach((h) => h()); + }; + } catch (error) { + // TODO: handle internal error + console.warn(error); + } +} + +record.addCustomEvent = (tag: string, payload: T) => { + if (!wrappedEmit) { + /* Highlight Code - disable this warning */ + // throw new Error('please add custom event after start recording'); + return; + } + wrappedEmit( + wrapEvent({ + type: EventType.Custom, + data: { + tag, + payload, + }, + }), + ); +}; + +record.freezePage = () => { + mutationBuffers.forEach((buf) => buf.freeze()); +}; + +record.takeFullSnapshot = (isCheckout?: boolean) => { + if (!takeFullSnapshot) { + throw new Error('please take full snapshot after start recording'); + } + takeFullSnapshot(isCheckout); +}; + +record.mirror = mirror; + +export default record; diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts new file mode 100644 index 00000000..beb76bf8 --- /dev/null +++ b/packages/rrweb/src/record/mutation.ts @@ -0,0 +1,713 @@ +import { + serializeNodeWithId, + transformAttribute, + IGNORED_NODE, + isShadowRoot, + needMaskingText, + maskInputValue, + obfuscateText, + Mirror, + isNativeShadowDom, +} from '@highlight-run/rrweb-snapshot'; +import type { + mutationRecord, + textCursor, + attributeCursor, + removedNodeMutation, + addedNodeMutation, + styleAttributeValue, + observerParam, + MutationBufferParam, + Optional, +} from '../types'; +import { + isBlocked, + isAncestorRemoved, + isIgnored, + isSerialized, + hasShadowRoot, + isSerializedIframe, + isSerializedStylesheet, +} from '../utils'; + +type DoubleLinkedListNode = { + previous: DoubleLinkedListNode | null; + next: DoubleLinkedListNode | null; + value: NodeInLinkedList; +}; +type NodeInLinkedList = Node & { + __ln: DoubleLinkedListNode; +}; + +function isNodeInLinkedList(n: Node | NodeInLinkedList): n is NodeInLinkedList { + return '__ln' in n; +} + +class DoubleLinkedList { + public length = 0; + public head: DoubleLinkedListNode | null = null; + + public get(position: number) { + if (position >= this.length) { + throw new Error('Position outside of list range'); + } + + let current = this.head; + for (let index = 0; index < position; index++) { + current = current?.next || null; + } + return current; + } + + public addNode(n: Node) { + const node: DoubleLinkedListNode = { + value: n as NodeInLinkedList, + previous: null, + next: null, + }; + (n as NodeInLinkedList).__ln = node; + if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) { + const current = n.previousSibling.__ln.next; + node.next = current; + node.previous = n.previousSibling.__ln; + n.previousSibling.__ln.next = node; + if (current) { + current.previous = node; + } + } else if ( + n.nextSibling && + isNodeInLinkedList(n.nextSibling) && + n.nextSibling.__ln.previous + ) { + const current = n.nextSibling.__ln.previous; + node.previous = current; + node.next = n.nextSibling.__ln; + n.nextSibling.__ln.previous = node; + if (current) { + current.next = node; + } + } else { + if (this.head) { + this.head.previous = node; + } + node.next = this.head; + this.head = node; + } + this.length++; + } + + public removeNode(n: NodeInLinkedList) { + const current = n.__ln; + if (!this.head) { + return; + } + + if (!current.previous) { + this.head = current.next; + if (this.head) { + this.head.previous = null; + } + } else { + current.previous.next = current.next; + if (current.next) { + current.next.previous = current.previous; + } + } + if (n.__ln) { + delete (n as Optional).__ln; + } + this.length--; + } +} + +const moveKey = (id: number, parentId: number) => `${id}@${parentId}`; + +/** + * controls behaviour of a MutationObserver + */ +export default class MutationBuffer { + private frozen = false; + private locked = false; + + private texts: textCursor[] = []; + private attributes: attributeCursor[] = []; + private removes: removedNodeMutation[] = []; + private mapRemoves: Node[] = []; + + private movedMap: Record = {}; + + /** + * the browser MutationObserver emits multiple mutations after + * a delay for performance reasons, making tracing added nodes hard + * in our `processMutations` callback function. + * For example, if we append an element el_1 into body, and then append + * another element el_2 into el_1, these two mutations may be passed to the + * callback function together when the two operations were done. + * Generally we need to trace child nodes of newly added nodes, but in this + * case if we count el_2 as el_1's child node in the first mutation record, + * then we will count el_2 again in the second mutation record which was + * duplicated. + * To avoid of duplicate counting added nodes, we use a Set to store + * added nodes and its child nodes during iterate mutation records. Then + * collect added nodes from the Set which have no duplicate copy. But + * this also causes newly added nodes will not be serialized with id ASAP, + * which means all the id related calculation should be lazy too. + */ + private addedSet = new Set(); + private movedSet = new Set(); + private droppedSet = new Set(); + + private mutationCb: observerParam['mutationCb']; + private blockClass: observerParam['blockClass']; + private blockSelector: observerParam['blockSelector']; + private maskTextClass: observerParam['maskTextClass']; + private maskTextSelector: observerParam['maskTextSelector']; + private inlineStylesheet: observerParam['inlineStylesheet']; + private maskInputOptions: observerParam['maskInputOptions']; + private maskTextFn: observerParam['maskTextFn']; + private maskInputFn: observerParam['maskInputFn']; + private keepIframeSrcFn: observerParam['keepIframeSrcFn']; + private recordCanvas: observerParam['recordCanvas']; + private inlineImages: observerParam['inlineImages']; + private slimDOMOptions: observerParam['slimDOMOptions']; + private doc: observerParam['doc']; + private mirror: observerParam['mirror']; + private iframeManager: observerParam['iframeManager']; + private stylesheetManager: observerParam['stylesheetManager']; + private shadowDomManager: observerParam['shadowDomManager']; + private canvasManager: observerParam['canvasManager']; + private enableStrictPrivacy: observerParam['enableStrictPrivacy']; + + public init(options: MutationBufferParam) { + ([ + 'mutationCb', + 'blockClass', + 'blockSelector', + 'maskTextClass', + 'maskTextSelector', + 'inlineStylesheet', + 'maskInputOptions', + 'maskTextFn', + 'maskInputFn', + 'keepIframeSrcFn', + 'recordCanvas', + 'inlineImages', + 'slimDOMOptions', + 'doc', + 'mirror', + 'iframeManager', + 'stylesheetManager', + 'shadowDomManager', + 'canvasManager', + 'enableStrictPrivacy', + ] as const).forEach((key) => { + // just a type trick, the runtime result is correct + this[key] = options[key] as never; + }); + } + + public freeze() { + this.frozen = true; + this.canvasManager.freeze(); + } + + public unfreeze() { + this.frozen = false; + this.canvasManager.unfreeze(); + this.emit(); + } + + public isFrozen() { + return this.frozen; + } + + public lock() { + this.locked = true; + this.canvasManager.lock(); + } + + public unlock() { + this.locked = false; + this.canvasManager.unlock(); + this.emit(); + } + + public reset() { + this.shadowDomManager.reset(); + this.canvasManager.reset(); + } + + public processMutations = (mutations: mutationRecord[]) => { + mutations.forEach(this.processMutation); // adds mutations to the buffer + this.emit(); // clears buffer if not locked/frozen + }; + + public emit = () => { + if (this.frozen || this.locked) { + return; + } + + // delay any modification of the mirror until this function + // so that the mirror for takeFullSnapshot doesn't get mutated while it's event is being processed + + const adds: addedNodeMutation[] = []; + + /** + * Sometimes child node may be pushed before its newly added + * parent, so we init a queue to store these nodes. + */ + const addList = new DoubleLinkedList(); + const getNextId = (n: Node): number | null => { + let ns: Node | null = n; + let nextId: number | null = IGNORED_NODE; // slimDOM: ignored + while (nextId === IGNORED_NODE) { + ns = ns && ns.nextSibling; + nextId = ns && this.mirror.getId(ns); + } + return nextId; + }; + const pushAdd = (n: Node) => { + const shadowHost: Element | null = n.getRootNode + ? (n.getRootNode() as ShadowRoot)?.host + : null; + // If n is in a nested shadow dom. + let rootShadowHost = shadowHost; + while ((rootShadowHost?.getRootNode?.() as ShadowRoot | undefined)?.host) + rootShadowHost = + (rootShadowHost?.getRootNode?.() as ShadowRoot | undefined)?.host || + null; + // ensure shadowHost is a Node, or doc.contains will throw an error + const notInDoc = + !this.doc.contains(n) && + (!rootShadowHost || + !(rootShadowHost instanceof Node) || + !this.doc.contains(rootShadowHost)); + if (!n.parentNode || notInDoc) { + return; + } + const parentId = isShadowRoot(n.parentNode) + ? this.mirror.getId(shadowHost) + : this.mirror.getId(n.parentNode); + const nextId = getNextId(n); + if (parentId === -1 || nextId === -1) { + return addList.addNode(n); + } + const sn = serializeNodeWithId(n, { + doc: this.doc, + mirror: this.mirror, + blockClass: this.blockClass, + blockSelector: this.blockSelector, + maskTextClass: this.maskTextClass, + maskTextSelector: this.maskTextSelector, + skipChild: true, + newlyAddedElement: true, + inlineStylesheet: this.inlineStylesheet, + maskInputOptions: this.maskInputOptions, + maskTextFn: this.maskTextFn, + maskInputFn: this.maskInputFn, + slimDOMOptions: this.slimDOMOptions, + recordCanvas: this.recordCanvas, + inlineImages: this.inlineImages, + enableStrictPrivacy: this.enableStrictPrivacy, + onSerialize: (currentN) => { + if (isSerializedIframe(currentN, this.mirror)) { + this.iframeManager.addIframe(currentN as HTMLIFrameElement); + } + if (isSerializedStylesheet(currentN, this.mirror)) { + this.stylesheetManager.addStylesheet(currentN as HTMLLinkElement); + } + if (hasShadowRoot(n)) { + this.shadowDomManager.addShadowRoot(n.shadowRoot, document); + } + }, + onIframeLoad: (iframe, childSn) => { + this.iframeManager.attachIframe(iframe, childSn, this.mirror); + this.shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (link, childSn) => { + this.stylesheetManager.attachStylesheet(link, childSn, this.mirror); + }, + }); + if (sn) { + adds.push({ + parentId, + nextId, + node: sn, + }); + } + }; + + while (this.mapRemoves.length) { + this.mirror.removeNodeFromMap(this.mapRemoves.shift()!); + } + + for (const n of Array.from(this.movedSet.values())) { + if ( + isParentRemoved(this.removes, n, this.mirror) && + !this.movedSet.has(n.parentNode!) + ) { + continue; + } + pushAdd(n); + } + + for (const n of Array.from(this.addedSet.values())) { + if ( + !isAncestorInSet(this.droppedSet, n) && + !isParentRemoved(this.removes, n, this.mirror) + ) { + pushAdd(n); + } else if (isAncestorInSet(this.movedSet, n)) { + pushAdd(n); + } else { + this.droppedSet.add(n); + } + } + + let candidate: DoubleLinkedListNode | null = null; + while (addList.length) { + let node: DoubleLinkedListNode | null = null; + if (candidate) { + const parentId = this.mirror.getId(candidate.value.parentNode); + const nextId = getNextId(candidate.value); + if (parentId !== -1 && nextId !== -1) { + node = candidate; + } + } + if (!node) { + for (let index = addList.length - 1; index >= 0; index--) { + const _node = addList.get(index)!; + // ensure _node is defined before attempting to find value + if (_node) { + const parentId = this.mirror.getId(_node.value.parentNode); + const nextId = getNextId(_node.value); + if (parentId !== -1 && nextId !== -1) { + node = _node; + break; + } + } + } + } + if (!node) { + /** + * If all nodes in queue could not find a serialized parent, + * it may be a bug or corner case. We need to escape the + * dead while loop at once. + */ + while (addList.head) { + addList.removeNode(addList.head.value); + } + break; + } + candidate = node.previous; + addList.removeNode(node.value); + pushAdd(node.value); + } + + const payload = { + texts: this.texts + .map((text) => { + let value = text.value; + if (this.enableStrictPrivacy && value) { + value = obfuscateText(value); + } + return { + id: this.mirror.getId(text.node), + value, + }; + }) + // text mutation's id was not in the mirror map means the target node has been removed + .filter((text) => this.mirror.has(text.id)), + attributes: this.attributes + .map((attribute) => ({ + id: this.mirror.getId(attribute.node), + attributes: attribute.attributes, + })) + // attribute mutation's id was not in the mirror map means the target node has been removed + .filter((attribute) => this.mirror.has(attribute.id)), + removes: this.removes, + adds, + }; + // payload may be empty if the mutations happened in some blocked elements + if ( + !payload.texts.length && + !payload.attributes.length && + !payload.removes.length && + !payload.adds.length + ) { + return; + } + + // reset + this.texts = []; + this.attributes = []; + this.removes = []; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.movedMap = {}; + + this.mutationCb(payload); + }; + + private processMutation = (m: mutationRecord) => { + if (isIgnored(m.target, this.mirror)) { + return; + } + switch (m.type) { + case 'characterData': { + const value = m.target.textContent; + if ( + !isBlocked(m.target, this.blockClass, false) && + value !== m.oldValue + ) { + this.texts.push({ + value: + needMaskingText( + m.target, + this.maskTextClass, + this.maskTextSelector, + ) && value + ? this.maskTextFn + ? this.maskTextFn(value) + : value.replace(/[\S]/g, '*') + : value, + node: m.target, + }); + } + break; + } + case 'attributes': { + const target = m.target as HTMLElement; + let value = (m.target as HTMLElement).getAttribute(m.attributeName!); + if (m.attributeName === 'value') { + value = maskInputValue({ + maskInputOptions: this.maskInputOptions, + tagName: (m.target as HTMLElement).tagName, + type: (m.target as HTMLElement).getAttribute('type'), + value, + maskInputFn: this.maskInputFn, + }); + } + if ( + isBlocked(m.target, this.blockClass, false) || + value === m.oldValue + ) { + return; + } + + let item: attributeCursor | undefined = this.attributes.find( + (a) => a.node === m.target, + ); + if ( + target.tagName === 'IFRAME' && + m.attributeName === 'src' && + !this.keepIframeSrcFn(value as string) + ) { + if (!(target as HTMLIFrameElement).contentDocument) { + // we can't record it directly as we can't see into it + // preserve the src attribute so a decision can be taken at replay time + m.attributeName = 'rr_src'; + } else { + return; + } + } + if (!item) { + item = { + node: m.target, + attributes: {}, + }; + this.attributes.push(item); + } + if (m.attributeName === 'style') { + const old = this.doc.createElement('span'); + if (m.oldValue) { + old.setAttribute('style', m.oldValue); + } + if ( + item.attributes.style === undefined || + item.attributes.style === null + ) { + item.attributes.style = {}; + } + const styleObj = item.attributes.style as styleAttributeValue; + for (const pname of Array.from(target.style)) { + const newValue = target.style.getPropertyValue(pname); + const newPriority = target.style.getPropertyPriority(pname); + if ( + newValue !== old.style.getPropertyValue(pname) || + newPriority !== old.style.getPropertyPriority(pname) + ) { + if (newPriority === '') { + styleObj[pname] = newValue; + } else { + styleObj[pname] = [newValue, newPriority]; + } + } + } + for (const pname of Array.from(old.style)) { + if (target.style.getPropertyValue(pname) === '') { + // "if not set, returns the empty string" + styleObj[pname] = false; // delete + } + } + } else { + const tagName = (m.target as HTMLElement).tagName; + if (tagName === 'INPUT') { + const node = m.target as HTMLInputElement; + if (node.type === 'password') { + item.attributes['value'] = '*'.repeat(node.value.length); + break; + } + } + // overwrite attribute if the mutations was triggered in same time + item.attributes[m.attributeName!] = transformAttribute( + this.doc, + target.tagName, + m.attributeName!, + value!, + ); + } + break; + } + case 'childList': { + /** + * Parent is blocked, ignore all child mutations + */ + if (isBlocked(m.target, this.blockClass, true)) return; + + m.addedNodes.forEach((n) => this.genAdds(n, m.target)); + m.removedNodes.forEach((n) => { + const nodeId = this.mirror.getId(n); + const parentId = isShadowRoot(m.target) + ? this.mirror.getId(m.target.host) + : this.mirror.getId(m.target); + if ( + isBlocked(m.target, this.blockClass, false) || + isIgnored(n, this.mirror) || + !isSerialized(n, this.mirror) + ) { + return; + } + // removed node has not been serialized yet, just remove it from the Set + if (this.addedSet.has(n)) { + deepDelete(this.addedSet, n); + this.droppedSet.add(n); + } else if (this.addedSet.has(m.target) && nodeId === -1) { + /** + * If target was newly added and removed child node was + * not serialized, it means the child node has been removed + * before callback fired, so we can ignore it because + * newly added node will be serialized without child nodes. + * TODO: verify this + */ + } else if (isAncestorRemoved(m.target, this.mirror)) { + /** + * If parent id was not in the mirror map any more, it + * means the parent node has already been removed. So + * the node is also removed which we do not need to track + * and replay. + */ + } else if ( + this.movedSet.has(n) && + this.movedMap[moveKey(nodeId, parentId)] + ) { + deepDelete(this.movedSet, n); + } else { + this.removes.push({ + parentId, + id: nodeId, + isShadow: + isShadowRoot(m.target) && isNativeShadowDom(m.target) + ? true + : undefined, + }); + } + this.mapRemoves.push(n); + }); + break; + } + default: + break; + } + }; + + /** + * Make sure you check if `n`'s parent is blocked before calling this function + * */ + private genAdds = (n: Node, target?: Node) => { + if (this.mirror.hasNode(n)) { + if (isIgnored(n, this.mirror)) { + return; + } + this.movedSet.add(n); + let targetId: number | null = null; + if (target && this.mirror.hasNode(target)) { + targetId = this.mirror.getId(target); + } + if (targetId && targetId !== -1) { + this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + } + } else { + this.addedSet.add(n); + this.droppedSet.delete(n); + } + + // if this node is blocked `serializeNode` will turn it into a placeholder element + // but we have to remove it's children otherwise they will be added as placeholders too + if (!isBlocked(n, this.blockClass, false)) + n.childNodes.forEach((childN) => this.genAdds(childN)); + }; +} + +/** + * Some utils to handle the mutation observer DOM records. + * It should be more clear to extend the native data structure + * like Set and Map, but currently Typescript does not support + * that. + */ +function deepDelete(addsSet: Set, n: Node) { + addsSet.delete(n); + n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); +} + +function isParentRemoved( + removes: removedNodeMutation[], + n: Node, + mirror: Mirror, +): boolean { + if (removes.length === 0) return false; + return _isParentRemoved(removes, n, mirror); +} + +function _isParentRemoved( + removes: removedNodeMutation[], + n: Node, + mirror: Mirror, +): boolean { + const { parentNode } = n; + if (!parentNode) { + return false; + } + const parentId = mirror.getId(parentNode); + if (removes.some((r) => r.id === parentId)) { + return true; + } + return _isParentRemoved(removes, parentNode, mirror); +} + +function isAncestorInSet(set: Set, n: Node): boolean { + if (set.size === 0) return false; + return _isAncestorInSet(set, n); +} + +function _isAncestorInSet(set: Set, n: Node): boolean { + const { parentNode } = n; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; + } + return _isAncestorInSet(set, parentNode); +} diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts new file mode 100644 index 00000000..e0190363 --- /dev/null +++ b/packages/rrweb/src/record/observer.ts @@ -0,0 +1,898 @@ +import { + MaskInputOptions, + maskInputValue, +} from '@highlight-run/rrweb-snapshot'; +import type { FontFaceSet } from 'css-font-loading-module'; +import { + throttle, + on, + hookSetter, + getWindowHeight, + getWindowWidth, + isBlocked, + isTouchEvent, + patch, + isCanvasNode, +} from '../utils'; +import { + mutationCallBack, + observerParam, + mousemoveCallBack, + mousePosition, + mouseInteractionCallBack, + MouseInteractions, + listenerHandler, + scrollCallback, + styleSheetRuleCallback, + viewportResizeCallback, + inputValue, + inputCallback, + hookResetter, + IncrementalSource, + hooksParam, + Arguments, + mediaInteractionCallback, + MediaInteractions, + canvasMutationCallback, + fontCallback, + fontParam, + styleDeclarationCallback, + IWindow, + MutationBufferParam, +} from '../types'; +import MutationBuffer from './mutation'; + +type WindowWithStoredMutationObserver = IWindow & { + __rrMutationObserver?: MutationObserver; +}; +type WindowWithAngularZone = IWindow & { + Zone?: { + __symbol__?: (key: string) => string; + }; +}; + +export const mutationBuffers: MutationBuffer[] = []; + +const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== 'undefined'; +const isCSSMediaRuleSupported = typeof CSSMediaRule !== 'undefined'; +const isCSSSupportsRuleSupported = typeof CSSSupportsRule !== 'undefined'; +const isCSSConditionRuleSupported = typeof CSSConditionRule !== 'undefined'; + +// Event.path is non-standard and used in some older browsers +type NonStandardEvent = Omit & { + path: EventTarget[]; +}; + +function getEventTarget(event: Event | NonStandardEvent): EventTarget | null { + try { + if ('composedPath' in event) { + const path = event.composedPath(); + if (path.length) { + return path[0]; + } + } else if ('path' in event && event.path.length) { + return event.path[0]; + } + return event.target; + } catch { + return event.target; + } +} + +export function initMutationObserver( + options: MutationBufferParam, + rootEl: Node, +): MutationObserver { + const mutationBuffer = new MutationBuffer(); + mutationBuffers.push(mutationBuffer); + // see mutation.ts for details + mutationBuffer.init(options); + let mutationObserverCtor = + window.MutationObserver || + /** + * Some websites may disable MutationObserver by removing it from the window object. + * If someone is using rrweb to build a browser extention or things like it, they + * could not change the website's code but can have an opportunity to inject some + * code before the website executing its JS logic. + * Then they can do this to store the native MutationObserver: + * window.__rrMutationObserver = MutationObserver + */ + (window as WindowWithStoredMutationObserver).__rrMutationObserver; + const angularZoneSymbol = (window as WindowWithAngularZone)?.Zone?.__symbol__?.( + 'MutationObserver', + ); + if ( + angularZoneSymbol && + ((window as unknown) as Record)[ + angularZoneSymbol + ] + ) { + mutationObserverCtor = ((window as unknown) as Record< + string, + typeof MutationObserver + >)[angularZoneSymbol]; + } + const observer = new (mutationObserverCtor as new ( + callback: MutationCallback, + ) => MutationObserver)(mutationBuffer.processMutations.bind(mutationBuffer)); + observer.observe(rootEl, { + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }); + return observer; +} + +function initMoveObserver({ + mousemoveCb, + sampling, + doc, + mirror, +}: observerParam): listenerHandler { + if (sampling.mousemove === false) { + return () => { + // + }; + } + + const threshold = + typeof sampling.mousemove === 'number' ? sampling.mousemove : 50; + const callbackThreshold = + typeof sampling.mousemoveCallback === 'number' + ? sampling.mousemoveCallback + : 500; + + let positions: mousePosition[] = []; + let timeBaseline: number | null; + const wrappedCb = throttle( + ( + source: + | IncrementalSource.MouseMove + | IncrementalSource.TouchMove + | IncrementalSource.Drag, + ) => { + const totalOffset = Date.now() - timeBaseline!; + mousemoveCb( + positions.map((p) => { + p.timeOffset -= totalOffset; + return p; + }), + source, + ); + positions = []; + timeBaseline = null; + }, + callbackThreshold, + ); + const updatePosition = throttle( + (evt) => { + const target = getEventTarget(evt); + const { clientX, clientY } = isTouchEvent(evt) + ? evt.changedTouches[0] + : evt; + if (!timeBaseline) { + timeBaseline = Date.now(); + } + positions.push({ + x: clientX, + y: clientY, + id: mirror.getId(target as Node), + timeOffset: Date.now() - timeBaseline, + }); + // it is possible DragEvent is undefined even on devices + // that support event 'drag' + wrappedCb( + typeof DragEvent !== 'undefined' && evt instanceof DragEvent + ? IncrementalSource.Drag + : evt instanceof MouseEvent + ? IncrementalSource.MouseMove + : IncrementalSource.TouchMove, + ); + }, + threshold, + { + trailing: false, + }, + ); + const handlers = [ + on('mousemove', updatePosition, doc), + on('touchmove', updatePosition, doc), + on('drag', updatePosition, doc), + ]; + return () => { + handlers.forEach((h) => h()); + }; +} + +function initMouseInteractionObserver({ + mouseInteractionCb, + doc, + mirror, + blockClass, + sampling, +}: observerParam): listenerHandler { + if (sampling.mouseInteraction === false) { + return () => { + // + }; + } + const disableMap: Record = + sampling.mouseInteraction === true || + sampling.mouseInteraction === undefined + ? {} + : sampling.mouseInteraction; + + const handlers: listenerHandler[] = []; + const getHandler = (eventKey: keyof typeof MouseInteractions) => { + return (event: MouseEvent | TouchEvent) => { + const target = getEventTarget(event) as Node; + /* Start of Highlight Code */ + if ( + isBlocked(target, blockClass, true) || + // We ignore canvas elements for rage click detection because we cannot infer what inside the canvas is getting interacted with. + isCanvasNode(target) + ) { + return; + } + /* End of Highlight Code */ + const e = isTouchEvent(event) ? event.changedTouches[0] : event; + if (!e) { + return; + } + const id = mirror.getId(target); + const { clientX, clientY } = e; + mouseInteractionCb({ + type: MouseInteractions[eventKey], + id, + x: clientX, + y: clientY, + }); + }; + }; + Object.keys(MouseInteractions) + .filter( + (key) => + Number.isNaN(Number(key)) && + !key.endsWith('_Departed') && + disableMap[key] !== false, + ) + .forEach((eventKey: keyof typeof MouseInteractions) => { + const eventName = eventKey.toLowerCase(); + const handler = getHandler(eventKey); + handlers.push(on(eventName, handler, doc)); + }); + return () => { + handlers.forEach((h) => h()); + }; +} + +export function initScrollObserver({ + scrollCb, + doc, + mirror, + blockClass, + sampling, +}: Pick< + observerParam, + 'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'sampling' +>): listenerHandler { + const updatePosition = throttle((evt) => { + const target = getEventTarget(evt); + if (!target || isBlocked(target as Node, blockClass, true)) { + return; + } + const id = mirror.getId(target as Node); + if (target === doc) { + const scrollEl = (doc.scrollingElement || doc.documentElement)!; + scrollCb({ + id, + x: scrollEl.scrollLeft, + y: scrollEl.scrollTop, + }); + } else { + scrollCb({ + id, + x: (target as HTMLElement).scrollLeft, + y: (target as HTMLElement).scrollTop, + }); + } + }, sampling.scroll || 100); + return on('scroll', updatePosition, doc); +} + +function initViewportResizeObserver({ + viewportResizeCb, +}: observerParam): listenerHandler { + let lastH = -1; + let lastW = -1; + const updateDimension = throttle(() => { + const height = getWindowHeight(); + const width = getWindowWidth(); + if (lastH !== height || lastW !== width) { + viewportResizeCb({ + width: Number(width), + height: Number(height), + }); + lastH = height; + lastW = width; + } + }, 200); + return on('resize', updateDimension, window); +} + +function wrapEventWithUserTriggeredFlag( + v: inputValue, + enable: boolean, +): inputValue { + const value = { ...v }; + if (!enable) delete value.userTriggered; + return value; +} + +export const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; +const lastInputValueMap: WeakMap = new WeakMap(); +function initInputObserver({ + inputCb, + doc, + mirror, + blockClass, + ignoreClass, + maskInputOptions, + maskInputFn, + sampling, + userTriggeredOnInput, +}: observerParam): listenerHandler { + function eventHandler(event: Event) { + let target = getEventTarget(event); + const userTriggered = event.isTrusted; + /** + * If a site changes the value 'selected' of an option element, the value of its parent element, usually a select element, will be changed as well. + * We can treat this change as a value change of the select element the current target belongs to. + */ + if (target && (target as Element).tagName === 'OPTION') + target = (target as Element).parentElement; + if ( + !target || + !(target as Element).tagName || + INPUT_TAGS.indexOf((target as Element).tagName) < 0 || + isBlocked(target as Node, blockClass, true) + ) { + return; + } + const type: string | undefined = (target as HTMLInputElement).type; + if ((target as HTMLElement).classList.contains(ignoreClass)) { + return; + } + let text = (target as HTMLInputElement).value; + let isChecked = false; + if (type === 'radio' || type === 'checkbox') { + isChecked = (target as HTMLInputElement).checked; + } else if ( + maskInputOptions[ + (target as Element).tagName.toLowerCase() as keyof MaskInputOptions + ] || + maskInputOptions[type as keyof MaskInputOptions] + ) { + text = maskInputValue({ + maskInputOptions, + tagName: (target as HTMLElement).tagName, + type, + value: text, + maskInputFn, + }); + } + cbWithDedup( + target, + wrapEventWithUserTriggeredFlag( + { text, isChecked, userTriggered }, + userTriggeredOnInput, + ), + ); + // if a radio was checked + // the other radios with the same name attribute will be unchecked. + const name: string | undefined = (target as HTMLInputElement).name; + if (type === 'radio' && name && isChecked) { + doc + .querySelectorAll(`input[type="radio"][name="${name}"]`) + .forEach((el) => { + if (el !== target) { + cbWithDedup( + el, + wrapEventWithUserTriggeredFlag( + { + text: (el as HTMLInputElement).value, + isChecked: !isChecked, + userTriggered: false, + }, + userTriggeredOnInput, + ), + ); + } + }); + } + } + function cbWithDedup(target: EventTarget, v: inputValue) { + const lastInputValue = lastInputValueMap.get(target); + if ( + !lastInputValue || + lastInputValue.text !== v.text || + lastInputValue.isChecked !== v.isChecked + ) { + lastInputValueMap.set(target, v); + const id = mirror.getId(target as Node); + inputCb({ + ...v, + id, + }); + } + } + const events = sampling.input === 'last' ? ['change'] : ['input', 'change']; + const handlers: Array< + listenerHandler | hookResetter + > = events.map((eventName) => on(eventName, eventHandler, doc)); + const propertyDescriptor = Object.getOwnPropertyDescriptor( + HTMLInputElement.prototype, + 'value', + ); + const hookProperties: Array<[HTMLElement, string]> = [ + [HTMLInputElement.prototype, 'value'], + [HTMLInputElement.prototype, 'checked'], + [HTMLSelectElement.prototype, 'value'], + [HTMLTextAreaElement.prototype, 'value'], + // Some UI library use selectedIndex to set select value + [HTMLSelectElement.prototype, 'selectedIndex'], + [HTMLOptionElement.prototype, 'selected'], + ]; + if (propertyDescriptor && propertyDescriptor.set) { + handlers.push( + ...hookProperties.map((p) => + hookSetter(p[0], p[1], { + set() { + // mock to a normal event + eventHandler({ target: this as EventTarget } as Event); + }, + }), + ), + ); + } + return () => { + handlers.forEach((h) => h()); + }; +} + +type GroupingCSSRule = + | CSSGroupingRule + | CSSMediaRule + | CSSSupportsRule + | CSSConditionRule; +type GroupingCSSRuleTypes = + | typeof CSSGroupingRule + | typeof CSSMediaRule + | typeof CSSSupportsRule + | typeof CSSConditionRule; + +function getNestedCSSRulePositions(rule: CSSRule): number[] { + const positions: number[] = []; + function recurse(childRule: CSSRule, pos: number[]) { + if ( + (isCSSGroupingRuleSupported && + childRule.parentRule instanceof CSSGroupingRule) || + (isCSSMediaRuleSupported && + childRule.parentRule instanceof CSSMediaRule) || + (isCSSSupportsRuleSupported && + childRule.parentRule instanceof CSSSupportsRule) || + (isCSSConditionRuleSupported && + childRule.parentRule instanceof CSSConditionRule) + ) { + const rules = Array.from( + (childRule.parentRule as GroupingCSSRule).cssRules, + ); + const index = rules.indexOf(childRule); + pos.unshift(index); + } else { + const rules = Array.from(childRule.parentStyleSheet!.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + return pos; + } + return recurse(rule, positions); +} + +function initStyleSheetObserver( + { styleSheetRuleCb, mirror }: observerParam, + { win }: { win: IWindow }, +): listenerHandler { + // eslint-disable-next-line @typescript-eslint/unbound-method + const insertRule = win.CSSStyleSheet.prototype.insertRule; + win.CSSStyleSheet.prototype.insertRule = function ( + this: CSSStyleSheet, + rule: string, + index?: number, + ) { + const id = mirror.getId(this.ownerNode as Node); + if (id !== -1) { + styleSheetRuleCb({ + id, + adds: [{ rule, index }], + }); + } + return insertRule.apply(this, [rule, index]); + }; + + // eslint-disable-next-line @typescript-eslint/unbound-method + const deleteRule = win.CSSStyleSheet.prototype.deleteRule; + win.CSSStyleSheet.prototype.deleteRule = function ( + this: CSSStyleSheet, + index: number, + ) { + const id = mirror.getId(this.ownerNode as Node); + if (id !== -1) { + styleSheetRuleCb({ + id, + removes: [{ index }], + }); + } + return deleteRule.apply(this, [index]); + }; + + const supportedNestedCSSRuleTypes: { + [key: string]: GroupingCSSRuleTypes; + } = {}; + if (isCSSGroupingRuleSupported) { + supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule; + } else { + // Some browsers (Safari) don't support CSSGroupingRule + // https://caniuse.com/?search=cssgroupingrule + // fall back to monkey patching classes that would have inherited from CSSGroupingRule + + if (isCSSMediaRuleSupported) { + supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule; + } + if (isCSSConditionRuleSupported) { + supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule; + } + if (isCSSSupportsRuleSupported) { + supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule; + } + } + + const unmodifiedFunctions: { + [key: string]: { + insertRule: (rule: string, index?: number) => number; + deleteRule: (index: number) => void; + }; + } = {}; + + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + unmodifiedFunctions[typeKey] = { + // eslint-disable-next-line @typescript-eslint/unbound-method + insertRule: type.prototype.insertRule, + // eslint-disable-next-line @typescript-eslint/unbound-method + deleteRule: type.prototype.deleteRule, + }; + + type.prototype.insertRule = function (rule: string, index?: number) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const id = mirror.getId(this.parentStyleSheet.ownerNode as Node); + if (id !== -1) { + styleSheetRuleCb({ + id, + adds: [ + { + rule, + index: [ + ...getNestedCSSRulePositions(this as CSSRule), + index || 0, // defaults to 0 + ], + }, + ], + }); + } + return unmodifiedFunctions[typeKey].insertRule.apply(this, [rule, index]); + }; + + type.prototype.deleteRule = function (index: number) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const id = mirror.getId(this.parentStyleSheet.ownerNode as Node); + if (id !== -1) { + styleSheetRuleCb({ + id, + removes: [ + { index: [...getNestedCSSRulePositions(this as CSSRule), index] }, + ], + }); + } + return unmodifiedFunctions[typeKey].deleteRule.apply(this, [index]); + }; + }); + + return () => { + win.CSSStyleSheet.prototype.insertRule = insertRule; + win.CSSStyleSheet.prototype.deleteRule = deleteRule; + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; + type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; + }); + }; +} + +function initStyleDeclarationObserver( + { styleDeclarationCb, mirror }: observerParam, + { win }: { win: IWindow }, +): listenerHandler { + // eslint-disable-next-line @typescript-eslint/unbound-method + const setProperty = win.CSSStyleDeclaration.prototype.setProperty; + win.CSSStyleDeclaration.prototype.setProperty = function ( + this: CSSStyleDeclaration, + property: string, + value: string, + priority: string, + ) { + const id = mirror.getId(this.parentRule?.parentStyleSheet?.ownerNode); + if (id !== -1) { + styleDeclarationCb({ + id, + set: { + property, + value, + priority, + }, + index: getNestedCSSRulePositions(this.parentRule!), + }); + } + return setProperty.apply(this, [property, value, priority]); + }; + + // eslint-disable-next-line @typescript-eslint/unbound-method + const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty; + win.CSSStyleDeclaration.prototype.removeProperty = function ( + this: CSSStyleDeclaration, + property: string, + ) { + const id = mirror.getId(this.parentRule?.parentStyleSheet?.ownerNode); + if (id !== -1) { + styleDeclarationCb({ + id, + remove: { + property, + }, + index: getNestedCSSRulePositions(this.parentRule!), + }); + } + return removeProperty.apply(this, [property]); + }; + + return () => { + win.CSSStyleDeclaration.prototype.setProperty = setProperty; + win.CSSStyleDeclaration.prototype.removeProperty = removeProperty; + }; +} + +function initMediaInteractionObserver({ + mediaInteractionCb, + blockClass, + mirror, + sampling, +}: observerParam): listenerHandler { + const handler = (type: MediaInteractions) => + throttle((event: Event) => { + const target = getEventTarget(event); + if (!target || isBlocked(target as Node, blockClass, true)) { + return; + } + const { currentTime, volume, muted } = target as HTMLMediaElement; + mediaInteractionCb({ + type, + id: mirror.getId(target as Node), + currentTime, + volume, + muted, + }); + }, sampling.media || 500); + const handlers = [ + on('play', handler(MediaInteractions.Play)), + on('pause', handler(MediaInteractions.Pause)), + on('seeked', handler(MediaInteractions.Seeked)), + on('volumechange', handler(MediaInteractions.VolumeChange)), + ]; + return () => { + handlers.forEach((h) => h()); + }; +} + +function initFontObserver({ fontCb, doc }: observerParam): listenerHandler { + const win = doc.defaultView as IWindow; + if (!win) { + return () => { + // + }; + } + + const handlers: listenerHandler[] = []; + + const fontMap = new WeakMap(); + + const originalFontFace = win.FontFace; + win.FontFace = (function FontFace( + family: string, + source: string | ArrayBufferLike, + descriptors?: FontFaceDescriptors, + ) { + const fontFace = new originalFontFace(family, source, descriptors); + fontMap.set(fontFace, { + family, + buffer: typeof source !== 'string', + descriptors, + fontSource: + typeof source === 'string' + ? source + : JSON.stringify(Array.from(new Uint8Array(source))), + }); + return fontFace; + } as unknown) as typeof FontFace; + + const restoreHandler = patch( + doc.fonts, + 'add', + function (original: (font: FontFace) => void) { + return function (this: FontFaceSet, fontFace: FontFace) { + setTimeout(() => { + const p = fontMap.get(fontFace); + if (p) { + fontCb(p); + fontMap.delete(fontFace); + } + }, 0); + return original.apply(this, [fontFace]); + }; + }, + ); + + handlers.push(() => { + win.FontFace = originalFontFace; + }); + handlers.push(restoreHandler); + + return () => { + handlers.forEach((h) => h()); + }; +} + +function mergeHooks(o: observerParam, hooks: hooksParam) { + const { + mutationCb, + mousemoveCb, + mouseInteractionCb, + scrollCb, + viewportResizeCb, + inputCb, + mediaInteractionCb, + styleSheetRuleCb, + styleDeclarationCb, + canvasMutationCb, + fontCb, + } = o; + o.mutationCb = (...p: Arguments) => { + if (hooks.mutation) { + hooks.mutation(...p); + } + mutationCb(...p); + }; + o.mousemoveCb = (...p: Arguments) => { + if (hooks.mousemove) { + hooks.mousemove(...p); + } + mousemoveCb(...p); + }; + o.mouseInteractionCb = (...p: Arguments) => { + if (hooks.mouseInteraction) { + hooks.mouseInteraction(...p); + } + mouseInteractionCb(...p); + }; + o.scrollCb = (...p: Arguments) => { + if (hooks.scroll) { + hooks.scroll(...p); + } + scrollCb(...p); + }; + o.viewportResizeCb = (...p: Arguments) => { + if (hooks.viewportResize) { + hooks.viewportResize(...p); + } + viewportResizeCb(...p); + }; + o.inputCb = (...p: Arguments) => { + if (hooks.input) { + hooks.input(...p); + } + inputCb(...p); + }; + o.mediaInteractionCb = (...p: Arguments) => { + if (hooks.mediaInteaction) { + hooks.mediaInteaction(...p); + } + mediaInteractionCb(...p); + }; + o.styleSheetRuleCb = (...p: Arguments) => { + if (hooks.styleSheetRule) { + hooks.styleSheetRule(...p); + } + styleSheetRuleCb(...p); + }; + o.styleDeclarationCb = (...p: Arguments) => { + if (hooks.styleDeclaration) { + hooks.styleDeclaration(...p); + } + styleDeclarationCb(...p); + }; + o.canvasMutationCb = (...p: Arguments) => { + if (hooks.canvasMutation) { + hooks.canvasMutation(...p); + } + canvasMutationCb(...p); + }; + o.fontCb = (...p: Arguments) => { + if (hooks.font) { + hooks.font(...p); + } + fontCb(...p); + }; +} + +export function initObservers( + o: observerParam, + hooks: hooksParam = {}, +): listenerHandler { + const currentWindow = o.doc.defaultView; // basically document.window + if (!currentWindow) { + return () => { + // + }; + } + + mergeHooks(o, hooks); + const mutationObserver = initMutationObserver(o, o.doc); + const mousemoveHandler = initMoveObserver(o); + const mouseInteractionHandler = initMouseInteractionObserver(o); + const scrollHandler = initScrollObserver(o); + const viewportResizeHandler = initViewportResizeObserver(o); + const inputHandler = initInputObserver(o); + const mediaInteractionHandler = initMediaInteractionObserver(o); + + const styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow }); + const styleDeclarationObserver = initStyleDeclarationObserver(o, { + win: currentWindow, + }); + const fontObserver = o.collectFonts + ? initFontObserver(o) + : () => { + // + }; + // plugins + const pluginHandlers: listenerHandler[] = []; + for (const plugin of o.plugins) { + pluginHandlers.push( + plugin.observer(plugin.callback, currentWindow, plugin.options), + ); + } + + return () => { + mutationBuffers.forEach((b) => b.reset()); + mutationObserver.disconnect(); + mousemoveHandler(); + mouseInteractionHandler(); + scrollHandler(); + viewportResizeHandler(); + inputHandler(); + mediaInteractionHandler(); + styleSheetObserver(); + styleDeclarationObserver(); + fontObserver(); + pluginHandlers.forEach((h) => h()); + }; +} diff --git a/packages/rrweb/src/record/observers/canvas/2d.ts b/packages/rrweb/src/record/observers/canvas/2d.ts new file mode 100644 index 00000000..3b4174ca --- /dev/null +++ b/packages/rrweb/src/record/observers/canvas/2d.ts @@ -0,0 +1,83 @@ +import type { Mirror } from '@highlight-run/rrweb-snapshot'; +import { + blockClass, + CanvasContext, + canvasManagerMutationCallback, + IWindow, + listenerHandler, +} from '../../../types'; +import { hookSetter, isBlocked, patch } from '../../../utils'; +import { serializeArgs } from './serialize-args'; + +export default function initCanvas2DMutationObserver( + cb: canvasManagerMutationCallback, + win: IWindow, + blockClass: blockClass, + mirror: Mirror, +): listenerHandler { + const handlers: listenerHandler[] = []; + const props2D = Object.getOwnPropertyNames( + win.CanvasRenderingContext2D.prototype, + ); + for (const prop of props2D) { + try { + if ( + typeof win.CanvasRenderingContext2D.prototype[ + prop as keyof CanvasRenderingContext2D + ] !== 'function' + ) { + continue; + } + const restoreHandler = patch( + win.CanvasRenderingContext2D.prototype, + prop, + function ( + original: ( + this: CanvasRenderingContext2D, + ...args: unknown[] + ) => void, + ) { + return function ( + this: CanvasRenderingContext2D, + ...args: Array + ) { + if (!isBlocked(this.canvas, blockClass, true)) { + // Using setTimeout as toDataURL can be heavy + // and we'd rather not block the main thread + setTimeout(() => { + const recordArgs = serializeArgs([...args], win, this); + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: recordArgs, + }); + }, 0); + } + return original.apply(this, args); + }; + }, + ); + handlers.push(restoreHandler); + } catch { + const hookHandler = hookSetter( + win.CanvasRenderingContext2D.prototype, + prop, + { + set(v) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: [v], + setter: true, + }); + }, + }, + ); + handlers.push(hookHandler); + } + } + return () => { + handlers.forEach((h) => h()); + }; +} diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts new file mode 100644 index 00000000..62f783c2 --- /dev/null +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -0,0 +1,301 @@ +import type { ICanvas, Mirror } from '@highlight-run/rrweb-snapshot'; +import type { + blockClass, + canvasManagerMutationCallback, + canvasMutationCallback, + canvasMutationCommand, + canvasMutationWithType, + IWindow, + listenerHandler, + CanvasArg, +} from '../../../types'; +import { CanvasContext } from '../../../types'; +import initCanvas2DMutationObserver from './2d'; +import initCanvasContextObserver from './canvas'; +import initCanvasWebGLMutationObserver from './webgl'; +import ImageBitmapDataURLWorker from 'web-worker:../../workers/image-bitmap-data-url-worker.ts'; +import type { ImageBitmapDataURLRequestWorker } from '../../workers/image-bitmap-data-url-worker'; + +export type RafStamps = { latestId: number; invokeId: number | null }; + +type pendingCanvasMutationsMap = Map< + HTMLCanvasElement, + canvasMutationWithType[] +>; + +export class CanvasManager { + private pendingCanvasMutations: pendingCanvasMutationsMap = new Map(); + private rafStamps: RafStamps = { latestId: 0, invokeId: null }; + private mirror: Mirror; + + private mutationCb: canvasMutationCallback; + private resetObservers?: listenerHandler; + private frozen = false; + private locked = false; + + public reset() { + this.pendingCanvasMutations.clear(); + this.resetObservers && this.resetObservers(); + } + + public freeze() { + this.frozen = true; + } + + public unfreeze() { + this.frozen = false; + } + + public lock() { + this.locked = true; + } + + public unlock() { + this.locked = false; + } + + constructor(options: { + recordCanvas: boolean; + mutationCb: canvasMutationCallback; + win: IWindow; + blockClass: blockClass; + mirror: Mirror; + sampling?: 'all' | number; + resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high'; + resizeFactor?: number; + maxSnapshotDimension?: number; + }) { + const { sampling = 'all', win, blockClass, recordCanvas } = options; + this.mutationCb = options.mutationCb; + this.mirror = options.mirror; + + if (recordCanvas && sampling === 'all') + this.initCanvasMutationObserver(win, blockClass); + if (recordCanvas && typeof sampling === 'number') + this.initCanvasFPSObserver( + sampling, + win, + blockClass, + options.resizeQuality, + options.resizeFactor, + options.maxSnapshotDimension, + ); + } + + private processMutation: canvasManagerMutationCallback = ( + target, + mutation, + ) => { + const newFrame = + this.rafStamps.invokeId && + this.rafStamps.latestId !== this.rafStamps.invokeId; + if (newFrame || !this.rafStamps.invokeId) + this.rafStamps.invokeId = this.rafStamps.latestId; + + if (!this.pendingCanvasMutations.has(target)) { + this.pendingCanvasMutations.set(target, []); + } + + this.pendingCanvasMutations.get(target)!.push(mutation); + }; + + private initCanvasFPSObserver( + fps: number, + win: IWindow, + blockClass: blockClass, + resizeQuality?: 'pixelated' | 'low' | 'medium' | 'high', + resizeFactor?: number, + maxSnapshotDimension?: number, + ) { + const canvasContextReset = initCanvasContextObserver(win, blockClass); + const snapshotInProgressMap: Map = new Map(); + const worker = new ImageBitmapDataURLWorker() as ImageBitmapDataURLRequestWorker; + worker.onmessage = (e) => { + const { id } = e.data; + snapshotInProgressMap.set(id, false); + + if (!('base64' in e.data)) return; + + const { base64, type, canvasWidth, canvasHeight } = e.data; + this.mutationCb({ + id, + type: CanvasContext['2D'], + commands: [ + { + property: 'clearRect', // wipe canvas + args: [0, 0, canvasWidth, canvasHeight], + }, + { + property: 'drawImage', // draws (semi-transparent) image + args: [ + { + rr_type: 'ImageBitmap', + args: [ + { + rr_type: 'Blob', + data: [{ rr_type: 'ArrayBuffer', base64 }], + type, + }, + ], + } as CanvasArg, + 0, + 0, + canvasWidth, + canvasHeight, + ], + }, + ], + }); + }; + + const timeBetweenSnapshots = 1000 / fps; + let lastSnapshotTime = 0; + let rafId: number; + + const takeCanvasSnapshots = (timestamp: DOMHighResTimeStamp) => { + if ( + lastSnapshotTime && + timestamp - lastSnapshotTime < timeBetweenSnapshots + ) { + rafId = requestAnimationFrame(takeCanvasSnapshots); + return; + } + lastSnapshotTime = timestamp; + + win.document + .querySelectorAll(`canvas:not(.${blockClass as string} *)`) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .forEach(async (canvas: HTMLCanvasElement) => { + const id = this.mirror.getId(canvas); + if (snapshotInProgressMap.get(id)) return; + snapshotInProgressMap.set(id, true); + if (['webgl', 'webgl2'].includes((canvas as ICanvas).__context)) { + // if the canvas hasn't been modified recently, + // its contents won't be in memory and `createImageBitmap` + // will return a transparent imageBitmap + + const context = canvas.getContext((canvas as ICanvas).__context) as + | WebGLRenderingContext + | WebGL2RenderingContext + | null; + if ( + context?.getContextAttributes()?.preserveDrawingBuffer === false + ) { + // Hack to load canvas back into memory so `createImageBitmap` can grab it's contents. + // Context: https://twitter.com/Juice10/status/1499775271758704643 + // This hack might change the background color of the canvas in the unlikely event that + // the canvas background was changed but clear was not called directly afterwards. + context?.clear(context.COLOR_BUFFER_BIT); + } + } + // canvas is not yet ready... this retry on the next sampling iteration. + // we don't want to crash the worker if the canvas is not yet rendered. + if (canvas.width === 0 || canvas.height === 0) { + return; + } + let scale = resizeFactor || 1; + if (maxSnapshotDimension) { + const maxDim = Math.max(canvas.width, canvas.height); + scale = Math.min(scale, maxSnapshotDimension / maxDim); + } + const width = canvas.width * scale; + const height = canvas.height * scale; + + const bitmap = await createImageBitmap(canvas, { + resizeQuality: resizeQuality || 'low', + resizeWidth: width, + resizeHeight: height, + }); + worker.postMessage( + { + id, + bitmap, + width, + height, + canvasWidth: canvas.width, + canvasHeight: canvas.height, + }, + [bitmap], + ); + }); + rafId = requestAnimationFrame(takeCanvasSnapshots); + }; + + rafId = requestAnimationFrame(takeCanvasSnapshots); + + this.resetObservers = () => { + canvasContextReset(); + cancelAnimationFrame(rafId); + }; + } + + private initCanvasMutationObserver( + win: IWindow, + blockClass: blockClass, + ): void { + this.startRAFTimestamping(); + this.startPendingCanvasMutationFlusher(); + + const canvasContextReset = initCanvasContextObserver(win, blockClass); + const canvas2DReset = initCanvas2DMutationObserver( + this.processMutation.bind(this), + win, + blockClass, + this.mirror, + ); + + const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver( + this.processMutation.bind(this), + win, + blockClass, + this.mirror, + ); + + this.resetObservers = () => { + canvasContextReset(); + canvas2DReset(); + canvasWebGL1and2Reset(); + }; + } + + private startPendingCanvasMutationFlusher() { + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + + private startRAFTimestamping() { + const setLatestRAFTimestamp = (timestamp: DOMHighResTimeStamp) => { + this.rafStamps.latestId = timestamp; + requestAnimationFrame(setLatestRAFTimestamp); + }; + requestAnimationFrame(setLatestRAFTimestamp); + } + + flushPendingCanvasMutations() { + this.pendingCanvasMutations.forEach( + (values: canvasMutationCommand[], canvas: HTMLCanvasElement) => { + const id = this.mirror.getId(canvas); + this.flushPendingCanvasMutationFor(canvas, id); + }, + ); + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + + flushPendingCanvasMutationFor(canvas: HTMLCanvasElement, id: number) { + if (this.frozen || this.locked) { + return; + } + + const valuesWithType = this.pendingCanvasMutations.get(canvas); + if (!valuesWithType || id === -1) return; + + const values = valuesWithType.map((value) => { + const { type, ...rest } = value; + return rest; + }); + const { type } = valuesWithType[0]; + + this.mutationCb({ id, type, commands: values }); + + this.pendingCanvasMutations.delete(canvas); + } +} diff --git a/packages/rrweb/src/record/observers/canvas/canvas.ts b/packages/rrweb/src/record/observers/canvas/canvas.ts new file mode 100644 index 00000000..19b3385b --- /dev/null +++ b/packages/rrweb/src/record/observers/canvas/canvas.ts @@ -0,0 +1,40 @@ +import type { ICanvas } from '@highlight-run/rrweb-snapshot'; +import type { blockClass, IWindow, listenerHandler } from '../../../types'; +import { isBlocked, patch } from '../../../utils'; + +export default function initCanvasContextObserver( + win: IWindow, + blockClass: blockClass, +): listenerHandler { + const handlers: listenerHandler[] = []; + try { + const restoreHandler = patch( + win.HTMLCanvasElement.prototype, + 'getContext', + function ( + original: ( + this: ICanvas, + contextType: string, + ...args: Array + ) => void, + ) { + return function ( + this: ICanvas, + contextType: string, + ...args: Array + ) { + if (!isBlocked(this, blockClass, true)) { + if (!('__context' in this)) this.__context = contextType; + } + return original.apply(this, [contextType, ...args]); + }; + }, + ); + handlers.push(restoreHandler); + } catch { + console.error('failed to patch HTMLCanvasElement.prototype.getContext'); + } + return () => { + handlers.forEach((h) => h()); + }; +} diff --git a/packages/rrweb/src/record/observers/canvas/serialize-args.ts b/packages/rrweb/src/record/observers/canvas/serialize-args.ts new file mode 100644 index 00000000..8e14e33a --- /dev/null +++ b/packages/rrweb/src/record/observers/canvas/serialize-args.ts @@ -0,0 +1,175 @@ +import { encode } from 'base64-arraybuffer'; +import type { IWindow, CanvasArg } from '../../../types'; + +// TODO: unify with `replay/webgl.ts` +type CanvasVarMap = Map; +const canvasVarMap: Map = new Map(); +export function variableListFor(ctx: RenderingContext, ctor: string) { + let contextMap = canvasVarMap.get(ctx); + if (!contextMap) { + contextMap = new Map(); + canvasVarMap.set(ctx, contextMap); + } + if (!contextMap.has(ctor)) { + contextMap.set(ctor, []); + } + return contextMap.get(ctor) as unknown[]; +} + +export const saveWebGLVar = ( + value: unknown, + win: IWindow, + ctx: RenderingContext, +): number | void => { + if ( + !value || + !(isInstanceOfWebGLObject(value, win) || typeof value === 'object') + ) + return; + + const name = value.constructor.name; + const list = variableListFor(ctx, name); + let index = list.indexOf(value); + + if (index === -1) { + index = list.length; + list.push(value); + } + return index; +}; + +// from webgl-recorder: https://github.com/evanw/webgl-recorder/blob/bef0e65596e981ee382126587e2dcbe0fc7748e2/webgl-recorder.js#L50-L77 +export function serializeArg( + value: unknown, + win: IWindow, + ctx: RenderingContext, +): CanvasArg { + if (value instanceof Array) { + return value.map((arg) => serializeArg(arg, win, ctx)); + } else if (value === null) { + return value; + } else if ( + value instanceof Float32Array || + value instanceof Float64Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Uint8Array || + value instanceof Uint16Array || + value instanceof Int16Array || + value instanceof Int8Array || + value instanceof Uint8ClampedArray + ) { + const name = value.constructor.name; + return { + rr_type: name, + args: [Object.values(value)], + }; + } else if ( + // SharedArrayBuffer disabled on most browsers due to spectre. + // More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/SharedArrayBuffer + // value instanceof SharedArrayBuffer || + value instanceof ArrayBuffer + ) { + const name = value.constructor.name as 'ArrayBuffer'; + const base64 = encode(value); + + return { + rr_type: name, + base64, + }; + } else if (value instanceof DataView) { + const name = value.constructor.name; + return { + rr_type: name, + args: [ + serializeArg(value.buffer, win, ctx), + value.byteOffset, + value.byteLength, + ], + }; + } else if (value instanceof HTMLImageElement) { + const name = value.constructor.name; + const { src } = value; + return { + rr_type: name, + src, + }; + } else if (value instanceof HTMLCanvasElement) { + const name = 'HTMLImageElement'; + // TODO: move `toDataURL` to web worker if possible + const src = value.toDataURL(); // heavy on large canvas + return { + rr_type: name, + src, + }; + } else if (value instanceof ImageData) { + const name = value.constructor.name; + return { + rr_type: name, + args: [serializeArg(value.data, win, ctx), value.width, value.height], + }; + // } else if (value instanceof Blob) { + // const name = value.constructor.name; + // return { + // rr_type: name, + // data: [serializeArg(await value.arrayBuffer(), win, ctx)], + // type: value.type, + // }; + } else if (isInstanceOfWebGLObject(value, win) || typeof value === 'object') { + const name = value.constructor.name; + const index = saveWebGLVar(value, win, ctx) as number; + + return { + rr_type: name, + index: index, + }; + } + + return value as CanvasArg; +} + +export const serializeArgs = ( + args: Array, + win: IWindow, + ctx: RenderingContext, +) => { + return [...args].map((arg) => serializeArg(arg, win, ctx)); +}; + +export const isInstanceOfWebGLObject = ( + value: unknown, + win: IWindow, +): value is + | WebGLActiveInfo + | WebGLBuffer + | WebGLFramebuffer + | WebGLProgram + | WebGLRenderbuffer + | WebGLShader + | WebGLShaderPrecisionFormat + | WebGLTexture + | WebGLUniformLocation + | WebGLVertexArrayObject => { + const webGLConstructorNames: string[] = [ + 'WebGLActiveInfo', + 'WebGLBuffer', + 'WebGLFramebuffer', + 'WebGLProgram', + 'WebGLRenderbuffer', + 'WebGLShader', + 'WebGLShaderPrecisionFormat', + 'WebGLTexture', + 'WebGLUniformLocation', + 'WebGLVertexArrayObject', + // In old Chrome versions, value won't be an instanceof WebGLVertexArrayObject. + 'WebGLVertexArrayObjectOES', + ]; + const supportedWebGLConstructorNames = webGLConstructorNames.filter( + (name: string) => typeof win[name as keyof Window] === 'function', + ); + return Boolean( + supportedWebGLConstructorNames.find( + (name: string) => value instanceof win[name as keyof Window], + ), + ); +}; diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts new file mode 100644 index 00000000..1beb50bf --- /dev/null +++ b/packages/rrweb/src/record/observers/canvas/webgl.ts @@ -0,0 +1,110 @@ +import type { Mirror } from '@highlight-run/rrweb-snapshot'; +import { + blockClass, + CanvasContext, + canvasManagerMutationCallback, + canvasMutationWithType, + IWindow, + listenerHandler, +} from '../../../types'; +import { hookSetter, isBlocked, patch } from '../../../utils'; +import { saveWebGLVar, serializeArgs } from './serialize-args'; + +function patchGLPrototype( + prototype: WebGLRenderingContext | WebGL2RenderingContext, + type: CanvasContext, + cb: canvasManagerMutationCallback, + blockClass: blockClass, + mirror: Mirror, + win: IWindow, +): listenerHandler[] { + const handlers: listenerHandler[] = []; + + const props = Object.getOwnPropertyNames(prototype); + + for (const prop of props) { + try { + if (typeof prototype[prop as keyof typeof prototype] !== 'function') { + continue; + } + const restoreHandler = patch( + prototype, + prop, + function ( + original: (this: typeof prototype, ...args: Array) => void, + ) { + return function (this: typeof prototype, ...args: Array) { + const result = original.apply(this, args); + saveWebGLVar(result, win, prototype); + if (!isBlocked(this.canvas, blockClass, true)) { + const recordArgs = serializeArgs([...args], win, prototype); + const mutation: canvasMutationWithType = { + type, + property: prop, + args: recordArgs, + }; + // TODO: this could potentially also be an OffscreenCanvas as well as HTMLCanvasElement + cb(this.canvas, mutation); + } + + return result; + }; + }, + ); + handlers.push(restoreHandler); + } catch { + const hookHandler = hookSetter(prototype, prop, { + set(v) { + // TODO: this could potentially also be an OffscreenCanvas as well as HTMLCanvasElement + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + cb(this.canvas as HTMLCanvasElement, { + type, + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + + return handlers; +} + +export default function initCanvasWebGLMutationObserver( + cb: canvasManagerMutationCallback, + win: IWindow, + blockClass: blockClass, + mirror: Mirror, +): listenerHandler { + const handlers: listenerHandler[] = []; + + handlers.push( + ...patchGLPrototype( + win.WebGLRenderingContext.prototype, + CanvasContext.WebGL, + cb, + blockClass, + mirror, + win, + ), + ); + + if (typeof win.WebGL2RenderingContext !== 'undefined') { + handlers.push( + ...patchGLPrototype( + win.WebGL2RenderingContext.prototype, + CanvasContext.WebGL2, + cb, + blockClass, + mirror, + win, + ), + ); + } + + return () => { + handlers.forEach((h) => h()); + }; +} diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts new file mode 100644 index 00000000..a98bc0bc --- /dev/null +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -0,0 +1,110 @@ +import type { + MutationBufferParam, + mutationCallBack, + SamplingStrategy, + scrollCallback, +} from '../types'; +import { initMutationObserver, initScrollObserver } from './observer'; +import { patch } from '../utils'; +import type { Mirror } from '@highlight-run/rrweb-snapshot'; +import { isNativeShadowDom } from '@highlight-run/rrweb-snapshot'; + +type BypassOptions = Omit< + MutationBufferParam, + 'doc' | 'mutationCb' | 'mirror' | 'shadowDomManager' +> & { + sampling: SamplingStrategy; +}; + +export class ShadowDomManager { + private mutationCb: mutationCallBack; + private scrollCb: scrollCallback; + private bypassOptions: BypassOptions; + private mirror: Mirror; + private restorePatches: (() => void)[] = []; + + constructor(options: { + mutationCb: mutationCallBack; + scrollCb: scrollCallback; + bypassOptions: BypassOptions; + mirror: Mirror; + }) { + this.mutationCb = options.mutationCb; + this.scrollCb = options.scrollCb; + this.bypassOptions = options.bypassOptions; + this.mirror = options.mirror; + + // Patch 'attachShadow' to observe newly added shadow doms. + // eslint-disable-next-line @typescript-eslint/no-this-alias + const manager = this; + this.restorePatches.push( + patch( + HTMLElement.prototype, + 'attachShadow', + function (original: (init: ShadowRootInit) => ShadowRoot) { + return function (this: HTMLElement, option: ShadowRootInit) { + const shadowRoot = original.call(this, option); + if (this.shadowRoot) + manager.addShadowRoot(this.shadowRoot, this.ownerDocument); + return shadowRoot; + }; + }, + ), + ); + } + + public addShadowRoot(shadowRoot: ShadowRoot, doc: Document) { + if (!isNativeShadowDom(shadowRoot)) return; + initMutationObserver( + { + ...this.bypassOptions, + doc, + mutationCb: this.mutationCb, + mirror: this.mirror, + shadowDomManager: this, + }, + shadowRoot, + ); + initScrollObserver({ + ...this.bypassOptions, + scrollCb: this.scrollCb, + // https://gist.github.com/praveenpuglia/0832da687ed5a5d7a0907046c9ef1813 + // scroll is not allowed to pass the boundary, so we need to listen the shadow document + doc: (shadowRoot as unknown) as Document, + mirror: this.mirror, + }); + } + + /** + * Monkey patch 'attachShadow' of an IFrameElement to observe newly added shadow doms. + */ + public observeAttachShadow(iframeElement: HTMLIFrameElement) { + if (iframeElement.contentWindow) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const manager = this; + this.restorePatches.push( + patch( + (iframeElement.contentWindow as Window & { + HTMLElement: { prototype: HTMLElement }; + }).HTMLElement.prototype, + 'attachShadow', + function (original: (init: ShadowRootInit) => ShadowRoot) { + return function (this: HTMLElement, option: ShadowRootInit) { + const shadowRoot = original.call(this, option); + if (this.shadowRoot) + manager.addShadowRoot( + this.shadowRoot, + iframeElement.contentDocument as Document, + ); + return shadowRoot; + }; + }, + ), + ); + } + } + + public reset() { + this.restorePatches.forEach((restorePatch) => restorePatch()); + } +} diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts new file mode 100644 index 00000000..f863bf8b --- /dev/null +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -0,0 +1,45 @@ +import type { Mirror, serializedNodeWithId } from '@highlight-run/rrweb-snapshot'; +import type { mutationCallBack } from '../types'; + +export class StylesheetManager { + private trackedStylesheets: WeakSet = new WeakSet(); + private mutationCb: mutationCallBack; + + constructor(options: { mutationCb: mutationCallBack }) { + this.mutationCb = options.mutationCb; + } + + public addStylesheet(linkEl: HTMLLinkElement) { + if (this.trackedStylesheets.has(linkEl)) return; + + this.trackedStylesheets.add(linkEl); + this.trackStylesheet(linkEl); + } + + // TODO: take snapshot on stylesheet reload by applying event listener + private trackStylesheet(linkEl: HTMLLinkElement) { + // linkEl.addEventListener('load', () => { + // // re-loaded, maybe take another snapshot? + // }); + } + + public attachStylesheet( + linkEl: HTMLLinkElement, + childSn: serializedNodeWithId, + mirror: Mirror, + ) { + this.mutationCb({ + adds: [ + { + parentId: mirror.getId(linkEl), + nextId: null, + node: childSn, + }, + ], + removes: [], + texts: [], + attributes: [], + }); + this.addStylesheet(linkEl); + } +} diff --git a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts new file mode 100644 index 00000000..c27a6b54 --- /dev/null +++ b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts @@ -0,0 +1,80 @@ +import { encode } from 'base64-arraybuffer'; +import type { + ImageBitmapDataURLWorkerParams, + ImageBitmapDataURLWorkerResponse, +} from '../../types'; + +const lastBlobMap: Map = new Map(); +const transparentBlobMap: Map = new Map(); + +export interface ImageBitmapDataURLRequestWorker { + postMessage: ( + message: ImageBitmapDataURLWorkerParams, + transfer?: [ImageBitmap], + ) => void; + onmessage: (message: MessageEvent) => void; +} + +interface ImageBitmapDataURLResponseWorker { + onmessage: + | null + | ((message: MessageEvent) => void); + postMessage(e: ImageBitmapDataURLWorkerResponse): void; +} + +async function getTransparentBlobFor( + width: number, + height: number, +): Promise { + const id = `${width}-${height}`; + if (transparentBlobMap.has(id)) return transparentBlobMap.get(id)!; + const offscreen = new OffscreenCanvas(width, height); + offscreen.getContext('2d'); // creates rendering context for `converToBlob` + const blob = await offscreen.convertToBlob(); // takes a while + const arrayBuffer = await blob.arrayBuffer(); + const base64 = encode(arrayBuffer); // cpu intensive + transparentBlobMap.set(id, base64); + return base64; +} + +// `as any` because: https://github.com/Microsoft/TypeScript/issues/20595 +const worker: ImageBitmapDataURLResponseWorker = self; + +// eslint-disable-next-line @typescript-eslint/no-misused-promises +worker.onmessage = async function (e) { + if (!('OffscreenCanvas' in globalThis)) + return worker.postMessage({ id: e.data.id }); + + const { id, bitmap, width, height, canvasWidth, canvasHeight } = e.data; + + const transparentBase64 = getTransparentBlobFor(width, height); + + const offscreen = new OffscreenCanvas(width, height); + const ctx = offscreen.getContext('2d')!; + + ctx.drawImage(bitmap, 0, 0, width, height); + bitmap.close(); + const blob = await offscreen.convertToBlob(); // takes a while + const type = blob.type; + const arrayBuffer = await blob.arrayBuffer(); + const base64 = encode(arrayBuffer); // cpu intensive + + // on first try we should check if canvas is transparent, + // no need to save it's contents in that case + if (!lastBlobMap.has(id) && (await transparentBase64) === base64) { + lastBlobMap.set(id, base64); + return worker.postMessage({ id }); + } + + if (lastBlobMap.get(id) === base64) return worker.postMessage({ id }); // unchanged + worker.postMessage({ + id, + type, + base64, + width, + height, + canvasWidth, + canvasHeight, + }); + lastBlobMap.set(id, base64); +}; diff --git a/packages/rrweb/src/record/workers/tsconfig.json b/packages/rrweb/src/record/workers/tsconfig.json new file mode 100644 index 00000000..cf0e05cb --- /dev/null +++ b/packages/rrweb/src/record/workers/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "lib": ["webworker"] + }, + "exclude": ["workers.d.ts"] +} diff --git a/packages/rrweb/src/record/workers/workers.d.ts b/packages/rrweb/src/record/workers/workers.d.ts new file mode 100644 index 00000000..ead3d9e1 --- /dev/null +++ b/packages/rrweb/src/record/workers/workers.d.ts @@ -0,0 +1,4 @@ +declare module 'web-worker:*' { + const WorkerFactory: new () => Worker; + export default WorkerFactory; +} diff --git a/packages/rrweb/src/replay/canvas/2d.ts b/packages/rrweb/src/replay/canvas/2d.ts new file mode 100644 index 00000000..f764b5c9 --- /dev/null +++ b/packages/rrweb/src/replay/canvas/2d.ts @@ -0,0 +1,51 @@ +import type { Replayer } from '../'; +import type { canvasMutationCommand } from '../../types'; +import { deserializeArg } from './deserialize-args'; + +export default async function canvasMutation({ + event, + mutation, + target, + imageMap, + errorHandler, +}: { + event: Parameters[0]; + mutation: canvasMutationCommand; + target: HTMLCanvasElement; + imageMap: Replayer['imageMap']; + errorHandler: Replayer['warnCanvasMutationFailed']; +}): Promise { + try { + const ctx = target.getContext('2d')!; + + if (mutation.setter) { + // skip some read-only type checks + ((ctx as unknown) as Record)[mutation.property] = + mutation.args[0]; + return; + } + const original = ctx[ + mutation.property as Exclude + ] as (ctx: CanvasRenderingContext2D, args: unknown[]) => void; + + /** + * We have serialized the image source into base64 string during recording, + * which has been preloaded before replay. + * So we can get call drawImage SYNCHRONOUSLY which avoid some fragile cast. + */ + if ( + mutation.property === 'drawImage' && + typeof mutation.args[0] === 'string' + ) { + imageMap.get(event); + original.apply(ctx, mutation.args); + } else { + const args = await Promise.all( + mutation.args.map(deserializeArg(imageMap, ctx)), + ); + original.apply(ctx, args); + } + } catch (error) { + errorHandler(mutation, error); + } +} diff --git a/packages/rrweb/src/replay/canvas/deserialize-args.ts b/packages/rrweb/src/replay/canvas/deserialize-args.ts new file mode 100644 index 00000000..77f4eef0 --- /dev/null +++ b/packages/rrweb/src/replay/canvas/deserialize-args.ts @@ -0,0 +1,99 @@ +import { decode } from 'base64-arraybuffer'; +import type { Replayer } from '../'; +import type { CanvasArg, SerializedCanvasArg } from '../../types'; + +// TODO: add ability to wipe this list +type GLVarMap = Map; +const webGLVarMap: Map< + CanvasRenderingContext2D | WebGLRenderingContext | WebGL2RenderingContext, + GLVarMap +> = new Map(); +export function variableListFor( + ctx: + | CanvasRenderingContext2D + | WebGLRenderingContext + | WebGL2RenderingContext, + ctor: string, +) { + let contextMap = webGLVarMap.get(ctx); + if (!contextMap) { + contextMap = new Map(); + webGLVarMap.set(ctx, contextMap); + } + if (!contextMap.has(ctor)) { + contextMap.set(ctor, []); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return contextMap.get(ctor) as any[]; +} + +export function isSerializedArg(arg: unknown): arg is SerializedCanvasArg { + return Boolean(arg && typeof arg === 'object' && 'rr_type' in arg); +} + +export function deserializeArg( + imageMap: Replayer['imageMap'], + ctx: + | CanvasRenderingContext2D + | WebGLRenderingContext + | WebGL2RenderingContext + | null, + preload?: { + isUnchanged: boolean; + }, +): (arg: CanvasArg) => Promise { + return async (arg: CanvasArg): Promise => { + if (arg && typeof arg === 'object' && 'rr_type' in arg) { + if (preload) preload.isUnchanged = false; + if (arg.rr_type === 'ImageBitmap' && 'args' in arg) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const args = await deserializeArg(imageMap, ctx, preload)(arg.args); + // eslint-disable-next-line prefer-spread + return await createImageBitmap.apply(null, args); + } else if ('index' in arg) { + if (preload || ctx === null) return arg; // we are preloading, ctx is unknown + const { rr_type: name, index } = arg; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return variableListFor(ctx, name)[index]; + } else if ('args' in arg) { + const { rr_type: name, args } = arg; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const ctor = window[name as keyof Window]; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return new ctor( + ...(await Promise.all( + args.map(deserializeArg(imageMap, ctx, preload)), + )), + ); + } else if ('base64' in arg) { + return decode(arg.base64); + } else if ('src' in arg) { + const image = imageMap.get(arg.src); + if (image) { + return image; + } else { + const image = new Image(); + image.src = arg.src; + imageMap.set(arg.src, image); + return image; + } + } else if ('data' in arg && arg.rr_type === 'Blob') { + const blobContents = await Promise.all( + arg.data.map(deserializeArg(imageMap, ctx, preload)), + ); + const blob = new Blob(blobContents, { + type: arg.type, + }); + return blob; + } + } else if (Array.isArray(arg)) { + const result = await Promise.all( + arg.map(deserializeArg(imageMap, ctx, preload)), + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return result; + } + return arg; + }; +} diff --git a/packages/rrweb/src/replay/canvas/index.ts b/packages/rrweb/src/replay/canvas/index.ts new file mode 100644 index 00000000..8d32b455 --- /dev/null +++ b/packages/rrweb/src/replay/canvas/index.ts @@ -0,0 +1,62 @@ +import type { Replayer } from '..'; +import { + CanvasContext, + canvasMutationCommand, + canvasMutationData, + canvasMutationParam, +} from '../../types'; +import webglMutation from './webgl'; +import canvas2DMutation from './2d'; + +export default async function canvasMutation({ + event, + mutation, + target, + imageMap, + canvasEventMap, + errorHandler, +}: { + event: Parameters[0]; + mutation: canvasMutationData; + target: HTMLCanvasElement; + imageMap: Replayer['imageMap']; + canvasEventMap: Replayer['canvasEventMap']; + errorHandler: Replayer['warnCanvasMutationFailed']; +}): Promise { + try { + const precomputedMutation: canvasMutationParam = + canvasEventMap.get(event) || mutation; + + const commands: canvasMutationCommand[] = + 'commands' in precomputedMutation + ? precomputedMutation.commands + : [precomputedMutation]; + + if ([CanvasContext.WebGL, CanvasContext.WebGL2].includes(mutation.type)) { + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + await webglMutation({ + mutation: command, + type: mutation.type, + target, + imageMap, + errorHandler, + }); + } + return; + } + // default is '2d' for backwards compatibility (rrweb below 1.1.x) + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + await canvas2DMutation({ + event, + mutation: command, + target, + imageMap, + errorHandler, + }); + } + } catch (error) { + errorHandler(mutation, error); + } +} diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts new file mode 100644 index 00000000..b39f6df4 --- /dev/null +++ b/packages/rrweb/src/replay/canvas/webgl.ts @@ -0,0 +1,131 @@ +import type { Replayer } from '../'; +import { CanvasContext, canvasMutationCommand } from '../../types'; +import { deserializeArg, variableListFor } from './deserialize-args'; + +function getContext( + target: HTMLCanvasElement, + type: CanvasContext, +): WebGLRenderingContext | WebGL2RenderingContext | null { + // Note to whomever is going to implement support for `contextAttributes`: + // if `preserveDrawingBuffer` is set to true, + // you might have to do `ctx.flush()` before every webgl canvas event + try { + if (type === CanvasContext.WebGL) { + return ( + target.getContext('webgl')! || target.getContext('experimental-webgl') + ); + } + return target.getContext('webgl2')!; + } catch (e) { + return null; + } +} + +const WebGLVariableConstructorsNames = [ + 'WebGLActiveInfo', + 'WebGLBuffer', + 'WebGLFramebuffer', + 'WebGLProgram', + 'WebGLRenderbuffer', + 'WebGLShader', + 'WebGLShaderPrecisionFormat', + 'WebGLTexture', + 'WebGLUniformLocation', + 'WebGLVertexArrayObject', +]; + +function saveToWebGLVarMap( + ctx: WebGLRenderingContext | WebGL2RenderingContext, + result: any, +) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!result?.constructor) return; // probably null or undefined + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const { name } = result.constructor; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + if (!WebGLVariableConstructorsNames.includes(name)) return; // not a WebGL variable + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const variables = variableListFor(ctx, name); + if (!variables.includes(result)) variables.push(result); +} + +export default async function webglMutation({ + mutation, + target, + type, + imageMap, + errorHandler, +}: { + mutation: canvasMutationCommand; + target: HTMLCanvasElement; + type: CanvasContext; + imageMap: Replayer['imageMap']; + errorHandler: Replayer['warnCanvasMutationFailed']; +}): Promise { + try { + const ctx = getContext(target, type); + if (!ctx) return; + + // NOTE: if `preserveDrawingBuffer` is set to true, + // we must flush the buffers on every new canvas event + // if (mutation.newFrame) ctx.flush(); + + if (mutation.setter) { + // skip some read-only type checks + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (ctx as any)[mutation.property] = mutation.args[0]; + return; + } + const original = ctx[ + mutation.property as Exclude + ] as ( + ctx: WebGLRenderingContext | WebGL2RenderingContext, + args: unknown[], + ) => void; + + const args = await Promise.all( + mutation.args.map(deserializeArg(imageMap, ctx)), + ); + const result = original.apply(ctx, args); + saveToWebGLVarMap(ctx, result); + + // Slows down replay considerably, only use for debugging + const debugMode = false; + if (debugMode) { + if (mutation.property === 'compileShader') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + if (!ctx.getShaderParameter(args[0], ctx.COMPILE_STATUS)) + console.warn( + 'something went wrong in replay', + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ctx.getShaderInfoLog(args[0]), + ); + } else if (mutation.property === 'linkProgram') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ctx.validateProgram(args[0]); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + if (!ctx.getProgramParameter(args[0], ctx.LINK_STATUS)) + console.warn( + 'something went wrong in replay', + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ctx.getProgramInfoLog(args[0]), + ); + } + const webglError = ctx.getError(); + if (webglError !== ctx.NO_ERROR) { + console.warn( + 'WEBGL ERROR', + webglError, + 'on command:', + mutation.property, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ...args, + ); + } + } + } catch (error) { + errorHandler(mutation, error); + } +} diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts new file mode 100644 index 00000000..bd96c770 --- /dev/null +++ b/packages/rrweb/src/replay/index.ts @@ -0,0 +1,2045 @@ +import { + rebuild, + buildNodeWithSN, + NodeType, + BuildCache, + createCache, + Mirror, + createMirror, +} from '@highlight-run/rrweb-snapshot'; +import { + RRDocument, + StyleRuleType, + createOrGetNode, + buildFromNode, + buildFromDom, + diff, + getDefaultSN, +} from '@highlight-run/rrdom'; +import type { + RRNode, + RRElement, + RRStyleElement, + RRIFrameElement, + RRMediaElement, + RRCanvasElement, + ReplayerHandler, + Mirror as RRDOMMirror, + VirtualStyleRules, +} from '@highlight-run/rrdom'; +import * as mittProxy from 'mitt'; +import { polyfill as smoothscrollPolyfill } from './smoothscroll'; +import { Timer } from './timer'; +import { createPlayerService, createSpeedService } from './machine'; +import { + EventType, + IncrementalSource, + fullSnapshotEvent, + eventWithTime, + MouseInteractions, + playerConfig, + playerMetaData, + viewportResizeDimension, + missingNodeMap, + addedNodeMutation, + incrementalSnapshotEvent, + incrementalData, + ReplayerEvents, + Handler, + Emitter, + MediaInteractions, + metaEvent, + mutationData, + scrollData, + inputData, + canvasMutationData, + styleValueWithPriority, + mouseMovePos, + IWindow, + canvasMutationCommand, + canvasMutationParam, + canvasEventWithTime, SessionInterval, +} from '../types'; +import { + polyfill, + queueToResolveTrees, + iterateResolveTree, + AppendedIframe, + getBaseDimension, + hasShadowRoot, + isSerializedIframe, + getNestedRule, + getPositionsAndIndex, + uniqueTextMutations, +} from '../utils'; +import getInjectStyleRules from './styles/inject-style'; +import './styles/style.css'; +import canvasMutation from './canvas'; +import { deserializeArg } from './canvas/deserialize-args'; + +const SKIP_TIME_THRESHOLD = 10 * 1000; +const SKIP_TIME_INTERVAL = 2 * 1000; +const SKIP_TIME_MIN = 1 * 1000; +const SKIP_DURATION_LIMIT = 60 * 60 * 1000; + +// https://github.com/rollup/rollup/issues/1267#issuecomment-296395734 +const mitt = mittProxy.default || mittProxy; + +const REPLAY_CONSOLE_PREFIX = '[replayer]'; + +const defaultMouseTailConfig = { + duration: 500, + lineCap: 'round', + lineWidth: 3, + strokeStyle: 'red', +} as const; + +function indicatesTouchDevice(e: eventWithTime) { + return ( + e.type == EventType.IncrementalSnapshot && + (e.data.source == IncrementalSource.TouchMove || + (e.data.source == IncrementalSource.MouseInteraction && + e.data.type == MouseInteractions.TouchStart)) + ); +} + +export class Replayer { + public wrapper: HTMLDivElement; + public iframe: HTMLIFrameElement; + + public service: ReturnType; + public speedService: ReturnType; + public get timer() { + return this.service.state.context.timer; + } + + public config: playerConfig; + + // In the fast-forward process, if the virtual-dom optimization is used, this flag value is true. + public usingVirtualDom = false; + public virtualDom: RRDocument = new RRDocument(); + + private mouse: HTMLDivElement; + private mouseTail: HTMLCanvasElement | null = null; + private tailPositions: Array<{ x: number; y: number }> = []; + + private emitter: Emitter = mitt(); + + private nextUserInteractionEvent: eventWithTime | null; + private activityIntervals: Array = []; + private inactiveEndTimestamp: number | null; + + private legacy_missingNodeRetryMap: missingNodeMap = {}; + + // The replayer uses the cache to speed up replay and scrubbing. + private cache: BuildCache = createCache(); + + private imageMap: Map = new Map(); + private canvasEventMap: Map = new Map(); + + private mirror: Mirror = createMirror(); + + private firstFullSnapshot: eventWithTime | true | null = null; + + private newDocumentQueue: addedNodeMutation[] = []; + + private mousePos: mouseMovePos | null = null; + private touchActive: boolean | null = null; + + constructor( + events: Array, + config?: Partial, + ) { + if (!config?.liveMode && events.length < 2) { + throw new Error('Replayer need at least 2 events.'); + } + const defaultConfig: playerConfig = { + speed: 1, + maxSpeed: 360, + root: document.body, + loadTimeout: 0, + skipInactive: false, + showWarning: true, + showDebug: false, + blockClass: 'highlight-block', + liveMode: false, + insertStyleRules: [], + triggerFocus: true, + UNSAFE_replayCanvas: false, + pauseAnimation: true, + mouseTail: defaultMouseTailConfig, + useVirtualDom: true, // Virtual-dom optimization is enabled by default. + inactiveThreshold: 0.02, + inactiveSkipTime: SKIP_TIME_INTERVAL, + }; + this.config = Object.assign({}, defaultConfig, config); + + this.handleResize = this.handleResize.bind(this); + this.getCastFn = this.getCastFn.bind(this); + this.applyEventsSynchronously = this.applyEventsSynchronously.bind(this); + this.emitter.on(ReplayerEvents.Resize, this.handleResize as Handler); + + this.setupDom(); + + this.emitter.on(ReplayerEvents.Flush, () => { + if (this.usingVirtualDom) { + const replayerHandler: ReplayerHandler = { + mirror: this.mirror, + applyCanvas: ( + canvasEvent: canvasEventWithTime, + canvasMutationData: canvasMutationData, + target: HTMLCanvasElement, + ) => { + void canvasMutation({ + event: canvasEvent, + mutation: canvasMutationData, + target, + imageMap: this.imageMap, + canvasEventMap: this.canvasEventMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); + }, + applyInput: this.applyInput.bind(this), + applyScroll: this.applyScroll.bind(this), + }; + diff( + this.iframe.contentDocument!, + this.virtualDom, + replayerHandler, + this.virtualDom.mirror, + ); + this.virtualDom.destroyTree(); + this.usingVirtualDom = false; + + // If these legacy missing nodes haven't been resolved, they should be converted to real Nodes. + if (Object.keys(this.legacy_missingNodeRetryMap).length) { + for (const key in this.legacy_missingNodeRetryMap) { + try { + const value = this.legacy_missingNodeRetryMap[key]; + const realNode = createOrGetNode( + value.node as RRNode, + this.mirror, + this.virtualDom.mirror, + ); + diff( + realNode, + value.node as RRNode, + replayerHandler, + this.virtualDom.mirror, + ); + value.node = realNode; + } catch (error) { + if (this.config.showWarning) { + console.warn(error); + } + } + } + } + } + + if (this.mousePos) { + this.moveAndHover( + this.mousePos.x, + this.mousePos.y, + this.mousePos.id, + true, + this.mousePos.debugData, + ); + } + this.mousePos = null; + }); + this.emitter.on(ReplayerEvents.PlayBack, () => { + this.firstFullSnapshot = null; + this.mirror.reset(); + }); + + const timer = new Timer([], config?.speed || defaultConfig.speed); + this.service = createPlayerService( + { + events: events + .map((e) => { + if (config && config.unpackFn) { + return config.unpackFn(e as string); + } + return e as eventWithTime; + }) + .sort((a1, a2) => a1.timestamp - a2.timestamp), + timer, + timeOffset: 0, + baselineTime: 0, + lastPlayedEvent: null, + }, + { + getCastFn: this.getCastFn, + applyEventsSynchronously: this.applyEventsSynchronously, + emitter: this.emitter, + }, + ); + this.service.start(); + this.service.subscribe((state) => { + this.emitter.emit(ReplayerEvents.StateChange, { + player: state, + }); + }); + this.speedService = createSpeedService({ + normalSpeed: -1, + timer, + }); + this.speedService.start(); + this.speedService.subscribe((state) => { + this.emitter.emit(ReplayerEvents.StateChange, { + speed: state, + }); + }); + + // rebuild first full snapshot as the poster of the player + // maybe we can cache it for performance optimization + const firstMeta = this.service.state.context.events.find( + (e) => e.type === EventType.Meta, + ); + const firstFullsnapshot = this.service.state.context.events.find( + (e) => e.type === EventType.FullSnapshot, + ); + if (firstMeta) { + const { width, height } = firstMeta.data as metaEvent['data']; + setTimeout(() => { + this.emitter.emit(ReplayerEvents.Resize, { + width, + height, + }); + }, 0); + } + if (firstFullsnapshot) { + setTimeout(() => { + // when something has been played, there is no need to rebuild poster + if (this.firstFullSnapshot) { + // true if any other fullSnapshot has been executed by Timer already + return; + } + this.firstFullSnapshot = firstFullsnapshot; + this.rebuildFullSnapshot( + firstFullsnapshot as fullSnapshotEvent & { timestamp: number }, + ); + this.iframe.contentWindow!.scrollTo( + (firstFullsnapshot as fullSnapshotEvent).data.initialOffset, + ); + }, 1); + } + if (this.service.state.context.events.find(indicatesTouchDevice)) { + this.mouse.classList.add('touch-device'); + } + } + + public on(event: string, handler: Handler) { + this.emitter.on(event, handler); + return this; + } + + public off(event: string, handler: Handler) { + this.emitter.off(event, handler); + return this; + } + + public setConfig(config: Partial) { + Object.keys(config).forEach((key) => { + const newConfigValue = config[key as keyof playerConfig]; + (this.config as Record)[ + key as keyof playerConfig + ] = config[key as keyof playerConfig]; + }); + if (!this.config.skipInactive) { + this.backToNormal(); + } + if (typeof config.speed !== 'undefined') { + this.speedService.send({ + type: 'SET_SPEED', + payload: { + speed: config.speed, + }, + }); + } + if (typeof config.mouseTail !== 'undefined') { + if (config.mouseTail === false) { + if (this.mouseTail) { + this.mouseTail.style.display = 'none'; + } + } else { + if (!this.mouseTail) { + this.mouseTail = document.createElement('canvas'); + this.mouseTail.width = Number.parseFloat(this.iframe.width); + this.mouseTail.height = Number.parseFloat(this.iframe.height); + this.mouseTail.classList.add('replayer-mouse-tail'); + this.wrapper.insertBefore(this.mouseTail, this.iframe); + } + this.mouseTail.style.display = 'inherit'; + } + } + } + + /* Start Highlight Code */ + public getActivityIntervals(): Array { + if (this.activityIntervals.length == 0) { + // Preprocessing to get all active/inactive segments in a session + const allIntervals: Array = []; + const metadata = this.getMetaData(); + const userInteractionEvents = [ + { timestamp: metadata.startTime }, + ...this.service.state.context.events.filter((ev) => + this.isUserInteraction(ev), + ), + { timestamp: metadata.endTime }, + ]; + for (let i = 1; i < userInteractionEvents.length; i++) { + const currentInterval = userInteractionEvents[i - 1]; + const _event = userInteractionEvents[i]; + if ( + _event.timestamp! - currentInterval.timestamp! > + SKIP_TIME_THRESHOLD + ) { + allIntervals.push({ + startTime: currentInterval.timestamp!, + endTime: _event.timestamp!, + duration: _event.timestamp! - currentInterval.timestamp!, + active: false, + }); + } else { + allIntervals.push({ + startTime: currentInterval.timestamp!, + endTime: _event.timestamp!, + duration: _event.timestamp! - currentInterval.timestamp!, + active: true, + }); + } + } + // Merges continuous active/inactive ranges + const mergedIntervals: Array = []; + let currentInterval = allIntervals[0]; + for (let i = 1; i < allIntervals.length; i++) { + if (allIntervals[i].active != allIntervals[i - 1].active) { + mergedIntervals.push({ + startTime: currentInterval.startTime, + endTime: allIntervals[i - 1].endTime, + duration: allIntervals[i - 1].endTime - currentInterval.startTime, + active: allIntervals[i - 1].active, + }); + currentInterval = allIntervals[i]; + } + } + if (currentInterval && allIntervals.length > 0) { + mergedIntervals.push({ + startTime: currentInterval.startTime, + endTime: allIntervals[allIntervals.length - 1].endTime, + duration: + allIntervals[allIntervals.length - 1].endTime - + currentInterval.startTime, + active: allIntervals[allIntervals.length - 1].active, + }); + } + // Merges inactive segments that are less than a threshold into surrounding active sessions + // TODO: Change this from a 3n pass to n + currentInterval = mergedIntervals[0]; + for (let i = 1; i < mergedIntervals.length; i++) { + if ( + (!mergedIntervals[i].active && + mergedIntervals[i].duration > + this.config.inactiveThreshold * metadata.totalTime) || + (!mergedIntervals[i - 1].active && + mergedIntervals[i - 1].duration > + this.config.inactiveThreshold * metadata.totalTime) + ) { + this.activityIntervals.push({ + startTime: currentInterval.startTime, + endTime: mergedIntervals[i - 1].endTime, + duration: + mergedIntervals[i - 1].endTime - currentInterval.startTime, + active: mergedIntervals[i - 1].active, + }); + currentInterval = mergedIntervals[i]; + } + } + if (currentInterval && mergedIntervals.length > 0) { + this.activityIntervals.push({ + startTime: currentInterval.startTime, + endTime: mergedIntervals[mergedIntervals.length - 1].endTime, + duration: + mergedIntervals[mergedIntervals.length - 1].endTime - + currentInterval.startTime, + active: mergedIntervals[mergedIntervals.length - 1].active, + }); + } + } + return this.activityIntervals; + } + /* End Highlight Code */ + + public getMetaData(): playerMetaData { + const firstEvent = this.service.state.context.events[0]; + const lastEvent = this.service.state.context.events[ + this.service.state.context.events.length - 1 + ]; + return { + startTime: firstEvent.timestamp, + endTime: lastEvent.timestamp, + totalTime: lastEvent.timestamp - firstEvent.timestamp, + }; + } + + public getCurrentTime(): number { + return this.timer.timeOffset + this.getTimeOffset(); + } + + public getTimeOffset(): number { + const { baselineTime, events } = this.service.state.context; + return baselineTime - events[0].timestamp; + } + + public getMirror(): Mirror { + return this.mirror; + } + + /** + * This API was designed to be used as play at any time offset. + * Since we minimized the data collected from recorder, we do not + * have the ability of undo an event. + * So the implementation of play at any time offset will always iterate + * all of the events, cast event before the offset synchronously + * and cast event after the offset asynchronously with timer. + * @param timeOffset - number + */ + public play(timeOffset = 0) { + if (this.service.state.matches('paused')) { + this.service.send({ type: 'PLAY', payload: { timeOffset } }); + } else { + this.service.send({ type: 'PAUSE' }); + this.service.send({ type: 'PLAY', payload: { timeOffset } }); + } + this.iframe.contentDocument + ?.getElementsByTagName('html')[0] + ?.classList.remove('rrweb-paused'); + this.emitter.emit(ReplayerEvents.Start); + this.handleInactivity(this.getMetaData().startTime + timeOffset, true); + } + + public pause(timeOffset?: number) { + if (timeOffset === undefined && this.service.state.matches('playing')) { + this.service.send({ type: 'PAUSE' }); + } + if (typeof timeOffset === 'number') { + this.play(timeOffset); + this.service.send({ type: 'PAUSE' }); + } + this.iframe.contentDocument + ?.getElementsByTagName('html')[0] + ?.classList.add('rrweb-paused'); + this.emitter.emit(ReplayerEvents.Pause); + } + + public resume(timeOffset = 0) { + console.warn( + `The 'resume' will be departed in 1.0. Please use 'play' method which has the same interface.`, + ); + this.play(timeOffset); + this.emitter.emit(ReplayerEvents.Resume); + } + + public startLive(baselineTime?: number) { + this.service.send({ type: 'TO_LIVE', payload: { baselineTime } }); + } + + public addEvent(rawEvent: eventWithTime | string) { + const event = this.config.unpackFn + ? this.config.unpackFn(rawEvent as string) + : (rawEvent as eventWithTime); + if (indicatesTouchDevice(event)) { + this.mouse.classList.add('touch-device'); + } + void Promise.resolve().then(() => + this.service.send({ type: 'ADD_EVENT', payload: { event } }), + ); + } + + public replaceEvents(events: eventWithTime[]) { + for (const event of events) { + if (indicatesTouchDevice(event)) { + this.mouse.classList.add('touch-device'); + break; + } + } + this.service.send({ type: 'REPLACE_EVENTS', payload: { events } }); + } + + public enableInteract() { + this.iframe.setAttribute('scrolling', 'auto'); + this.iframe.style.pointerEvents = 'auto'; + } + + public disableInteract() { + this.iframe.setAttribute('scrolling', 'no'); + this.iframe.style.pointerEvents = 'none'; + } + + /** + * Empties the replayer's cache and reclaims memory. + * The replayer will use this cache to speed up the playback. + */ + public resetCache() { + this.cache = createCache(); + } + + private setupDom() { + this.wrapper = document.createElement('div'); + this.wrapper.classList.add('replayer-wrapper'); + this.config.root.appendChild(this.wrapper); + + this.mouse = document.createElement('div'); + this.mouse.classList.add('replayer-mouse'); + this.wrapper.appendChild(this.mouse); + + if (this.config.mouseTail !== false) { + this.mouseTail = document.createElement('canvas'); + this.mouseTail.classList.add('replayer-mouse-tail'); + this.mouseTail.style.display = 'inherit'; + this.wrapper.appendChild(this.mouseTail); + } + + this.iframe = document.createElement('iframe'); + const attributes = ['allow-same-origin']; + if (this.config.UNSAFE_replayCanvas) { + attributes.push('allow-scripts'); + } + // hide iframe before first meta event + this.iframe.style.display = 'none'; + this.iframe.setAttribute('sandbox', attributes.join(' ')); + this.disableInteract(); + this.wrapper.appendChild(this.iframe); + if (this.iframe.contentWindow && this.iframe.contentDocument) { + smoothscrollPolyfill( + this.iframe.contentWindow, + this.iframe.contentDocument, + ); + + polyfill(this.iframe.contentWindow as IWindow); + } + } + + private handleResize = (dimension: viewportResizeDimension) => { + this.iframe.style.display = 'inherit'; + for (const el of [this.mouseTail, this.iframe]) { + if (!el) { + continue; + } + el.setAttribute('width', String(dimension.width)); + el.setAttribute('height', String(dimension.height)); + } + }; + + private applyEventsSynchronously = (events: Array) => { + for (const event of events) { + switch (event.type) { + case EventType.DomContentLoaded: + case EventType.Load: + case EventType.Custom: + continue; + case EventType.FullSnapshot: + case EventType.Meta: + case EventType.Plugin: + case EventType.IncrementalSnapshot: + break; + default: + break; + } + const castFn = this.getCastFn(event, true); + castFn(); + } + if (this.touchActive === true) { + this.mouse.classList.add('touch-active'); + } else if (this.touchActive === false) { + this.mouse.classList.remove('touch-active'); + } + this.touchActive = null; + }; + + private getCastFn = (event: eventWithTime, isSync = false) => { + let castFn: undefined | (() => void); + switch (event.type) { + case EventType.DomContentLoaded: + case EventType.Load: + break; + case EventType.Custom: + castFn = () => { + /** + * emit custom-event and pass the event object. + * + * This will add more value to the custom event and allows the client to react for custom-event. + */ + this.emitter.emit(ReplayerEvents.CustomEvent, event); + }; + break; + case EventType.Meta: + castFn = () => + this.emitter.emit(ReplayerEvents.Resize, { + width: event.data.width, + height: event.data.height, + }); + break; + case EventType.FullSnapshot: + castFn = () => { + if (this.firstFullSnapshot) { + if (this.firstFullSnapshot === event) { + // we've already built this exact FullSnapshot when the player was mounted, and haven't built any other FullSnapshot since + this.firstFullSnapshot = true; // forget as we might need to re-execute this FullSnapshot later e.g. to rebuild after scrubbing + return; + } + } else { + // Timer (requestAnimationFrame) can be faster than setTimeout(..., 1) + this.firstFullSnapshot = true; + } + this.rebuildFullSnapshot(event, isSync); + this.iframe.contentWindow!.scrollTo(event.data.initialOffset); + }; + break; + case EventType.IncrementalSnapshot: + castFn = () => { + this.applyIncremental(event, isSync); + if (isSync) { + // do not check skip in sync + return; + } + this.handleInactivity(event.timestamp); + if (event === this.nextUserInteractionEvent) { + this.nextUserInteractionEvent = null; + this.backToNormal(); + } + if (this.config.skipInactive && !this.nextUserInteractionEvent) { + for (const _event of this.service.state.context.events) { + if (_event.timestamp <= event.timestamp) { + continue; + } + if (this.isUserInteraction(_event)) { + if ( + _event.delay! - event.delay! > + SKIP_TIME_THRESHOLD * + this.speedService.state.context.timer.speed + ) { + this.nextUserInteractionEvent = _event; + } + break; + } + } + if (this.nextUserInteractionEvent) { + const skipTime = + this.nextUserInteractionEvent.delay! - event.delay!; + const payload = { + speed: Math.min( + Math.round(skipTime / SKIP_TIME_INTERVAL), + this.config.maxSpeed, + ), + }; + this.speedService.send({ type: 'FAST_FORWARD', payload }); + this.emitter.emit(ReplayerEvents.SkipStart, payload); + } + } + }; + break; + default: + } + const wrappedCastFn = () => { + if (castFn) { + castFn(); + } + + for (const plugin of this.config.plugins || []) { + plugin.handler(event, isSync, { replayer: this }); + } + + this.service.send({ type: 'CAST_EVENT', payload: { event } }); + + // events are kept sorted by timestamp, check if this is the last event + const last_index = this.service.state.context.events.length - 1; + if (event === this.service.state.context.events[last_index]) { + const finish = () => { + if (last_index < this.service.state.context.events.length - 1) { + // more events have been added since the setTimeout + return; + } + this.backToNormal(); + this.service.send('END'); + this.emitter.emit(ReplayerEvents.Finish); + }; + if ( + event.type === EventType.IncrementalSnapshot && + event.data.source === IncrementalSource.MouseMove && + event.data.positions.length + ) { + // defer finish event if the last event is a mouse move + setTimeout(() => { + finish(); + }, Math.max(0, -event.data.positions[0].timeOffset + 50)); // Add 50 to make sure the timer would check the last mousemove event. Otherwise, the timer may be stopped by the service before checking the last event. + } else { + finish(); + } + } + + this.emitter.emit(ReplayerEvents.EventCast, event); + }; + return wrappedCastFn; + }; + + /* Start of Highlight Code */ + private handleInactivity(timestamp: number, resetNext?: boolean) { + if (timestamp === this.inactiveEndTimestamp || resetNext) { + this.inactiveEndTimestamp = null; + this.backToNormal(); + } + if (this.config.skipInactive && !this.inactiveEndTimestamp) { + for (const interval of this.getActivityIntervals()) { + if ( + timestamp >= interval.startTime! && + timestamp < interval.endTime! && + !interval.active + ) { + this.inactiveEndTimestamp = interval.endTime; + break; + } + } + if (this.inactiveEndTimestamp) { + const skipTime = this.inactiveEndTimestamp! - timestamp!; + const payload = { + speed: + (skipTime / SKIP_DURATION_LIMIT) * this.config.inactiveSkipTime < + SKIP_TIME_MIN + ? skipTime / SKIP_TIME_MIN + : Math.round( + Math.max(skipTime, SKIP_DURATION_LIMIT) / + this.config.inactiveSkipTime, + ), + }; + this.speedService.send({ type: 'FAST_FORWARD', payload }); + this.emitter.emit(ReplayerEvents.SkipStart, payload); + } + } + } + /* End of Highlight Code */ + + private rebuildFullSnapshot( + event: fullSnapshotEvent & { timestamp: number }, + isSync = false, + ) { + if (!this.iframe.contentDocument) { + return console.warn('Looks like your replayer has been destroyed.'); + } + if (Object.keys(this.legacy_missingNodeRetryMap).length) { + console.warn( + 'Found unresolved missing node map', + this.legacy_missingNodeRetryMap, + ); + } + this.legacy_missingNodeRetryMap = {}; + const collected: AppendedIframe[] = []; + rebuild(event.data.node, { + doc: this.iframe.contentDocument, + afterAppend: (builtNode) => { + this.collectIframeAndAttachDocument(collected, builtNode); + }, + cache: this.cache, + mirror: this.mirror, + }); + for (const { mutationInQueue, builtNode } of collected) { + this.attachDocumentToIframe(mutationInQueue, builtNode); + this.newDocumentQueue = this.newDocumentQueue.filter( + (m) => m !== mutationInQueue, + ); + } + const { documentElement, head } = this.iframe.contentDocument; + this.insertStyleRules(documentElement, head); + if (!this.service.state.matches('playing')) { + this.iframe.contentDocument + .getElementsByTagName('html')[0] + .classList.add('rrweb-paused'); + } + this.emitter.emit(ReplayerEvents.FullsnapshotRebuilded, event); + if (!isSync) { + this.waitForStylesheetLoad(); + } + if (this.config.UNSAFE_replayCanvas) { + void this.preloadAllImages(); + } + } + + private insertStyleRules( + documentElement: HTMLElement | RRElement, + head: HTMLHeadElement | RRElement, + ) { + const injectStylesRules = getInjectStyleRules( + this.config.blockClass, + ).concat(this.config.insertStyleRules); + if (this.config.pauseAnimation) { + injectStylesRules.push( + 'html.rrweb-paused *, html.rrweb-paused *:before, html.rrweb-paused *:after { animation-play-state: paused !important; }', + ); + } + if (this.usingVirtualDom) { + const styleEl = this.virtualDom.createElement('style'); + this.virtualDom.mirror.add( + styleEl, + getDefaultSN(styleEl, this.virtualDom.unserializedId), + ); + (documentElement as RRElement)!.insertBefore(styleEl, head as RRElement); + for (let idx = 0; idx < injectStylesRules.length; idx++) { + // push virtual styles + styleEl.rules.push({ + cssText: injectStylesRules[idx], + type: StyleRuleType.Insert, + index: idx, + }); + } + } else { + const styleEl = document.createElement('style'); + (documentElement as HTMLElement)!.insertBefore( + styleEl, + head as HTMLHeadElement, + ); + for (let idx = 0; idx < injectStylesRules.length; idx++) { + styleEl.sheet!.insertRule(injectStylesRules[idx], idx); + } + } + } + + private attachDocumentToIframe( + mutation: addedNodeMutation, + iframeEl: HTMLIFrameElement | RRIFrameElement, + ) { + const mirror: RRDOMMirror | Mirror = this.usingVirtualDom + ? this.virtualDom.mirror + : this.mirror; + type TNode = typeof mirror extends Mirror ? Node : RRNode; + type TMirror = typeof mirror extends Mirror ? Mirror : RRDOMMirror; + + const collected: AppendedIframe[] = []; + buildNodeWithSN(mutation.node, { + doc: iframeEl.contentDocument! as Document, + mirror: mirror as Mirror, + hackCss: true, + skipChild: false, + afterAppend: (builtNode) => { + this.collectIframeAndAttachDocument(collected, builtNode); + const sn = (mirror as TMirror).getMeta((builtNode as unknown) as TNode); + if ( + sn?.type === NodeType.Element && + sn?.tagName.toUpperCase() === 'HTML' + ) { + const { documentElement, head } = iframeEl.contentDocument!; + this.insertStyleRules( + documentElement as HTMLElement | RRElement, + head as HTMLElement | RRElement, + ); + } + }, + cache: this.cache, + }); + for (const { mutationInQueue, builtNode } of collected) { + this.attachDocumentToIframe(mutationInQueue, builtNode); + this.newDocumentQueue = this.newDocumentQueue.filter( + (m) => m !== mutationInQueue, + ); + } + } + + private collectIframeAndAttachDocument( + collected: AppendedIframe[], + builtNode: Node, + ) { + if (isSerializedIframe(builtNode, this.mirror)) { + const mutationInQueue = this.newDocumentQueue.find( + (m) => m.parentId === this.mirror.getId(builtNode), + ); + if (mutationInQueue) { + collected.push({ + mutationInQueue, + builtNode: builtNode as HTMLIFrameElement, + }); + } + } + } + + /** + * pause when loading style sheet, resume when loaded all timeout exceed + */ + private waitForStylesheetLoad() { + const head = this.iframe.contentDocument?.head; + if (head) { + const unloadSheets: Set = new Set(); + let timer: ReturnType | -1; + let beforeLoadState = this.service.state; + const stateHandler = () => { + beforeLoadState = this.service.state; + }; + this.emitter.on(ReplayerEvents.Start, stateHandler); + this.emitter.on(ReplayerEvents.Pause, stateHandler); + const unsubscribe = () => { + this.emitter.off(ReplayerEvents.Start, stateHandler); + this.emitter.off(ReplayerEvents.Pause, stateHandler); + }; + head + .querySelectorAll('link[rel="stylesheet"]') + .forEach((css: HTMLLinkElement) => { + if (!css.sheet) { + unloadSheets.add(css); + css.addEventListener('load', () => { + unloadSheets.delete(css); + // all loaded and timer not released yet + if (unloadSheets.size === 0 && timer !== -1) { + if (beforeLoadState.matches('playing')) { + this.play(this.getCurrentTime()); + } + this.emitter.emit(ReplayerEvents.LoadStylesheetEnd); + if (timer) { + clearTimeout(timer); + } + unsubscribe(); + } + }); + } + }); + + if (unloadSheets.size > 0) { + // find some unload sheets after iterate + this.service.send({ type: 'PAUSE' }); + this.emitter.emit(ReplayerEvents.LoadStylesheetStart); + timer = setTimeout(() => { + if (beforeLoadState.matches('playing')) { + this.play(this.getCurrentTime()); + } + // mark timer was called + timer = -1; + unsubscribe(); + }, this.config.loadTimeout); + } + } + } + + /** + * pause when there are some canvas drawImage args need to be loaded + */ + private async preloadAllImages(): Promise { + let beforeLoadState = this.service.state; + const stateHandler = () => { + beforeLoadState = this.service.state; + }; + this.emitter.on(ReplayerEvents.Start, stateHandler); + this.emitter.on(ReplayerEvents.Pause, stateHandler); + const promises: Promise[] = []; + for (const event of this.service.state.context.events) { + if ( + event.type === EventType.IncrementalSnapshot && + event.data.source === IncrementalSource.CanvasMutation + ) { + promises.push( + this.deserializeAndPreloadCanvasEvents(event.data, event), + ); + const commands = + 'commands' in event.data ? event.data.commands : [event.data]; + commands.forEach((c) => { + this.preloadImages(c, event); + }); + } + } + return Promise.all(promises); + } + + private preloadImages(data: canvasMutationCommand, event: eventWithTime) { + if ( + data.property === 'drawImage' && + typeof data.args[0] === 'string' && + !this.imageMap.has(event) + ) { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const imgd = ctx?.createImageData(canvas.width, canvas.height); + let d = imgd?.data; + d = JSON.parse(data.args[0]) as Uint8ClampedArray; + ctx?.putImageData(imgd!, 0, 0); + } + } + private async deserializeAndPreloadCanvasEvents( + data: canvasMutationData, + event: eventWithTime, + ) { + if (!this.canvasEventMap.has(event)) { + const status = { + isUnchanged: true, + }; + if ('commands' in data) { + const commands = await Promise.all( + data.commands.map(async (c) => { + const args = await Promise.all( + c.args.map(deserializeArg(this.imageMap, null, status)), + ); + return { ...c, args }; + }), + ); + if (status.isUnchanged === false) + this.canvasEventMap.set(event, { ...data, commands }); + } else { + const args = await Promise.all( + data.args.map(deserializeArg(this.imageMap, null, status)), + ); + if (status.isUnchanged === false) + this.canvasEventMap.set(event, { ...data, args }); + } + } + } + + private applyIncremental( + e: incrementalSnapshotEvent & { timestamp: number; delay?: number }, + isSync: boolean, + ) { + const { data: d } = e; + switch (d.source) { + case IncrementalSource.Mutation: { + try { + this.applyMutation(d, isSync); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions + this.warn(`Exception in mutation ${error.message || error}`, d); + } + break; + } + case IncrementalSource.Drag: + case IncrementalSource.TouchMove: + case IncrementalSource.MouseMove: + if (isSync) { + const lastPosition = d.positions[d.positions.length - 1]; + this.mousePos = { + x: lastPosition.x, + y: lastPosition.y, + id: lastPosition.id, + debugData: d, + }; + } else { + d.positions.forEach((p) => { + const action = { + doAction: () => { + this.moveAndHover(p.x, p.y, p.id, isSync, d); + }, + delay: + p.timeOffset + + e.timestamp - + this.service.state.context.baselineTime, + }; + this.timer.addAction(action); + }); + // add a dummy action to keep timer alive + this.timer.addAction({ + doAction() { + // + }, + delay: e.delay! - d.positions[0]?.timeOffset, + }); + } + break; + case IncrementalSource.MouseInteraction: { + /** + * Same as the situation of missing input target. + */ + if (d.id === -1 || isSync) { + break; + } + const event = new Event(MouseInteractions[d.type].toLowerCase()); + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + this.emitter.emit(ReplayerEvents.MouseInteraction, { + type: d.type, + target, + }); + const { triggerFocus } = this.config; + switch (d.type) { + case MouseInteractions.Blur: + if ('blur' in (target as HTMLElement)) { + (target as HTMLElement).blur(); + } + break; + case MouseInteractions.Focus: + if (triggerFocus && (target as HTMLElement).focus) { + (target as HTMLElement).focus({ + preventScroll: true, + }); + } + break; + case MouseInteractions.Click: + case MouseInteractions.TouchStart: + case MouseInteractions.TouchEnd: + if (isSync) { + if (d.type === MouseInteractions.TouchStart) { + this.touchActive = true; + } else if (d.type === MouseInteractions.TouchEnd) { + this.touchActive = false; + } + this.mousePos = { + x: d.x, + y: d.y, + id: d.id, + debugData: d, + }; + } else { + if (d.type === MouseInteractions.TouchStart) { + // don't draw a trail as user has lifted finger and is placing at a new point + this.tailPositions.length = 0; + } + this.moveAndHover(d.x, d.y, d.id, isSync, d); + if (d.type === MouseInteractions.Click) { + /* + * don't want target.click() here as could trigger an iframe navigation + * instead any effects of the click should already be covered by mutations + */ + /* + * removal and addition of .active class (along with void line to trigger repaint) + * triggers the 'click' css animation in styles/style.css + */ + this.mouse.classList.remove('active'); + void this.mouse.offsetWidth; + this.mouse.classList.add('active'); + } else if (d.type === MouseInteractions.TouchStart) { + void this.mouse.offsetWidth; // needed for the position update of moveAndHover to apply without the .touch-active transition + this.mouse.classList.add('touch-active'); + } else if (d.type === MouseInteractions.TouchEnd) { + this.mouse.classList.remove('touch-active'); + } + } + break; + case MouseInteractions.TouchCancel: + if (isSync) { + this.touchActive = false; + } else { + this.mouse.classList.remove('touch-active'); + } + break; + default: + target.dispatchEvent(event); + } + break; + } + case IncrementalSource.Scroll: { + /** + * Same as the situation of missing input target. + */ + if (d.id === -1) { + break; + } + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id) as RRElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.scrollData = d; + break; + } + // Use isSync rather than this.usingVirtualDom because not every fast-forward process uses virtual dom optimization. + this.applyScroll(d, isSync); + break; + } + case IncrementalSource.ViewportResize: + this.emitter.emit(ReplayerEvents.Resize, { + width: d.width, + height: d.height, + }); + break; + case IncrementalSource.Input: { + /** + * Input event on an unserialized node usually means the event + * was synchrony triggered programmatically after the node was + * created. This means there was not an user observable interaction + * and we do not need to replay it. + */ + if (d.id === -1) { + break; + } + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id) as RRElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.inputData = d; + break; + } + this.applyInput(d); + break; + } + case IncrementalSource.MediaInteraction: { + const target = this.usingVirtualDom + ? this.virtualDom.mirror.getNode(d.id) + : this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + const mediaEl = target as HTMLMediaElement | RRMediaElement; + try { + if (d.currentTime) { + mediaEl.currentTime = d.currentTime; + } + if (d.volume) { + mediaEl.volume = d.volume; + } + if (d.muted) { + mediaEl.muted = d.muted; + } + if (d.type === MediaInteractions.Pause) { + mediaEl.pause(); + } + if (d.type === MediaInteractions.Play) { + // remove listener for 'canplay' event because play() is async and returns a promise + // i.e. media will evntualy start to play when data is loaded + // 'canplay' event fires even when currentTime attribute changes which may lead to + // unexpeted behavior + void mediaEl.play(); + } + } catch (error) { + if (this.config.showWarning) { + console.warn( + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions + `Failed to replay media interactions: ${error.message || error}`, + ); + } + } + break; + } + case IncrementalSource.StyleSheetRule: { + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id) as RRStyleElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + const rules: VirtualStyleRules = target.rules; + d.adds?.forEach(({ rule, index: nestedIndex }) => + rules?.push({ + cssText: rule, + index: nestedIndex, + type: StyleRuleType.Insert, + }), + ); + d.removes?.forEach(({ index: nestedIndex }) => + rules?.push({ index: nestedIndex, type: StyleRuleType.Remove }), + ); + } else { + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + const styleSheet = (target as HTMLStyleElement).sheet!; + d.adds?.forEach(({ rule, index: nestedIndex }) => { + try { + if (Array.isArray(nestedIndex)) { + const { positions, index } = getPositionsAndIndex(nestedIndex); + const nestedRule = getNestedRule( + styleSheet.cssRules, + positions, + ); + nestedRule.insertRule(rule, index); + } else { + const index = + nestedIndex === undefined + ? undefined + : Math.min(nestedIndex, styleSheet.cssRules.length); + styleSheet.insertRule(rule, index); + } + } catch (e) { + /** + * sometimes we may capture rules with browser prefix + * insert rule with prefixs in other browsers may cause Error + */ + /** + * accessing styleSheet rules may cause SecurityError + * for specific access control settings + */ + } + }); + + d.removes?.forEach(({ index: nestedIndex }) => { + try { + if (Array.isArray(nestedIndex)) { + const { positions, index } = getPositionsAndIndex(nestedIndex); + const nestedRule = getNestedRule( + styleSheet.cssRules, + positions, + ); + nestedRule.deleteRule(index || 0); + } else { + styleSheet?.deleteRule(nestedIndex); + } + } catch (e) { + /** + * same as insertRule + */ + } + }); + } + break; + } + case IncrementalSource.StyleDeclaration: { + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id) as RRStyleElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + const rules: VirtualStyleRules = target.rules; + d.set && + rules.push({ + type: StyleRuleType.SetProperty, + index: d.index, + ...d.set, + }); + d.remove && + rules.push({ + type: StyleRuleType.RemoveProperty, + index: d.index, + ...d.remove, + }); + } else { + const target = (this.mirror.getNode( + d.id, + ) as Node) as HTMLStyleElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + const styleSheet = target.sheet!; + if (d.set) { + const rule = (getNestedRule( + styleSheet.rules, + d.index, + ) as unknown) as CSSStyleRule; + rule.style.setProperty(d.set.property, d.set.value, d.set.priority); + } + + if (d.remove) { + const rule = (getNestedRule( + styleSheet.rules, + d.index, + ) as unknown) as CSSStyleRule; + rule.style.removeProperty(d.remove.property); + } + } + break; + } + case IncrementalSource.CanvasMutation: { + if (!this.config.UNSAFE_replayCanvas) { + return; + } + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode( + d.id, + ) as RRCanvasElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.canvasMutations.push({ + event: e as canvasEventWithTime, + mutation: d, + }); + } else { + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + void canvasMutation({ + event: e, + mutation: d, + target: target as HTMLCanvasElement, + imageMap: this.imageMap, + canvasEventMap: this.canvasEventMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); + } + break; + } + case IncrementalSource.Font: { + try { + const fontFace = new FontFace( + d.family, + d.buffer + ? new Uint8Array(JSON.parse(d.fontSource) as Iterable) + : d.fontSource, + d.descriptors, + ); + this.iframe.contentDocument?.fonts.add(fontFace); + } catch (error) { + if (this.config.showWarning) { + console.warn(error); + } + } + break; + } + default: + } + } + + private applyMutation(d: mutationData, isSync: boolean) { + // Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events. + if (this.config.useVirtualDom && !this.usingVirtualDom && isSync) { + this.usingVirtualDom = true; + buildFromDom(this.iframe.contentDocument!, this.mirror, this.virtualDom); + // If these legacy missing nodes haven't been resolved, they should be converted to virtual nodes. + if (Object.keys(this.legacy_missingNodeRetryMap).length) { + for (const key in this.legacy_missingNodeRetryMap) { + try { + const value = this.legacy_missingNodeRetryMap[key]; + const virtualNode = buildFromNode( + value.node as Node, + this.virtualDom, + this.mirror, + ); + if (virtualNode) value.node = virtualNode; + } catch (error) { + if (this.config.showWarning) { + console.warn(error); + } + } + } + } + } + const mirror = this.usingVirtualDom ? this.virtualDom.mirror : this.mirror; + type TNode = typeof mirror extends Mirror ? Node : RRNode; + + d.removes.forEach((mutation) => { + const target = mirror.getNode(mutation.id); + if (!target) { + if (d.removes.find((r) => r.id === mutation.parentId)) { + // no need to warn, parent was already removed + return; + } + return this.warnNodeNotFound(d, mutation.id); + } + let parent: Node | null | ShadowRoot | RRNode = mirror.getNode( + mutation.parentId, + ); + if (!parent) { + return this.warnNodeNotFound(d, mutation.parentId); + } + if (mutation.isShadow && hasShadowRoot(parent as Node)) { + parent = (parent as Element | RRElement).shadowRoot; + } + // target may be removed with its parents before + mirror.removeNodeFromMap(target as Node & RRNode); + if (parent) + try { + parent.removeChild(target as Node & RRNode); + /** + * https://github.com/rrweb-io/rrweb/pull/887 + * Remove any virtual style rules for stylesheets if a child text node is removed. + */ + if ( + this.usingVirtualDom && + target.nodeName === '#text' && + parent.nodeName === 'STYLE' && + (parent as RRStyleElement).rules?.length > 0 + ) + (parent as RRStyleElement).rules = []; + } catch (error) { + if (error instanceof DOMException) { + this.warn( + 'parent could not remove child in mutation', + parent, + target, + d, + ); + } else { + throw error; + } + } + }); + + const legacy_missingNodeMap: missingNodeMap = { + ...this.legacy_missingNodeRetryMap, + }; + const queue: addedNodeMutation[] = []; + + // next not present at this moment + const nextNotInDOM = (mutation: addedNodeMutation) => { + let next: TNode | null = null; + if (mutation.nextId) { + next = mirror.getNode(mutation.nextId) as TNode | null; + } + // next not present at this moment + if ( + mutation.nextId !== null && + mutation.nextId !== undefined && + mutation.nextId !== -1 && + !next + ) { + return true; + } + return false; + }; + + const appendNode = (mutation: addedNodeMutation) => { + if (!this.iframe.contentDocument) { + return console.warn('Looks like your replayer has been destroyed.'); + } + let parent: Node | null | ShadowRoot | RRNode = mirror.getNode( + mutation.parentId, + ); + if (!parent) { + if (mutation.node.type === NodeType.Document) { + // is newly added document, maybe the document node of an iframe + return this.newDocumentQueue.push(mutation); + } + return queue.push(mutation); + } + + if (mutation.node.isShadow) { + // If the parent is attached a shadow dom after it's created, it won't have a shadow root. + if (!hasShadowRoot(parent)) { + (parent as Element | RRElement).attachShadow({ mode: 'open' }); + parent = (parent as Element | RRElement).shadowRoot! as Node | RRNode; + } else parent = parent.shadowRoot as Node | RRNode; + } + + let previous: Node | RRNode | null = null; + let next: Node | RRNode | null = null; + if (mutation.previousId) { + previous = mirror.getNode(mutation.previousId); + } + if (mutation.nextId) { + next = mirror.getNode(mutation.nextId); + } + if (nextNotInDOM(mutation)) { + return queue.push(mutation); + } + + if (mutation.node.rootId && !mirror.getNode(mutation.node.rootId)) { + return; + } + + const targetDoc = mutation.node.rootId + ? mirror.getNode(mutation.node.rootId) + : this.usingVirtualDom + ? this.virtualDom + : this.iframe.contentDocument; + if (isSerializedIframe(parent, mirror)) { + this.attachDocumentToIframe( + mutation, + parent as HTMLIFrameElement | RRIFrameElement, + ); + return; + } + const target = buildNodeWithSN(mutation.node, { + doc: targetDoc as Document, // can be Document or RRDocument + mirror: mirror as Mirror, // can be this.mirror or virtualDom.mirror + skipChild: true, + hackCss: true, + cache: this.cache, + }) as Node | RRNode; + + // legacy data, we should not have -1 siblings any more + if (mutation.previousId === -1 || mutation.nextId === -1) { + legacy_missingNodeMap[mutation.node.id] = { + node: target, + mutation, + }; + return; + } + + // Typescripts type system is not smart enough + // to understand what is going on with the types below + type TNode = typeof mirror extends Mirror ? Node : RRNode; + type TMirror = typeof mirror extends Mirror ? Mirror : RRDOMMirror; + + const parentSn = (mirror as TMirror).getMeta(parent as TNode); + if ( + parentSn && + parentSn.type === NodeType.Element && + parentSn.tagName === 'textarea' && + mutation.node.type === NodeType.Text + ) { + const childNodeArray = Array.isArray(parent.childNodes) + ? parent.childNodes + : Array.from(parent.childNodes); + + // https://github.com/rrweb-io/rrweb/issues/745 + // parent is textarea, will only keep one child node as the value + for (const c of childNodeArray) { + if (c.nodeType === parent.TEXT_NODE) { + parent.removeChild(c as Node & RRNode); + } + } + } + + if (previous && previous.nextSibling && previous.nextSibling.parentNode) { + (parent as TNode).insertBefore( + target as TNode, + previous.nextSibling as TNode, + ); + } else if (next && next.parentNode) { + // making sure the parent contains the reference nodes + // before we insert target before next. + (parent as TNode).contains(next as TNode) + ? (parent as TNode).insertBefore(target as TNode, next as TNode) + : (parent as TNode).insertBefore(target as TNode, null); + } else { + /** + * Sometimes the document changes and the MutationObserver is disconnected, so the removal of child elements can't be detected and recorded. After the change of document, we may get another mutation which adds a new html element, while the old html element still exists in the dom, and we need to remove the old html element first to avoid collision. + */ + if (parent === targetDoc) { + while (targetDoc.firstChild) { + (targetDoc as TNode).removeChild(targetDoc.firstChild as TNode); + } + } + + (parent as TNode).appendChild(target as TNode); + } + /** + * https://github.com/rrweb-io/rrweb/pull/887 + * Remove any virtual style rules for stylesheets if a new text node is appended. + */ + if ( + this.usingVirtualDom && + target.nodeName === '#text' && + parent.nodeName === 'STYLE' && + (parent as RRStyleElement).rules?.length > 0 + ) + (parent as RRStyleElement).rules = []; + + if (isSerializedIframe(target, this.mirror)) { + const targetId = this.mirror.getId(target as HTMLIFrameElement); + const mutationInQueue = this.newDocumentQueue.find( + (m) => m.parentId === targetId, + ); + if (mutationInQueue) { + this.attachDocumentToIframe( + mutationInQueue, + target as HTMLIFrameElement, + ); + this.newDocumentQueue = this.newDocumentQueue.filter( + (m) => m !== mutationInQueue, + ); + } + } + + if (mutation.previousId || mutation.nextId) { + this.legacy_resolveMissingNode( + legacy_missingNodeMap, + parent, + target, + mutation, + ); + } + }; + + d.adds.forEach((mutation) => { + appendNode(mutation); + }); + + const startTime = Date.now(); + while (queue.length) { + // transform queue to resolve tree + const resolveTrees = queueToResolveTrees(queue); + queue.length = 0; + if (Date.now() - startTime > 500) { + this.warn( + 'Timeout in the loop, please check the resolve tree data:', + resolveTrees, + ); + break; + } + for (const tree of resolveTrees) { + const parent = mirror.getNode(tree.value.parentId); + if (!parent) { + this.debug( + 'Drop resolve tree since there is no parent for the root node.', + tree, + ); + } else { + iterateResolveTree(tree, (mutation) => { + appendNode(mutation); + }); + } + } + } + + if (Object.keys(legacy_missingNodeMap).length) { + Object.assign(this.legacy_missingNodeRetryMap, legacy_missingNodeMap); + } + + uniqueTextMutations(d.texts).forEach((mutation) => { + const target = mirror.getNode(mutation.id); + if (!target) { + if (d.removes.find((r) => r.id === mutation.id)) { + // no need to warn, element was already removed + return; + } + return this.warnNodeNotFound(d, mutation.id); + } + target.textContent = mutation.value; + + /** + * https://github.com/rrweb-io/rrweb/pull/865 + * Remove any virtual style rules for stylesheets whose contents are replaced. + */ + if (this.usingVirtualDom) { + const parent = target.parentNode as RRStyleElement; + if (parent?.rules?.length > 0) parent.rules = []; + } + }); + d.attributes.forEach((mutation) => { + const target = mirror.getNode(mutation.id); + if (!target) { + if (d.removes.find((r) => r.id === mutation.id)) { + // no need to warn, element was already removed + return; + } + return this.warnNodeNotFound(d, mutation.id); + } + for (const attributeName in mutation.attributes) { + if (typeof attributeName === 'string') { + const value = mutation.attributes[attributeName]; + if (value === null) { + (target as Element | RRElement).removeAttribute(attributeName); + } else if (typeof value === 'string') { + try { + (target as Element | RRElement).setAttribute( + attributeName, + value, + ); + } catch (error) { + if (this.config.showWarning) { + console.warn( + 'An error occurred may due to the checkout feature.', + error, + ); + } + } + } else if (attributeName === 'style') { + const styleValues = value; + const targetEl = target as HTMLElement | RRElement; + for (const s in styleValues) { + if (styleValues[s] === false) { + targetEl.style.removeProperty(s); + } else if (styleValues[s] instanceof Array) { + const svp = styleValues[s] as styleValueWithPriority; + targetEl.style.setProperty(s, svp[0], svp[1]); + } else { + const svs = styleValues[s] as string; + targetEl.style.setProperty(s, svs); + } + } + } + } + } + }); + } + + /** + * Apply the scroll data on real elements. + * If the replayer is in sync mode, smooth scroll behavior should be disabled. + * @param d - the scroll data + * @param isSync - whether the replayer is in sync mode(fast-forward) + */ + private applyScroll(d: scrollData, isSync: boolean) { + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + const sn = this.mirror.getMeta(target); + if (target === this.iframe.contentDocument) { + this.iframe.contentWindow!.scrollTo({ + top: d.y, + left: d.x, + behavior: isSync ? 'auto' : 'smooth', + }); + } else if (sn?.type === NodeType.Document) { + // nest iframe content document + (target as Document).defaultView!.scrollTo({ + top: d.y, + left: d.x, + behavior: isSync ? 'auto' : 'smooth', + }); + } else { + try { + (target as Element).scrollTop = d.y; + (target as Element).scrollLeft = d.x; + } catch (error) { + /** + * Seldomly we may found scroll target was removed before + * its last scroll event. + */ + } + } + } + + private applyInput(d: inputData) { + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + try { + (target as HTMLInputElement).checked = d.isChecked; + (target as HTMLInputElement).value = d.text; + } catch (error) { + // for safe + } + } + + private legacy_resolveMissingNode( + map: missingNodeMap, + parent: Node | RRNode, + target: Node | RRNode, + targetMutation: addedNodeMutation, + ) { + const { previousId, nextId } = targetMutation; + const previousInMap = previousId && map[previousId]; + const nextInMap = nextId && map[nextId]; + if (previousInMap) { + const { node, mutation } = previousInMap; + parent.insertBefore(node as Node & RRNode, target as Node & RRNode); + delete map[mutation.node.id]; + delete this.legacy_missingNodeRetryMap[mutation.node.id]; + if (mutation.previousId || mutation.nextId) { + this.legacy_resolveMissingNode(map, parent, node, mutation); + } + } + if (nextInMap) { + const { node, mutation } = nextInMap; + parent.insertBefore( + node as Node & RRNode, + target.nextSibling as Node & RRNode, + ); + delete map[mutation.node.id]; + delete this.legacy_missingNodeRetryMap[mutation.node.id]; + if (mutation.previousId || mutation.nextId) { + this.legacy_resolveMissingNode(map, parent, node, mutation); + } + } + } + + private moveAndHover( + x: number, + y: number, + id: number, + isSync: boolean, + debugData: incrementalData, + ) { + const target = this.mirror.getNode(id); + if (!target) { + return this.debugNodeNotFound(debugData, id); + } + + const base = getBaseDimension(target, this.iframe); + const _x = x * base.absoluteScale + base.x; + const _y = y * base.absoluteScale + base.y; + + this.mouse.style.left = `${_x}px`; + this.mouse.style.top = `${_y}px`; + if (!isSync) { + this.drawMouseTail({ x: _x, y: _y }); + } + this.hoverElements(target as Element); + } + + private drawMouseTail(position: { x: number; y: number }) { + if (!this.mouseTail) { + return; + } + + const { lineCap, lineWidth, strokeStyle, duration } = + this.config.mouseTail === true + ? defaultMouseTailConfig + : Object.assign({}, defaultMouseTailConfig, this.config.mouseTail); + + const draw = () => { + if (!this.mouseTail) { + return; + } + const ctx = this.mouseTail.getContext('2d'); + if (!ctx || !this.tailPositions.length) { + return; + } + ctx.clearRect(0, 0, this.mouseTail.width, this.mouseTail.height); + ctx.beginPath(); + ctx.lineWidth = lineWidth; + ctx.lineCap = lineCap; + ctx.strokeStyle = strokeStyle; + ctx.moveTo(this.tailPositions[0].x, this.tailPositions[0].y); + this.tailPositions.forEach((p) => ctx.lineTo(p.x, p.y)); + ctx.stroke(); + }; + + this.tailPositions.push(position); + draw(); + setTimeout(() => { + this.tailPositions = this.tailPositions.filter((p) => p !== position); + draw(); + }, duration / this.speedService.state.context.timer.speed); + } + + private hoverElements(el: Element) { + this.iframe.contentDocument + ?.querySelectorAll('.\\:hover') + .forEach((hoveredEl) => { + hoveredEl.classList.remove(':hover'); + }); + let currentEl: Element | null = el; + while (currentEl) { + if (currentEl.classList) { + currentEl.classList.add(':hover'); + } + currentEl = currentEl.parentElement; + } + } + + private isUserInteraction(event: eventWithTime): boolean { + if (event.type !== EventType.IncrementalSnapshot) { + return false; + } + return ( + event.data.source > IncrementalSource.Mutation && + event.data.source <= IncrementalSource.Input + ); + } + + private backToNormal() { + this.nextUserInteractionEvent = null; + if (this.speedService.state.matches('normal')) { + return; + } + this.speedService.send({ type: 'BACK_TO_NORMAL' }); + this.emitter.emit(ReplayerEvents.SkipEnd, { + speed: this.speedService.state.context.normalSpeed, + }); + } + + private warnNodeNotFound(d: incrementalData, id: number) { + this.warn(`Node with id '${id}' not found. `, d); + } + + private warnCanvasMutationFailed( + d: canvasMutationData | canvasMutationCommand, + error: unknown, + ) { + this.warn(`Has error on canvas update`, error, 'canvas mutation:', d); + } + + private debugNodeNotFound(d: incrementalData, id: number) { + /** + * There maybe some valid scenes of node not being found. + * Because DOM events are macrotask and MutationObserver callback + * is microtask, so events fired on a removed DOM may emit + * snapshots in the reverse order. + */ + this.debug(REPLAY_CONSOLE_PREFIX, `Node with id '${id}' not found. `, d); + } + + private warn(...args: Parameters) { + if (!this.config.showWarning) { + return; + } + console.warn(REPLAY_CONSOLE_PREFIX, ...args); + } + + private debug(...args: Parameters) { + if (!this.config.showDebug) { + return; + } + console.log(REPLAY_CONSOLE_PREFIX, ...args); + } +} diff --git a/src/replay/machine.ts b/packages/rrweb/src/replay/machine.ts similarity index 73% rename from src/replay/machine.ts rename to packages/rrweb/src/replay/machine.ts index fc53b023..6eff1cf7 100644 --- a/src/replay/machine.ts +++ b/packages/rrweb/src/replay/machine.ts @@ -9,7 +9,6 @@ import { IncrementalSource, } from '../types'; import { Timer, addDelay } from './timer'; -import { needCastInSyncMode } from '../utils'; export type PlayerContext = { events: eventWithTime[]; @@ -39,6 +38,10 @@ export type PlayerEvent = event: eventWithTime; }; } + | { + type: 'REPLACE_EVENTS'; + payload: { events: eventWithTime[]; }; + } | { type: 'END'; }; @@ -77,11 +80,12 @@ export function discardPriorSnapshots( type PlayerAssets = { emitter: Emitter; + applyEventsSynchronously(events: Array): void; getCastFn(event: eventWithTime, isSync: boolean): () => void; }; export function createPlayerService( context: PlayerContext, - { getCastFn, emitter }: PlayerAssets, + { getCastFn, applyEventsSynchronously, emitter }: PlayerAssets, ) { const playerMachine = createMachine( { @@ -107,6 +111,10 @@ export function createPlayerService( target: 'playing', actions: ['addEvent'], }, + REPLACE_EVENTS: { + target: 'playing', + actions: ['replaceEvents'], + }, }, }, paused: { @@ -127,6 +135,10 @@ export function createPlayerService( target: 'paused', actions: ['addEvent'], }, + REPLACE_EVENTS: { + target: 'paused', + actions: ['replaceEvents'], + }, }, }, live: { @@ -165,26 +177,31 @@ export function createPlayerService( }; }), play(ctx) { - console.warn('play'); const { timer, events, baselineTime, lastPlayedEvent } = ctx; timer.clear(); + for (const event of events) { // TODO: improve this API addDelay(event, baselineTime); } const neededEvents = discardPriorSnapshots(events, baselineTime); + let lastPlayedTimestamp = lastPlayedEvent?.timestamp; + if ( + lastPlayedEvent?.type === EventType.IncrementalSnapshot && + lastPlayedEvent.data.source === IncrementalSource.MouseMove + ) { + lastPlayedTimestamp = + lastPlayedEvent.timestamp + + lastPlayedEvent.data.positions[0]?.timeOffset; + } + if (baselineTime < (lastPlayedTimestamp || 0)) { + emitter.emit(ReplayerEvents.PlayBack); + } + + const syncEvents = new Array(); const actions = new Array(); for (const event of neededEvents) { - let lastPlayedTimestamp = lastPlayedEvent?.timestamp; - if ( - lastPlayedEvent?.type === EventType.IncrementalSnapshot && - lastPlayedEvent.data.source === IncrementalSource.MouseMove - ) { - lastPlayedTimestamp = - lastPlayedEvent.timestamp + - lastPlayedEvent.data.positions[0]?.timeOffset; - } if ( lastPlayedTimestamp && lastPlayedTimestamp < baselineTime && @@ -193,23 +210,19 @@ export function createPlayerService( ) { continue; } - const isSync = event.timestamp < baselineTime; - if (isSync && !needCastInSyncMode(event)) { - continue; - } - const castFn = getCastFn(event, isSync); - if (isSync) { - castFn(); + if (event.timestamp < baselineTime) { + syncEvents.push(event); } else { + const castFn = getCastFn(event, false); actions.push({ doAction: () => { castFn(); - emitter.emit(ReplayerEvents.EventCast, event); }, delay: event.delay!, }); } } + applyEventsSynchronously(syncEvents); emitter.emit(ReplayerEvents.Flush); timer.addActions(actions); timer.start(); @@ -233,27 +246,72 @@ export function createPlayerService( return Date.now(); }, }), + /* Highlight Code Start */ + replaceEvents: assign((ctx, machineEvent) => { + const { events: curEvents, timer, baselineTime } = ctx; + if (machineEvent.type === 'REPLACE_EVENTS') { + const { events: newEvents } = machineEvent.payload; + curEvents.length = 0; + const actions: actionWithDelay[] = []; + for (const event of newEvents) { + addDelay(event, baselineTime); + curEvents.push(event); + if (event.timestamp >= timer.timeOffset + baselineTime) { + const castFn = getCastFn(event, false); + actions.push({ + doAction: () => { + castFn(); + }, + delay: event.delay!, + }); + } + } + + if (timer.isActive()) { + timer.replaceActions(actions); + } + } + return { ...ctx, events: curEvents }; + }), + /* Highlight Code End */ addEvent: assign((ctx, machineEvent) => { const { baselineTime, timer, events } = ctx; if (machineEvent.type === 'ADD_EVENT') { const { event } = machineEvent.payload; addDelay(event, baselineTime); - events.push(event); + + let end = events.length - 1; + if (!events[end] || events[end].timestamp <= event.timestamp) { + // fast track + events.push(event); + } else { + let insertionIndex = -1; + let start = 0; + while (start <= end) { + const mid = Math.floor((start + end) / 2); + if (events[mid].timestamp <= event.timestamp) { + start = mid + 1; + } else { + end = mid - 1; + } + } + if (insertionIndex === -1) { + insertionIndex = start; + } + events.splice(insertionIndex, 0, event); + } + const isSync = event.timestamp < baselineTime; const castFn = getCastFn(event, isSync); if (isSync) { castFn(); - } else { + } else if (timer.isActive()) { timer.addAction({ doAction: () => { castFn(); - emitter.emit(ReplayerEvents.EventCast, event); }, delay: event.delay!, }); - if (!timer.isActive()) { - timer.start(); - } } } return { ...ctx, events }; diff --git a/src/replay/smoothscroll.ts b/packages/rrweb/src/replay/smoothscroll.ts similarity index 91% rename from src/replay/smoothscroll.ts rename to packages/rrweb/src/replay/smoothscroll.ts index fa2c0c81..569968b1 100644 --- a/src/replay/smoothscroll.ts +++ b/packages/rrweb/src/replay/smoothscroll.ts @@ -3,8 +3,8 @@ * Add support of customize target window and document */ +/* eslint-disable */ // @ts-nocheck -// tslint:disable export function polyfill(w: Window = window, d = document) { // return if scroll behavior is supported and polyfill is not forced if ( @@ -15,11 +15,11 @@ export function polyfill(w: Window = window, d = document) { } // globals - var Element = w.HTMLElement || w.Element; - var SCROLL_TIME = 468; + const Element = w.HTMLElement || w.Element; + const SCROLL_TIME = 468; // object gathering original scroll methods - var original = { + const original = { scroll: w.scroll || w.scrollTo, scrollBy: w.scrollBy, elementScroll: Element.prototype.scroll || scrollElement, @@ -27,7 +27,7 @@ export function polyfill(w: Window = window, d = document) { }; // define timing method - var now = + const now = w.performance && w.performance.now ? w.performance.now.bind(w.performance) : Date.now; @@ -39,7 +39,7 @@ export function polyfill(w: Window = window, d = document) { * @returns {Boolean} */ function isMicrosoftBrowser(userAgent) { - var userAgentPatterns = ['MSIE ', 'Trident/', 'Edge/']; + const userAgentPatterns = ['MSIE ', 'Trident/', 'Edge/']; return new RegExp(userAgentPatterns.join('|')).test(userAgent); } @@ -49,7 +49,7 @@ export function polyfill(w: Window = window, d = document) { * rounding up scrollHeight and scrollWidth causing false positives * on hasScrollableSpace */ - var ROUNDING_TOLERANCE = isMicrosoftBrowser(w.navigator.userAgent) ? 1 : 0; + const ROUNDING_TOLERANCE = isMicrosoftBrowser(w.navigator.userAgent) ? 1 : 0; /** * changes scroll position inside an element @@ -130,7 +130,7 @@ export function polyfill(w: Window = window, d = document) { * @returns {Boolean} */ function canOverflow(el, axis) { - var overflowValue = w.getComputedStyle(el, null)['overflow' + axis]; + const overflowValue = w.getComputedStyle(el, null)['overflow' + axis]; return overflowValue === 'auto' || overflowValue === 'scroll'; } @@ -143,8 +143,8 @@ export function polyfill(w: Window = window, d = document) { * @returns {Boolean} */ function isScrollable(el) { - var isScrollableY = hasScrollableSpace(el, 'Y') && canOverflow(el, 'Y'); - var isScrollableX = hasScrollableSpace(el, 'X') && canOverflow(el, 'X'); + const isScrollableY = hasScrollableSpace(el, 'Y') && canOverflow(el, 'Y'); + const isScrollableX = hasScrollableSpace(el, 'X') && canOverflow(el, 'X'); return isScrollableY || isScrollableX; } @@ -170,11 +170,11 @@ export function polyfill(w: Window = window, d = document) { * @returns {undefined} */ function step(context) { - var time = now(); - var value; - var currentX; - var currentY; - var elapsed = (time - context.startTime) / SCROLL_TIME; + const time = now(); + let value; + let currentX; + let currentY; + let elapsed = (time - context.startTime) / SCROLL_TIME; // avoid elapsed times higher than one elapsed = elapsed > 1 ? 1 : elapsed; @@ -202,11 +202,11 @@ export function polyfill(w: Window = window, d = document) { * @returns {undefined} */ function smoothScroll(el, x, y) { - var scrollable; - var startX; - var startY; - var method; - var startTime = now(); + let scrollable; + let startX; + let startY; + let method; + const startTime = now(); // define scroll context if (el === d.body) { @@ -342,8 +342,8 @@ export function polyfill(w: Window = window, d = document) { return; } - var left = arguments[0].left; - var top = arguments[0].top; + const left = arguments[0].left; + const top = arguments[0].top; // LET THE SMOOTHNESS BEGIN! smoothScroll.call( @@ -396,9 +396,9 @@ export function polyfill(w: Window = window, d = document) { } // LET THE SMOOTHNESS BEGIN! - var scrollableParent = findScrollableParent(this); - var parentRects = scrollableParent.getBoundingClientRect(); - var clientRects = this.getBoundingClientRect(); + const scrollableParent = findScrollableParent(this); + const parentRects = scrollableParent.getBoundingClientRect(); + const clientRects = this.getBoundingClientRect(); if (scrollableParent !== d.body) { // reveal element inside parent diff --git a/packages/rrweb/src/replay/styles/inject-style.ts b/packages/rrweb/src/replay/styles/inject-style.ts new file mode 100644 index 00000000..2ba28dfb --- /dev/null +++ b/packages/rrweb/src/replay/styles/inject-style.ts @@ -0,0 +1,7 @@ +const rules: (blockClass: string) => string[] = (blockClass: string) => [ + 'noscript { display: none !important; }', + `.${blockClass} { background: currentColor; border-radius: 5px; }`, + `.${blockClass}:hover::after {content: 'Redacted'; color: white; background: black; text-align: center; width: 100%; display: block;}`, +]; + +export default rules; diff --git a/packages/rrweb/src/replay/styles/style.css b/packages/rrweb/src/replay/styles/style.css new file mode 100644 index 00000000..708f0673 --- /dev/null +++ b/packages/rrweb/src/replay/styles/style.css @@ -0,0 +1,91 @@ +.replayer-wrapper { + position: relative; +} +.replayer-mouse { + position: absolute; + width: 20px; + height: 20px; + transition: left 0.05s linear, top 0.05s linear; + background-size: contain; + background-position: center center; + background-repeat: no-repeat; + background-image: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgdmlld0JveD0iNyA1IDE4IDE4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyOCAyOCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgaGVpZ2h0PSIzMDBweCIgd2lkdGg9IjMwMHB4Ij4NCjxwb2x5Z29uIGZpbGw9IiNGRkZGRkYiIHBvaW50cz0iOC4yLDIwLjkgOC4yLDQuOSAxOS44LDE2LjUgMTMsMTYuNSAxMi42LDE2LjYgIi8+DQo8cG9seWdvbiBmaWxsPSIjRkZGRkZGIiBwb2ludHM9IjE3LjMsMjEuNiAxMy43LDIzLjEgOSwxMiAxMi43LDEwLjUgIi8+DQo8cmVjdCB4PSIxMi41IiB5PSIxMy42IiB0cmFuc2Zvcm09Im1hdHJpeCgwLjkyMjEgLTAuMzg3MSAwLjM4NzEgMC45MjIxIC01Ljc2MDUgNi41OTA5KSIgd2lkdGg9IjIiIGhlaWdodD0iOCIvPg0KPHBvbHlnb24gcG9pbnRzPSI5LjIsNy4zIDkuMiwxOC41IDEyLjIsMTUuNiAxMi42LDE1LjUgMTcuNCwxNS41ICIvPg0KPC9zdmc+DQo='); + border-color: transparent; /* otherwise we transition from black when .touch-device class is added */ +} + +.replayer-mouse::after { + background: transparent; +} + +.replayer-mouse.active::after { + animation: click 0.2s ease-in-out 1; +} +.replayer-mouse.touch-device { + background-image: none; /* there's no passive cursor on touch-only screens */ + width: 70px; + height: 70px; + border-width: 4px; + border-style: solid; + border-radius: 100%; + margin-left: -37px; + margin-top: -37px; + border-color: rgba(73, 80, 246, 0); + transition: left 0s linear, top 0s linear, border-color 0.2s ease-in-out; +} +.replayer-mouse.touch-device.touch-active { + border-color: rgba(73, 80, 246, 1); + transition: left 0.25s linear, top 0.25s linear, border-color 0.2s ease-in-out; +} +.replayer-mouse.touch-device::after { + opacity: 0; /* there's no passive cursor on touch-only screens */ +} +.replayer-mouse.touch-device.active::after { + animation: touch-click 0.2s ease-in-out 1; +} +.replayer-mouse-tail { + position: absolute; + pointer-events: none; +} + +@keyframes click { + 0% { + opacity: 0.3; + width: 20px; + height: 20px; + background: transparent; + } + 75% { + width: 24px; + height: 24px; + opacity: 0.8; + background: #dc2626; + } + 100% { + background: transparent; + } +} + +@keyframes touch-click { + 0% { + opacity: 0; + width: 20px; + height: 20px; + } + 50% { + opacity: 0.5; + width: 10px; + height: 10px; + } +} + +.rr-player { + position: relative; + background: white; + float: left; + border-radius: 5px; + box-shadow: 0 24px 48px rgba(17, 16, 62, 0.12); +} + +.rr-player__frame { + overflow: hidden; +} diff --git a/src/replay/timer.ts b/packages/rrweb/src/replay/timer.ts similarity index 78% rename from src/replay/timer.ts rename to packages/rrweb/src/replay/timer.ts index f256b8eb..35194e6c 100644 --- a/src/replay/timer.ts +++ b/packages/rrweb/src/replay/timer.ts @@ -6,7 +6,7 @@ import { } from '../types'; export class Timer { - public timeOffset: number = 0; + public timeOffset = 0; public speed: number; private actions: actionWithDelay[]; @@ -19,7 +19,6 @@ export class Timer { } /** * Add an action after the timer starts. - * @param action */ public addAction(action: actionWithDelay) { const index = this.findActionIndex(action); @@ -27,34 +26,38 @@ export class Timer { } /** * Add all actions before the timer starts - * @param actions */ public addActions(actions: actionWithDelay[]) { - this.actions.push(...actions); + this.actions = this.actions.concat(actions); + } + + public replaceActions(actions: actionWithDelay[]) { + this.actions.length = 0; + this.actions.splice(0, 0, ...actions); } public start() { - this.actions.sort((a1, a2) => a1.delay - a2.delay); this.timeOffset = 0; let lastTimestamp = performance.now(); const { actions } = this; - const self = this; - function check(time: number) { - self.timeOffset += (time - lastTimestamp) * self.speed; + const check = () => { + const time = performance.now(); + this.timeOffset += (time - lastTimestamp) * this.speed; lastTimestamp = time; while (actions.length) { const action = actions[0]; - if (self.timeOffset >= action.delay) { + + if (this.timeOffset >= action.delay) { actions.shift(); action.doAction(); } else { break; } } - if (actions.length > 0 || self.liveMode) { - self.raf = requestAnimationFrame(check); + if (actions.length > 0 || this.liveMode) { + this.raf = requestAnimationFrame(check); } - } + }; this.raf = requestAnimationFrame(check); } @@ -82,13 +85,15 @@ export class Timer { let start = 0; let end = this.actions.length - 1; while (start <= end) { - let mid = Math.floor((start + end) / 2); + const mid = Math.floor((start + end) / 2); if (this.actions[mid].delay < action.delay) { start = mid + 1; } else if (this.actions[mid].delay > action.delay) { end = mid - 1; } else { - return mid; + // already an action with same delay (timestamp) + // the plus one will splice the new one after the existing one + return mid + 1; } } return start; @@ -109,6 +114,7 @@ export function addDelay(event: eventWithTime, baselineTime: number): number { event.delay = firstTimestamp - baselineTime; return firstTimestamp - baselineTime; } + event.delay = event.timestamp - baselineTime; return event.delay; } diff --git a/packages/rrweb/src/rrdom/index.ts b/packages/rrweb/src/rrdom/index.ts new file mode 100644 index 00000000..28b5e590 --- /dev/null +++ b/packages/rrweb/src/rrdom/index.ts @@ -0,0 +1,173 @@ +import { RRdomTreeNode, AnyObject } from './tree-node'; + +class RRdomTree { + private readonly symbol = '__rrdom__'; + + public initialize(object: AnyObject) { + this._node(object); + + return object; + } + + public hasChildren(object: AnyObject): boolean { + return Boolean(this._node(object).hasChildren); + } + + public firstChild(object: AnyObject) { + return this._node(object).firstChild || null; + } + + public lastChild(object: AnyObject) { + return this._node(object).lastChild || null; + } + + public previousSibling(object: AnyObject) { + return this._node(object).previousSibling || null; + } + + public nextSibling(object: AnyObject) { + return this._node(object).nextSibling || null; + } + + public parent(object: AnyObject) { + return this._node(object).parent || null; + } + + public insertAfter(referenceObject: AnyObject, newObject: AnyObject) { + const referenceNode = this._node(referenceObject); + const nextNode = this._node(referenceNode.nextSibling); + const newNode = this._node(newObject); + const parentNode = this._node(referenceNode.parent); + + if (newNode.isAttached) { + throw new Error('Node already attached'); + } + if (!referenceNode) { + throw new Error('Reference node not attached'); + } + + newNode.parent = referenceNode.parent; + newNode.previousSibling = referenceObject; + newNode.nextSibling = referenceNode.nextSibling; + referenceNode.nextSibling = newObject; + + if (nextNode) { + nextNode.previousSibling = newObject; + } + + if (parentNode && parentNode.lastChild === referenceObject) { + parentNode.lastChild = newObject; + } + + if (parentNode) { + parentNode.childrenChanged(); + } + + return newObject; + } + + public insertBefore(referenceObject: AnyObject, newObject: AnyObject) { + const referenceNode = this._node(referenceObject); + const prevNode = this._node(referenceNode.previousSibling); + const newNode = this._node(newObject); + const parentNode = this._node(referenceNode.parent); + + if (newNode.isAttached) { + throw new Error('Node already attached'); + } + if (!referenceNode) { + throw new Error('Reference node not attached'); + } + + newNode.parent = referenceNode.parent; + newNode.previousSibling = referenceNode.previousSibling; + newNode.nextSibling = referenceObject; + referenceNode.previousSibling = newObject; + + if (prevNode) { + prevNode.nextSibling = newObject; + } + + if (parentNode && parentNode.firstChild === referenceObject) { + parentNode.firstChild = newObject; + } + + if (parentNode) { + parentNode.childrenChanged(); + } + + return newObject; + } + + public appendChild(referenceObject: AnyObject, newObject: AnyObject) { + const referenceNode = this._node(referenceObject); + const newNode = this._node(newObject); + + if (newNode.isAttached) { + throw new Error('Node already attached'); + } + if (!referenceNode) { + throw new Error('Reference node not attached'); + } + + if (referenceNode.hasChildren) { + this.insertAfter(referenceNode.lastChild!, newObject); + } else { + newNode.parent = referenceObject; + referenceNode.firstChild = newObject; + referenceNode.lastChild = newObject; + referenceNode.childrenChanged(); + } + + return newObject; + } + + public remove(removeObject: AnyObject) { + const removeNode = this._node(removeObject); + const parentNode = this._node(removeNode.parent); + const prevNode = this._node(removeNode.previousSibling); + const nextNode = this._node(removeNode.nextSibling); + + if (parentNode) { + if (parentNode.firstChild === removeObject) { + parentNode.firstChild = removeNode.nextSibling; + } + + if (parentNode.lastChild === removeObject) { + parentNode.lastChild = removeNode.previousSibling; + } + } + + if (prevNode) { + prevNode.nextSibling = removeNode.nextSibling; + } + + if (nextNode) { + nextNode.previousSibling = removeNode.previousSibling; + } + + removeNode.parent = null; + removeNode.previousSibling = null; + removeNode.nextSibling = null; + removeNode.cachedIndex = -1; + removeNode.cachedIndexVersion = NaN; + + if (parentNode) { + parentNode.childrenChanged(); + } + + return removeObject; + } + + private _node(object: AnyObject | null): RRdomTreeNode { + if (!object) { + throw new Error('Object is falsy'); + } + + if (this.symbol in object) { + return object[this.symbol] as RRdomTreeNode; + } + + return (object[this.symbol] = new RRdomTreeNode()); + } +} diff --git a/packages/rrweb/src/rrdom/tree-node.ts b/packages/rrweb/src/rrdom/tree-node.ts new file mode 100644 index 00000000..88839ee6 --- /dev/null +++ b/packages/rrweb/src/rrdom/tree-node.ts @@ -0,0 +1,51 @@ +export type AnyObject = { [key: string]: any; __rrdom__?: RRdomTreeNode }; + +export class RRdomTreeNode implements AnyObject { + public parent: AnyObject | null = null; + public previousSibling: AnyObject | null = null; + public nextSibling: AnyObject | null = null; + + public firstChild: AnyObject | null = null; + public lastChild: AnyObject | null = null; + + // This value is incremented anytime a children is added or removed + public childrenVersion = 0; + // The last child object which has a cached index + public childIndexCachedUpTo: AnyObject | null = null; + + /** + * This value represents the cached node index, as long as + * cachedIndexVersion matches with the childrenVersion of the parent + */ + public cachedIndex = -1; + public cachedIndexVersion = NaN; + + public get isAttached() { + return Boolean(this.parent || this.previousSibling || this.nextSibling); + } + + public get hasChildren() { + return Boolean(this.firstChild); + } + + public childrenChanged() { + this.childrenVersion = (this.childrenVersion + 1) & 0xffffffff; + this.childIndexCachedUpTo = null; + } + + public getCachedIndex(parentNode: AnyObject) { + if (this.cachedIndexVersion !== parentNode.childrenVersion) { + this.cachedIndexVersion = NaN; + // cachedIndex is no longer valid + return -1; + } + + return this.cachedIndex; + } + + public setCachedIndex(parentNode: AnyObject, index: number) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.cachedIndexVersion = parentNode.childrenVersion; + this.cachedIndex = index; + } +} diff --git a/src/types.ts b/packages/rrweb/src/types.ts similarity index 53% rename from src/types.ts rename to packages/rrweb/src/types.ts index 02f5ce1c..cfacfbda 100644 --- a/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -1,12 +1,19 @@ -import { +import type { serializedNodeWithId, - idNodeMap, + Mirror, INode, MaskInputOptions, SlimDOMOptions, -} from 'rrweb-snapshot'; -import { PackFn, UnpackFn } from './packer/base'; -import { FontFaceDescriptors } from 'css-font-loading-module'; + MaskInputFn, + MaskTextFn, +} from '@highlight-run/rrweb-snapshot'; +import type { PackFn, UnpackFn } from './packer/base'; +import type { IframeManager } from './record/iframe-manager'; +import type { ShadowDomManager } from './record/shadow-dom-manager'; +import type { Replayer } from './replay'; +import type { RRNode } from '@highlight-run/rrdom'; +import type { CanvasManager } from './record/observers/canvas/canvas-manager'; +import type { StylesheetManager } from './record/stylesheet-manager'; export enum EventType { DomContentLoaded, @@ -15,16 +22,24 @@ export enum EventType { IncrementalSnapshot, Meta, Custom, + Plugin, } +export type SessionInterval = { + startTime: number; + endTime: number; + duration: number; + active: boolean; +}; + export type domContentLoadedEvent = { type: EventType.DomContentLoaded; - data: {}; + data: unknown; }; export type loadedEvent = { type: EventType.Load; - data: {}; + data: unknown; }; export type fullSnapshotEvent = { @@ -52,11 +67,6 @@ export type metaEvent = { }; }; -export type logEvent = { - type: EventType.IncrementalSnapshot; - data: incrementalData; -}; - export type customEvent = { type: EventType.Custom; data: { @@ -65,7 +75,13 @@ export type customEvent = { }; }; -export type styleSheetEvent = {}; +export type pluginEvent = { + type: EventType.Plugin; + data: { + plugin: string; + payload: T; + }; +}; export enum IncrementalSource { Mutation, @@ -80,6 +96,8 @@ export enum IncrementalSource { CanvasMutation, Font, Log, + Drag, + StyleDeclaration, } export type mutationData = { @@ -87,7 +105,10 @@ export type mutationData = { } & mutationCallbackParam; export type mousemoveData = { - source: IncrementalSource.MouseMove | IncrementalSource.TouchMove; + source: + | IncrementalSource.MouseMove + | IncrementalSource.TouchMove + | IncrementalSource.Drag; positions: mousePosition[]; }; @@ -101,7 +122,7 @@ export type scrollData = { export type viewportResizeData = { source: IncrementalSource.ViewportResize; -} & viewportResizeDimention; +} & viewportResizeDimension; export type inputData = { source: IncrementalSource.Input; @@ -116,6 +137,10 @@ export type styleSheetRuleData = { source: IncrementalSource.StyleSheetRule; } & styleSheetRuleParam; +export type styleDeclarationData = { + source: IncrementalSource.StyleDeclaration; +} & styleDeclarationParam; + export type canvasMutationData = { source: IncrementalSource.CanvasMutation; } & canvasMutationParam; @@ -124,10 +149,6 @@ export type fontData = { source: IncrementalSource.Font; } & fontParam; -export type logData = { - source: IncrementalSource.Log; -} & LogParam; - export type incrementalData = | mutationData | mousemoveData @@ -139,7 +160,7 @@ export type incrementalData = | styleSheetRuleData | canvasMutationData | fontData - | logData; + | styleDeclarationData; export type event = | domContentLoadedEvent @@ -147,22 +168,59 @@ export type event = | fullSnapshotEvent | incrementalSnapshotEvent | metaEvent - | logEvent - | customEvent; + | customEvent + | pluginEvent; export type eventWithTime = event & { timestamp: number; delay?: number; }; +export type canvasEventWithTime = eventWithTime & { + type: EventType.IncrementalSnapshot; + data: canvasMutationData; +}; + export type blockClass = string | RegExp; +export type maskTextClass = string | RegExp; + +export type CanvasSamplingStrategy = Partial<{ + /** + * 'all' will record every single canvas call + * number between 1 and 60, will record an image snapshots in a web-worker a (maximum) number of times per second. + * Number only supported where [`OffscreenCanvas`](http://mdn.io/offscreencanvas) is supported. + */ + fps: 'all' | number; + /** + * A scaling to apply to canvas shapshotting. Adjusts the resolution at which + * canvases are recorded by this multiple. + */ + resizeFactor: number; + /** + * The quality of canvas snapshots + */ + resizeQuality: 'pixelated' | 'low' | 'medium' | 'high'; + /** + * The maximum dimension to take canvas snapshots at. + * This setting takes precedence over resizeFactor if the resulting image size + * from the resizeFactor calculation is larger than this value. + * Eg: set to 600 to ensure that the canvas is saved with images no larger than 600px + * in either dimension (while preserving the original canvas aspect ratio). + */ + maxSnapshotDimension: number; +}>; + export type SamplingStrategy = Partial<{ /** * false means not to record mouse/touch move events * number is the throttle threshold of recording mouse/touch move */ mousemove: boolean | number; + /** + * number is the throttle threshold of mouse/touch move callback + */ + mousemoveCallback: number; /** * false means not to record mouse interaction events * can also specify record some kinds of mouse interactions @@ -172,13 +230,30 @@ export type SamplingStrategy = Partial<{ * number is the throttle threshold of recording scroll */ scroll: number; + /** + * number is the throttle threshold of recording media interactions + */ + media: number; /** * 'all' will record all the input events * 'last' will only record the last input value while input a sequence of chars */ input: 'all' | 'last'; + + canvas: CanvasSamplingStrategy; }>; +export type RecordPlugin = { + name: string; + observer?: ( + cb: (...args: Array) => void, + win: IWindow, + options: TOptions, + ) => listenerHandler; + eventProcessor?: (event: eventWithTime) => eventWithTime & TExtend; + options: TOptions; +}; + export type recordOptions = { emit?: (e: T, isCheckout?: boolean) => void; checkoutEveryNth?: number; @@ -186,19 +261,30 @@ export type recordOptions = { blockClass?: blockClass; blockSelector?: string; ignoreClass?: string; + maskTextClass?: maskTextClass; + maskTextSelector?: string; maskAllInputs?: boolean; maskInputOptions?: MaskInputOptions; maskInputFn?: MaskInputFn; + maskTextFn?: MaskTextFn; slimDOMOptions?: SlimDOMOptions | 'all' | true; inlineStylesheet?: boolean; hooks?: hooksParam; packFn?: PackFn; sampling?: SamplingStrategy; recordCanvas?: boolean; + userTriggeredOnInput?: boolean; collectFonts?: boolean; + inlineImages?: boolean; + plugins?: RecordPlugin[]; // departed, please use sampling options mousemoveWait?: number; - recordLog?: boolean | LogRecordOptions; + keepIframeSrcFn?: KeepIframeSrcFn; + /** + * Enabling this will disable recording of text data on the page. This is useful if you do not want to record personally identifiable information. + * Text will be randomized. Instead of seeing "Hello World" in a recording, you will see "1fds1 j59a0". + */ + enableStrictPrivacy?: boolean; }; export type observerParam = { @@ -212,19 +298,64 @@ export type observerParam = { blockClass: blockClass; blockSelector: string | null; ignoreClass: string; + maskTextClass: maskTextClass; + maskTextSelector: string | null; maskInputOptions: MaskInputOptions; maskInputFn?: MaskInputFn; + maskTextFn?: MaskTextFn; + keepIframeSrcFn: KeepIframeSrcFn; inlineStylesheet: boolean; styleSheetRuleCb: styleSheetRuleCallback; + styleDeclarationCb: styleDeclarationCallback; canvasMutationCb: canvasMutationCallback; fontCb: fontCallback; - logCb: logCallback; - logOptions: LogRecordOptions; sampling: SamplingStrategy; recordCanvas: boolean; + inlineImages: boolean; + userTriggeredOnInput: boolean; collectFonts: boolean; slimDOMOptions: SlimDOMOptions; -}; + doc: Document; + mirror: Mirror; + iframeManager: IframeManager; + stylesheetManager: StylesheetManager; + shadowDomManager: ShadowDomManager; + canvasManager: CanvasManager; + enableStrictPrivacy: boolean; + plugins: Array<{ + observer: ( + cb: (...arg: Array) => void, + win: IWindow, + options: unknown, + ) => listenerHandler; + callback: (...arg: Array) => void; + options: unknown; + }>; +}; + +export type MutationBufferParam = Pick< + observerParam, + | 'mutationCb' + | 'blockClass' + | 'blockSelector' + | 'maskTextClass' + | 'maskTextSelector' + | 'inlineStylesheet' + | 'maskInputOptions' + | 'maskTextFn' + | 'maskInputFn' + | 'keepIframeSrcFn' + | 'recordCanvas' + | 'inlineImages' + | 'slimDOMOptions' + | 'doc' + | 'mirror' + | 'iframeManager' + | 'stylesheetManager' + | 'shadowDomManager' + | 'canvasManager' + | 'enableStrictPrivacy' +>; export type hooksParam = { mutation?: mutationCallBack; @@ -235,9 +366,9 @@ export type hooksParam = { input?: inputCallback; mediaInteaction?: mediaInteractionCallback; styleSheetRule?: styleSheetRuleCallback; + styleDeclaration?: styleDeclarationCallback; canvasMutation?: canvasMutationCallback; font?: fontCallback; - log?: logCallback; }; // https://dom.spec.whatwg.org/#interface-mutationrecord @@ -259,22 +390,29 @@ export type textMutation = { value: string | null; }; +export type styleAttributeValue = { + [key: string]: styleValueWithPriority | string | false; +}; + +export type styleValueWithPriority = [string, string]; + export type attributeCursor = { node: Node; attributes: { - [key: string]: string | null; + [key: string]: string | styleAttributeValue | null; }; }; export type attributeMutation = { id: number; attributes: { - [key: string]: string | null; + [key: string]: string | styleAttributeValue | null; }; }; export type removedNodeMutation = { parentId: number; id: number; + isShadow?: boolean; }; export type addedNodeMutation = { @@ -285,18 +423,22 @@ export type addedNodeMutation = { node: serializedNodeWithId; }; -type mutationCallbackParam = { +export type mutationCallbackParam = { texts: textMutation[]; attributes: attributeMutation[]; removes: removedNodeMutation[]; adds: addedNodeMutation[]; + isAttachIframe?: true; }; export type mutationCallBack = (m: mutationCallbackParam) => void; export type mousemoveCallBack = ( p: mousePosition[], - source: IncrementalSource.MouseMove | IncrementalSource.TouchMove, + source: + | IncrementalSource.MouseMove + | IncrementalSource.TouchMove + | IncrementalSource.Drag, ) => void; export type mousePosition = { @@ -306,6 +448,13 @@ export type mousePosition = { timeOffset: number; }; +export type mouseMovePos = { + x: number; + y: number; + id: number; + debugData: incrementalData; +}; + export enum MouseInteractions { MouseUp, MouseDown, @@ -317,8 +466,46 @@ export enum MouseInteractions { TouchStart, TouchMove_Departed, // we will start a separate observer for touch move event TouchEnd, + TouchCancel, +} + +export enum CanvasContext { + '2D', + WebGL, + WebGL2, } +export type SerializedCanvasArg = + | { + rr_type: 'ArrayBuffer'; + base64: string; // base64 + } + | { + rr_type: 'Blob'; + data: Array; + type?: string; + } + | { + rr_type: string; + src: string; // url of image + } + | { + rr_type: string; + args: Array; + } + | { + rr_type: string; + index: number; + }; + +export type CanvasArg = + | SerializedCanvasArg + | string + | number + | boolean + | null + | CanvasArg[]; + type mouseInteractionParam = { type: MouseInteractions; id: number; @@ -338,11 +525,11 @@ export type scrollCallback = (p: scrollPosition) => void; export type styleSheetAddRule = { rule: string; - index?: number; + index?: number | number[]; }; export type styleSheetDeleteRule = { - index: number; + index: number | number[]; }; export type styleSheetRuleParam = { @@ -353,15 +540,72 @@ export type styleSheetRuleParam = { export type styleSheetRuleCallback = (s: styleSheetRuleParam) => void; -export type canvasMutationCallback = (p: canvasMutationParam) => void; - -export type canvasMutationParam = { +export type styleDeclarationParam = { id: number; + index: number[]; + set?: { + property: string; + value: string | null; + priority: string | undefined; + }; + remove?: { + property: string; + }; +}; + +export type styleDeclarationCallback = (s: styleDeclarationParam) => void; + +export type canvasMutationCommand = { property: string; args: Array; setter?: true; }; +export type canvasMutationParam = + | { + id: number; + type: CanvasContext; + commands: canvasMutationCommand[]; + } + | ({ + id: number; + type: CanvasContext; + } & canvasMutationCommand); + +export type canvasMutationWithType = { + type: CanvasContext; +} & canvasMutationCommand; + +export type canvasMutationCallback = (p: canvasMutationParam) => void; + +export type canvasManagerMutationCallback = ( + target: HTMLCanvasElement, + p: canvasMutationWithType, +) => void; + +export type ImageBitmapDataURLWorkerParams = { + id: number; + bitmap: ImageBitmap; + width: number; + height: number; + canvasWidth: number; + canvasHeight: number; +}; + +export type ImageBitmapDataURLWorkerResponse = + | { + id: number; + } + | { + id: number; + type: string; + base64: string; + width: number; + height: number; + canvasWidth: number; + canvasHeight: number; + }; + export type fontParam = { family: string; fontSource: string; @@ -369,77 +613,24 @@ export type fontParam = { descriptors?: FontFaceDescriptors; }; -export type LogLevel = - | 'assert' - | 'clear' - | 'count' - | 'countReset' - | 'debug' - | 'dir' - | 'dirxml' - | 'error' - | 'group' - | 'groupCollapsed' - | 'groupEnd' - | 'info' - | 'log' - | 'table' - | 'time' - | 'timeEnd' - | 'timeLog' - | 'trace' - | 'warn'; - -/* fork from interface Console */ -// all kinds of console functions -export type Logger = { - assert?: (value: any, message?: string, ...optionalParams: any[]) => void; - clear?: () => void; - count?: (label?: string) => void; - countReset?: (label?: string) => void; - debug?: (message?: any, ...optionalParams: any[]) => void; - dir?: (obj: any, options?: NodeJS.InspectOptions) => void; - dirxml?: (...data: any[]) => void; - error?: (message?: any, ...optionalParams: any[]) => void; - group?: (...label: any[]) => void; - groupCollapsed?: (label?: any[]) => void; - groupEnd?: () => void; - info?: (message?: any, ...optionalParams: any[]) => void; - log?: (message?: any, ...optionalParams: any[]) => void; - table?: (tabularData: any, properties?: ReadonlyArray) => void; - time?: (label?: string) => void; - timeEnd?: (label?: string) => void; - timeLog?: (label?: string, ...data: any[]) => void; - trace?: (message?: any, ...optionalParams: any[]) => void; - warn?: (message?: any, ...optionalParams: any[]) => void; -}; - -/** - * define an interface to replay log records - * (data: logData) => void> function to display the log data - */ -export type ReplayLogger = Partial void>>; - -export type LogParam = { - level: LogLevel; - trace: Array; - payload: Array; -}; - export type fontCallback = (p: fontParam) => void; -export type logCallback = (p: LogParam) => void; - -export type viewportResizeDimention = { +export type viewportResizeDimension = { width: number; height: number; }; -export type viewportResizeCallback = (d: viewportResizeDimention) => void; +export type viewportResizeCallback = (d: viewportResizeDimension) => void; export type inputValue = { text: string; isChecked: boolean; + + // `userTriggered` indicates if this event was triggered directly by user (userTriggered: true) + // or was triggered indirectly (userTriggered: false) + // Example of `userTriggered` in action: + // User clicks on radio element (userTriggered: true) which triggers the other radio element to change (userTriggered: false) + userTriggered?: boolean; }; export type inputCallback = (v: inputValue & { id: number }) => void; @@ -447,21 +638,38 @@ export type inputCallback = (v: inputValue & { id: number }) => void; export const enum MediaInteractions { Play, Pause, + Seeked, + VolumeChange, } export type mediaInteractionParam = { type: MediaInteractions; id: number; + currentTime?: number; + volume?: number; + muted?: boolean; }; export type mediaInteractionCallback = (p: mediaInteractionParam) => void; -export type Mirror = { - map: idNodeMap; - getId: (n: INode) => number; +export type DocumentDimension = { + x: number; + y: number; + // scale value relative to its parent iframe + relativeScale: number; + // scale value relative to the root iframe + absoluteScale: number; +}; + +export type DeprecatedMirror = { + map: { + [key: number]: INode; + }; + getId: (n: Node) => number; getNode: (id: number) => INode | null; - removeNodeFromMap: (n: INode) => void; + removeNodeFromMap: (n: Node) => void; has: (id: number) => boolean; + reset: () => void; }; export type throttleOptions = { @@ -472,8 +680,16 @@ export type throttleOptions = { export type listenerHandler = () => void; export type hookResetter = () => void; +export type ReplayPlugin = { + handler: ( + event: eventWithTime, + isSync: boolean, + context: { replayer: Replayer }, + ) => void; +}; export type playerConfig = { speed: number; + maxSpeed: number; root: Element; loadTimeout: number; skipInactive: boolean; @@ -494,12 +710,10 @@ export type playerConfig = { strokeStyle?: string; }; unpackFn?: UnpackFn; - logConfig: LogReplayConfig; -}; - -export type LogReplayConfig = { - level?: Array | undefined; - replayLogger: ReplayLogger | undefined; + useVirtualDom: boolean; + plugins?: ReplayPlugin[]; + inactiveThreshold: number; + inactiveSkipTime: number; }; export type playerMetaData = { @@ -509,7 +723,7 @@ export type playerMetaData = { }; export type missingNode = { - node: Node; + node: Node | RRNode; mutation: addedNodeMutation; }; export type missingNodeMap = { @@ -549,29 +763,17 @@ export enum ReplayerEvents { CustomEvent = 'custom-event', Flush = 'flush', StateChange = 'state-change', + PlayBack = 'play-back', } -export type MaskInputFn = (text: string) => string; +export type KeepIframeSrcFn = (src: string) => boolean; -// store the state that would be changed during the process(unmount from dom and mount again) -export type ElementState = { - // [scrollLeft,scrollTop] - scroll?: [number, number]; -}; +declare global { + interface Window { + FontFace: typeof FontFace; + } +} -export type StringifyOptions = { - // limit of string length - stringLengthLimit?: number; - /** - * limit of number of keys in an object - * if an object contains more keys than this limit, we would call its toString function directly - */ - numOfKeysLimit: number; -}; +export type IWindow = Window & typeof globalThis; -export type LogRecordOptions = { - level?: Array | undefined; - lengthThreshold?: number; - stringifyOptions?: StringifyOptions; - logger?: Logger; -}; +export type Optional = Pick, K> & Omit; diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts new file mode 100644 index 00000000..4d410793 --- /dev/null +++ b/packages/rrweb/src/utils.ts @@ -0,0 +1,467 @@ +import type { + throttleOptions, + listenerHandler, + hookResetter, + blockClass, + addedNodeMutation, + DocumentDimension, + IWindow, + DeprecatedMirror, + textMutation, +} from './types'; +import type { IMirror, Mirror } from '@highlight-run/rrweb-snapshot'; +import { isShadowRoot, IGNORED_NODE, classMatchesRegex } from '@highlight-run/rrweb-snapshot'; +import type { RRNode, RRIFrameElement } from '@highlight-run/rrdom'; + +export function on( + type: string, + fn: EventListenerOrEventListenerObject, + target: Document | IWindow = document, +): listenerHandler { + const options = { capture: true, passive: true }; + target.addEventListener(type, fn, options); + return () => target.removeEventListener(type, fn, options); +} + +// https://github.com/rrweb-io/rrweb/pull/407 +const DEPARTED_MIRROR_ACCESS_WARNING = + 'Please stop import mirror directly. Instead of that,' + + '\r\n' + + 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' + + '\r\n' + + 'or you can use record.mirror to access the mirror instance during recording.'; +export let _mirror: DeprecatedMirror = { + map: {}, + getId() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return -1; + }, + getNode() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return null; + }, + removeNodeFromMap() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + has() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return false; + }, + reset() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, +}; +if (typeof window !== 'undefined' && window.Proxy && window.Reflect) { + _mirror = new Proxy(_mirror, { + get(target, prop, receiver) { + if (prop === 'map') { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return Reflect.get(target, prop, receiver); + }, + }); +} + +// copy from underscore and modified +export function throttle( + func: (arg: T) => void, + wait: number, + options: throttleOptions = {}, +) { + let timeout: ReturnType | null = null; + let previous = 0; + return function (...args: T[]) { + const now = Date.now(); + if (!previous && options.leading === false) { + previous = now; + } + const remaining = wait - (now - previous); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-this-alias + const context = this; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + func.apply(context, args); + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(() => { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + func.apply(context, args); + }, remaining); + } + }; +} + +export function hookSetter( + target: T, + key: string | number | symbol, + d: PropertyDescriptor, + isRevoked?: boolean, + win = window, +): hookResetter { + const original = win.Object.getOwnPropertyDescriptor(target, key); + win.Object.defineProperty( + target, + key, + isRevoked + ? d + : { + set(value) { + // put hooked setter into event loop to avoid of set latency + setTimeout(() => { + d.set!.call(this, value); + }, 0); + if (original && original.set) { + original.set.call(this, value); + } + }, + }, + ); + return () => hookSetter(target, key, original || {}, true); +} + +// copy from https://github.com/getsentry/sentry-javascript/blob/b2109071975af8bf0316d3b5b38f519bdaf5dc15/packages/utils/src/object.ts +export function patch( + source: { [key: string]: any }, + name: string, + replacement: (...args: unknown[]) => unknown, +): () => void { + try { + if (!(name in source)) { + return () => { + // + }; + } + + const original = source[name] as () => unknown; + const wrapped = replacement(original); + + // Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work + // otherwise it'll throw "TypeError: Object.defineProperties called on non-object" + if (typeof wrapped === 'function') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + wrapped.prototype = wrapped.prototype || {}; + Object.defineProperties(wrapped, { + __rrweb_original__: { + enumerable: false, + value: original, + }, + }); + } + + source[name] = wrapped; + + return () => { + source[name] = original; + }; + } catch { + return () => { + // + }; + // This can throw if multiple fill happens on a global object like XMLHttpRequest + // Fixes https://github.com/getsentry/sentry-javascript/issues/2043 + } +} + +export function getWindowHeight(): number { + return ( + window.innerHeight || + (document.documentElement && document.documentElement.clientHeight) || + (document.body && document.body.clientHeight) + ); +} + +export function getWindowWidth(): number { + return ( + window.innerWidth || + (document.documentElement && document.documentElement.clientWidth) || + (document.body && document.body.clientWidth) + ); +} + +/** + * Start of Highlight Code + */ +export const isCanvasNode = (node: Node | null): boolean => { + try { + if (node instanceof HTMLElement) { + return node.tagName === 'CANVAS'; + } + } catch { + return false; + } + return false; +}; +/** + * End of Highlight Code + */ + +/** + * Checks if the given element set to be blocked by rrweb + * @param node - node to check + * @param blockClass - class name to check + * @param ignoreParents - whether to search through parent nodes for the block class + * @returns true/false if the node was blocked or not + */ +export function isBlocked( + node: Node | null, + blockClass: blockClass, + checkAncestors: boolean, +): boolean { + if (!node) { + return false; + } + const el: HTMLElement | null = + node.nodeType === node.ELEMENT_NODE + ? (node as HTMLElement) + : node.parentElement; + if (!el) return false; + + if (typeof blockClass === 'string') { + if (el.classList.contains(blockClass)) return true; + if (checkAncestors && el.closest('.' + blockClass) !== null) return true; + } else { + if (classMatchesRegex(el, blockClass, checkAncestors)) return true; + } + return false; +} + +export function isSerialized(n: Node, mirror: Mirror): boolean { + return mirror.getId(n) !== -1; +} + +export function isIgnored(n: Node, mirror: Mirror): boolean { + // The main part of the slimDOM check happens in + // rrweb-snapshot::serializeNodeWithId + return mirror.getId(n) === IGNORED_NODE; +} + +export function isAncestorRemoved(target: Node, mirror: Mirror): boolean { + if (isShadowRoot(target)) { + return false; + } + const id = mirror.getId(target); + if (!mirror.has(id)) { + return true; + } + if ( + target.parentNode && + target.parentNode.nodeType === target.DOCUMENT_NODE + ) { + return false; + } + // if the root is not document, it means the node is not in the DOM tree anymore + if (!target.parentNode) { + return true; + } + return isAncestorRemoved(target.parentNode, mirror); +} + +export function isTouchEvent( + event: MouseEvent | TouchEvent, +): event is TouchEvent { + return Boolean((event as TouchEvent).changedTouches); +} + +export function polyfill(win = window) { + if ('NodeList' in win && !win.NodeList.prototype.forEach) { + // eslint-disable-next-line @typescript-eslint/unbound-method + win.NodeList.prototype.forEach = (Array.prototype + .forEach as unknown) as NodeList['forEach']; + } + + if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { + // eslint-disable-next-line @typescript-eslint/unbound-method + win.DOMTokenList.prototype.forEach = (Array.prototype + .forEach as unknown) as DOMTokenList['forEach']; + } + + // https://github.com/Financial-Times/polyfill-service/pull/183 + if (!Node.prototype.contains) { + Node.prototype.contains = (...args: unknown[]) => { + let node = args[0] as Node | null; + if (!(0 in args)) { + throw new TypeError('1 argument is required'); + } + + do { + if (this === node) { + return true; + } + } while ((node = node && node.parentNode)); + + return false; + }; + } +} + +type ResolveTree = { + value: addedNodeMutation; + children: ResolveTree[]; + parent: ResolveTree | null; +}; + +export function queueToResolveTrees(queue: addedNodeMutation[]): ResolveTree[] { + const queueNodeMap: Record = {}; + const putIntoMap = ( + m: addedNodeMutation, + parent: ResolveTree | null, + ): ResolveTree => { + const nodeInTree: ResolveTree = { + value: m, + parent, + children: [], + }; + queueNodeMap[m.node.id] = nodeInTree; + return nodeInTree; + }; + + const queueNodeTrees: ResolveTree[] = []; + for (const mutation of queue) { + const { nextId, parentId } = mutation; + if (nextId && nextId in queueNodeMap) { + const nextInTree = queueNodeMap[nextId]; + if (nextInTree.parent) { + const idx = nextInTree.parent.children.indexOf(nextInTree); + nextInTree.parent.children.splice( + idx, + 0, + putIntoMap(mutation, nextInTree.parent), + ); + } else { + const idx = queueNodeTrees.indexOf(nextInTree); + queueNodeTrees.splice(idx, 0, putIntoMap(mutation, null)); + } + continue; + } + if (parentId in queueNodeMap) { + const parentInTree = queueNodeMap[parentId]; + parentInTree.children.push(putIntoMap(mutation, parentInTree)); + continue; + } + queueNodeTrees.push(putIntoMap(mutation, null)); + } + + return queueNodeTrees; +} + +export function iterateResolveTree( + tree: ResolveTree, + cb: (mutation: addedNodeMutation) => unknown, +) { + cb(tree.value); + /** + * The resolve tree was designed to reflect the DOM layout, + * but we need append next sibling first, so we do a reverse + * loop here. + */ + for (let i = tree.children.length - 1; i >= 0; i--) { + iterateResolveTree(tree.children[i], cb); + } +} + +export type AppendedIframe = { + mutationInQueue: addedNodeMutation; + builtNode: HTMLIFrameElement | RRIFrameElement; +}; + +export function isSerializedIframe( + n: TNode, + mirror: IMirror, +): boolean { + return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n)); +} + +export function isSerializedStylesheet( + n: TNode, + mirror: IMirror, +): boolean { + return Boolean( + n.nodeName === 'LINK' && + n.nodeType === n.ELEMENT_NODE && + (n as HTMLElement).getAttribute && + (n as HTMLElement).getAttribute('rel') === 'stylesheet' && + mirror.getMeta(n), + ); +} + +export function getBaseDimension( + node: Node, + rootIframe: Node, +): DocumentDimension { + const frameElement = node.ownerDocument?.defaultView?.frameElement; + if (!frameElement || frameElement === rootIframe) { + return { + x: 0, + y: 0, + relativeScale: 1, + absoluteScale: 1, + }; + } + + const frameDimension = frameElement.getBoundingClientRect(); + const frameBaseDimension = getBaseDimension(frameElement, rootIframe); + // the iframe element may have a scale transform + const relativeScale = frameDimension.height / frameElement.clientHeight; + return { + x: + frameDimension.x * frameBaseDimension.relativeScale + + frameBaseDimension.x, + y: + frameDimension.y * frameBaseDimension.relativeScale + + frameBaseDimension.y, + relativeScale, + absoluteScale: frameBaseDimension.absoluteScale * relativeScale, + }; +} + +export function hasShadowRoot( + n: T, +): n is T & { shadowRoot: ShadowRoot } { + return Boolean(((n as unknown) as Element)?.shadowRoot); +} + +export function getNestedRule( + rules: CSSRuleList, + position: number[], +): CSSGroupingRule { + const rule = rules[position[0]] as CSSGroupingRule; + if (position.length === 1) { + return rule; + } else { + return getNestedRule( + (rule.cssRules[position[1]] as CSSGroupingRule).cssRules, + position.slice(2), + ); + } +} + +export function getPositionsAndIndex(nestedIndex: number[]) { + const positions = [...nestedIndex]; + const index = positions.pop(); + return { positions, index }; +} + +/** + * Returns the latest mutation in the queue for each node. + * @param mutations - mutations The text mutations to filter. + * @returns The filtered text mutations. + */ +export function uniqueTextMutations(mutations: textMutation[]): textMutation[] { + const idSet = new Set(); + const uniqueMutations: textMutation[] = []; + + for (let i = mutations.length; i--; ) { + const mutation = mutations[i]; + if (!idSet.has(mutation.id)) { + uniqueMutations.push(mutation); + idSet.add(mutation.id); + } + } + + return uniqueMutations; +} diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap new file mode 100644 index 00000000..3b4d7ed6 --- /dev/null +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -0,0 +1,13115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`record integration tests can freeze mutations 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"mutation observer\\", + \\"id\\": 8 + } + ], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 15 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 16 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 18 + } + ], + \\"id\\": 17 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 19 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 20, + \\"attributes\\": { + \\"foo\\": \\"bar\\" + } + }, + { + \\"id\\": 5, + \\"attributes\\": { + \\"test\\": \\"true\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 10, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": { + \\"foo\\": \\"bar\\" + }, + \\"childNodes\\": [], + \\"id\\": 20 + } + } + ] + } + } +]" +`; + +exports[`record integration tests can mask character data mutations 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"mutation observer\\", + \\"id\\": 8 + } + ], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 15 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 16 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 18 + } + ], + \\"id\\": 17 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 19 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 7, + \\"attributes\\": { + \\"class\\": \\"highlight-mask\\" + } + } + ], + \\"removes\\": [ + { + \\"parentId\\": 7, + \\"id\\": 8 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 10, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": { + \\"class\\": \\"highlight-mask\\" + }, + \\"childNodes\\": [], + \\"id\\": 20 + } + }, + { + \\"parentId\\": 20, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"*** **** ****\\", + \\"id\\": 21 + } + }, + { + \\"parentId\\": 7, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"*******\\", + \\"id\\": 22 + } + } + ] + } + } +]" +`; + +exports[`record integration tests can record attribute mutation 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"mutation observer\\", + \\"id\\": 8 + } + ], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 15 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 16 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 18 + } + ], + \\"id\\": 17 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 19 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 5, + \\"attributes\\": { + \\"test\\": \\"true\\" + } + } + ], + \\"removes\\": [ + { + \\"parentId\\": 5, + \\"id\\": 10 + } + ], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests can record character data muatations 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"mutation observer\\", + \\"id\\": 8 + } + ], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 15 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 16 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 18 + } + ], + \\"id\\": 17 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 19 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 5, + \\"id\\": 10 + }, + { + \\"parentId\\": 7, + \\"id\\": 8 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 7, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"mutated\\", + \\"id\\": 20 + } + } + ] + } + } +]" +`; + +exports[`record integration tests can record childList mutations 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"mutation observer\\", + \\"id\\": 8 + } + ], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 15 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 16 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 18 + } + ], + \\"id\\": 17 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 19 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 5, + \\"id\\": 10 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 7, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 20 + } + } + ] + } + } +]" +`; + +exports[`record integration tests can record form interactions 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"form fields\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"form\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"text\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"on\\" + }, + \\"childNodes\\": [], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + } + ], + \\"id\\": 25 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"off\\", + \\"checked\\": true + }, + \\"childNodes\\": [], + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"checkbox\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"checkbox\\" + }, + \\"childNodes\\": [], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + } + ], + \\"id\\": 35 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 39 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"textarea\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"textarea\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"cols\\": \\"30\\", + \\"rows\\": \\"10\\" + }, + \\"childNodes\\": [], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 40 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 44 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"select\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"select\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"value\\": \\"1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"1\\", + \\"selected\\": true + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 50 + } + ], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 51 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"2\\", + \\"id\\": 53 + } + ], + \\"id\\": 52 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 54 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 55 + } + ], + \\"id\\": 45 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 56 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"password\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 58 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"password\\" + }, + \\"childNodes\\": [], + \\"id\\": 59 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 60 + } + ], + \\"id\\": 57 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 61 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 62 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 64 + } + ], + \\"id\\": 63 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 65 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tes\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"test\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"off\\", + \\"isChecked\\": false, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tex\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"text\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"texta\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textar\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textare\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea \\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea t\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea te\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea tes\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea test\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"1\\", + \\"isChecked\\": false, + \\"id\\": 47 + } + } +]" +`; + +exports[`record integration tests can record node mutations 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Select2 3.5\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"link\\", + \\"attributes\\": { + \\"rel\\": \\"stylesheet\\", + \\"href\\": \\"https://cdn.jsdelivr.net/npm/select2@3.5.1/select2.css\\" + }, + \\"childNodes\\": [], + \\"id\\": 15 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\", + \\"id\\": 16 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"blockquote\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n Select2 is a jQuery replacement for select boxes.\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n In the 3.5 version it use a quite complicated DOM generation strategy which is a good battle-test for rrweb's recorder.\\\\n \\", + \\"id\\": 23 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"select2-container\\", + \\"id\\": \\"s2id_el\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"a\\", + \\"attributes\\": { + \\"href\\": \\"javascript:void(0)\\", + \\"class\\": \\"select2-choice\\", + \\"tabindex\\": \\"-1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": { + \\"class\\": \\"select2-chosen\\", + \\"id\\": \\"select2-chosen-1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"A\\", + \\"id\\": 29 + } + ], + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"abbr\\", + \\"attributes\\": { + \\"class\\": \\"select2-search-choice-close\\" + }, + \\"childNodes\\": [], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": { + \\"class\\": \\"select2-arrow\\", + \\"role\\": \\"presentation\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": { + \\"role\\": \\"presentation\\" + }, + \\"childNodes\\": [], + \\"id\\": 33 + } + ], + \\"id\\": 32 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"s2id_autogen1\\", + \\"class\\": \\"select2-offscreen\\" + }, + \\"childNodes\\": [], + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"class\\": \\"select2-focusser select2-offscreen\\", + \\"type\\": \\"text\\", + \\"aria-haspopup\\": \\"true\\", + \\"role\\": \\"button\\", + \\"aria-labelledby\\": \\"select2-chosen-1\\", + \\"id\\": \\"s2id_autogen1\\" + }, + \\"childNodes\\": [], + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"select2-drop select2-display-none select2-with-searchbox\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 37 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"select2-search\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 39 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"s2id_autogen1_search\\", + \\"class\\": \\"select2-offscreen\\" + }, + \\"childNodes\\": [], + \\"id\\": 40 + }, + { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\", + \\"autocomplete\\": \\"off\\", + \\"autocorrect\\": \\"off\\", + \\"autocapitalize\\": \\"off\\", + \\"spellcheck\\": \\"false\\", + \\"class\\": \\"select2-input\\", + \\"role\\": \\"combobox\\", + \\"aria-expanded\\": \\"true\\", + \\"aria-autocomplete\\": \\"list\\", + \\"aria-owns\\": \\"select2-results-1\\", + \\"id\\": \\"s2id_autogen1_search\\", + \\"placeholder\\": \\"\\" + }, + \\"childNodes\\": [], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 43 + } + ], + \\"id\\": 38 + }, + { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 44 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": { + \\"class\\": \\"select2-results\\", + \\"role\\": \\"listbox\\", + \\"id\\": \\"select2-results-1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 46 + } + ], + \\"id\\": 45 + } + ], + \\"id\\": 36 + } + ], + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"select\\", + \\"attributes\\": { + \\"id\\": \\"el\\", + \\"tabindex\\": \\"-1\\", + \\"title\\": \\"\\", + \\"style\\": \\"display: none;\\", + \\"value\\": \\"a\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"a\\", + \\"selected\\": true + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"A\\", + \\"id\\": 50 + } + ], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 51 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"b\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"B\\", + \\"id\\": 53 + } + ], + \\"id\\": 52 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 54 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 55 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 56 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 57 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/select2@3.5.2-browserify/select2.min.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 58 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 59 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 61 + } + ], + \\"id\\": 60 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": { + \\"role\\": \\"status\\", + \\"aria-live\\": \\"polite\\", + \\"class\\": \\"select2-hidden-accessible\\" + }, + \\"childNodes\\": [], + \\"id\\": 62 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 63 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 65 + } + ], + \\"id\\": 64 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 66 + } + ], + \\"id\\": 18 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 26 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 25, + \\"attributes\\": { + \\"class\\": \\"select2-container select2-dropdown-open select2-container-active\\" + } + }, + { + \\"id\\": 36, + \\"attributes\\": { + \\"id\\": \\"select2-drop\\", + \\"style\\": { + \\"left\\": \\"Npx\\", + \\"width\\": \\"Npx\\", + \\"top\\": \\"Npx\\", + \\"bottom\\": \\"auto\\", + \\"display\\": \\"block\\", + \\"position\\": false, + \\"visibility\\": false + }, + \\"class\\": \\"select2-drop select2-display-none select2-with-searchbox select2-drop-active\\" + } + }, + { + \\"id\\": 70, + \\"attributes\\": { + \\"style\\": { + \\"display\\": false + } + } + }, + { + \\"id\\": 42, + \\"attributes\\": { + \\"class\\": \\"select2-input select2-focused\\", + \\"aria-activedescendant\\": \\"select2-result-label-2\\" + } + }, + { + \\"id\\": 35, + \\"attributes\\": { + \\"disabled\\": \\"\\" + } + }, + { + \\"id\\": 72, + \\"attributes\\": { + \\"class\\": \\"select2-results-dept-0 select2-result select2-result-selectable select2-highlighted\\" + } + } + ], + \\"removes\\": [ + { + \\"parentId\\": 25, + \\"id\\": 26 + }, + { + \\"parentId\\": 25, + \\"id\\": 36 + }, + { + \\"parentId\\": 45, + \\"id\\": 46 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 25, + \\"nextId\\": 34, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"a\\", + \\"attributes\\": { + \\"href\\": \\"javascript:void(0)\\", + \\"class\\": \\"select2-choice\\", + \\"tabindex\\": \\"-1\\" + }, + \\"childNodes\\": [], + \\"id\\": 26 + } + }, + { + \\"parentId\\": 26, + \\"nextId\\": 28, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 27 + } + }, + { + \\"parentId\\": 26, + \\"nextId\\": 30, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": { + \\"class\\": \\"select2-chosen\\", + \\"id\\": \\"select2-chosen-1\\" + }, + \\"childNodes\\": [], + \\"id\\": 28 + } + }, + { + \\"parentId\\": 28, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"A\\", + \\"id\\": 29 + } + }, + { + \\"parentId\\": 26, + \\"nextId\\": 31, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"abbr\\", + \\"attributes\\": { + \\"class\\": \\"select2-search-choice-close\\" + }, + \\"childNodes\\": [], + \\"id\\": 30 + } + }, + { + \\"parentId\\": 26, + \\"nextId\\": 32, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 31 + } + }, + { + \\"parentId\\": 26, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": { + \\"class\\": \\"select2-arrow\\", + \\"role\\": \\"presentation\\" + }, + \\"childNodes\\": [], + \\"id\\": 32 + } + }, + { + \\"parentId\\": 32, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": { + \\"role\\": \\"presentation\\" + }, + \\"childNodes\\": [], + \\"id\\": 33 + } + }, + { + \\"parentId\\": 18, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"select2-drop select2-display-none select2-with-searchbox select2-drop-active\\", + \\"id\\": \\"select2-drop\\", + \\"style\\": \\"left: Npx; width: Npx; top: Npx; bottom: auto; display: block;\\" + }, + \\"childNodes\\": [], + \\"id\\": 36 + } + }, + { + \\"parentId\\": 36, + \\"nextId\\": 38, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 37 + } + }, + { + \\"parentId\\": 36, + \\"nextId\\": 44, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"select2-search\\" + }, + \\"childNodes\\": [], + \\"id\\": 38 + } + }, + { + \\"parentId\\": 38, + \\"nextId\\": 40, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 39 + } + }, + { + \\"parentId\\": 38, + \\"nextId\\": 41, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"s2id_autogen1_search\\", + \\"class\\": \\"select2-offscreen\\" + }, + \\"childNodes\\": [], + \\"id\\": 40 + } + }, + { + \\"parentId\\": 38, + \\"nextId\\": 42, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 41 + } + }, + { + \\"parentId\\": 38, + \\"nextId\\": 43, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\", + \\"autocomplete\\": \\"off\\", + \\"autocorrect\\": \\"off\\", + \\"autocapitalize\\": \\"off\\", + \\"spellcheck\\": \\"false\\", + \\"class\\": \\"select2-input select2-focused\\", + \\"role\\": \\"combobox\\", + \\"aria-expanded\\": \\"true\\", + \\"aria-autocomplete\\": \\"list\\", + \\"aria-owns\\": \\"select2-results-1\\", + \\"id\\": \\"s2id_autogen1_search\\", + \\"placeholder\\": \\"\\", + \\"aria-activedescendant\\": \\"select2-result-label-2\\" + }, + \\"childNodes\\": [], + \\"id\\": 42 + } + }, + { + \\"parentId\\": 38, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 43 + } + }, + { + \\"parentId\\": 36, + \\"nextId\\": 45, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 44 + } + }, + { + \\"parentId\\": 36, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": { + \\"class\\": \\"select2-results\\", + \\"role\\": \\"listbox\\", + \\"id\\": \\"select2-results-1\\" + }, + \\"childNodes\\": [], + \\"id\\": 45 + } + }, + { + \\"parentId\\": 45, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": { + \\"class\\": \\"select2-results-dept-0 select2-result select2-result-selectable\\", + \\"role\\": \\"presentation\\" + }, + \\"childNodes\\": [], + \\"id\\": 67 + } + }, + { + \\"parentId\\": 67, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"select2-result-label\\", + \\"id\\": \\"select2-result-label-3\\", + \\"role\\": \\"option\\" + }, + \\"childNodes\\": [], + \\"id\\": 68 + } + }, + { + \\"parentId\\": 68, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"B\\", + \\"id\\": 69 + } + }, + { + \\"parentId\\": 18, + \\"nextId\\": 36, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"select2-drop-mask\\", + \\"class\\": \\"select2-drop-mask\\", + \\"style\\": \\"\\" + }, + \\"childNodes\\": [], + \\"id\\": 70 + } + }, + { + \\"parentId\\": 62, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"2 results are available, use up and down arrow keys to navigate.\\", + \\"id\\": 71 + } + }, + { + \\"parentId\\": 45, + \\"nextId\\": 67, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": { + \\"class\\": \\"select2-results-dept-0 select2-result select2-result-selectable select2-highlighted\\", + \\"role\\": \\"presentation\\" + }, + \\"childNodes\\": [], + \\"id\\": 72 + } + }, + { + \\"parentId\\": 72, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"select2-result-label\\", + \\"id\\": \\"select2-result-label-2\\", + \\"role\\": \\"option\\" + }, + \\"childNodes\\": [], + \\"id\\": 73 + } + }, + { + \\"parentId\\": 73, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"A\\", + \\"id\\": 74 + } + }, + { + \\"parentId\\": 73, + \\"nextId\\": 74, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": { + \\"class\\": \\"select2-match\\" + }, + \\"childNodes\\": [], + \\"id\\": 75 + } + }, + { + \\"parentId\\": 68, + \\"nextId\\": 69, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": { + \\"class\\": \\"select2-match\\" + }, + \\"childNodes\\": [], + \\"id\\": 76 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"\\", + \\"isChecked\\": false, + \\"id\\": 35 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 70 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 36, + \\"attributes\\": { + \\"style\\": { + \\"color\\": [ + \\"black\\", + \\"important\\" + ] + } + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests can use maskInputOptions to configure which type of inputs should be masked 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"form fields\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"form\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"text\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"on\\" + }, + \\"childNodes\\": [], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + } + ], + \\"id\\": 25 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"off\\", + \\"checked\\": true + }, + \\"childNodes\\": [], + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"checkbox\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"checkbox\\" + }, + \\"childNodes\\": [], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + } + ], + \\"id\\": 35 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 39 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"textarea\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"textarea\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"cols\\": \\"30\\", + \\"rows\\": \\"10\\" + }, + \\"childNodes\\": [], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 40 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 44 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"select\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"select\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"value\\": \\"1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"1\\", + \\"selected\\": true + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 50 + } + ], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 51 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"2\\", + \\"id\\": 53 + } + ], + \\"id\\": 52 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 54 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 55 + } + ], + \\"id\\": 45 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 56 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"password\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 58 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"password\\" + }, + \\"childNodes\\": [], + \\"id\\": 59 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 60 + } + ], + \\"id\\": 57 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 61 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 62 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 64 + } + ], + \\"id\\": 63 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 65 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tes\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"test\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"off\\", + \\"isChecked\\": false, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tex\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"text\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"texta\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textar\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textare\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea \\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea t\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea te\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea tes\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea test\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*****\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"******\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*******\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"********\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"1\\", + \\"isChecked\\": false, + \\"id\\": 47 + } + } +]" +`; + +exports[`record integration tests mutations should work when blocked class is unblocked 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"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\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Uber Application for Codegen Testing\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 7 + } + ], + \\"id\\": 3 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 12 + } + ], + \\"id\\": 11 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 14 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"h1\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n Verify that block class bugs are fixed\\\\n \\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"first\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"visible\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"VISIBLE\\", + \\"id\\": 26 + } + ], + \\"id\\": 25 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 27 + } + ], + \\"id\\": 23 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 30 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 31 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 32 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"rr-block\\", + \\"rr_width\\": \\"1904px\\", + \\"rr_height\\": \\"21px\\" + }, + \\"childNodes\\": [], + \\"id\\": 33 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": { + \\"onclick\\": \\"mutate1()\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"MUTATE\\", + \\"id\\": 40 + } + ], + \\"id\\": 39 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 42 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 43 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 44 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 45 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"second\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"visible2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 50 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"VISIBLE\\", + \\"id\\": 52 + } + ], + \\"id\\": 51 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 53 + } + ], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 54 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 55 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 56 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 57 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 58 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"rr-block\\", + \\"rr_width\\": \\"1904px\\", + \\"rr_height\\": \\"21px\\" + }, + \\"childNodes\\": [], + \\"id\\": 59 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 60 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 61 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 62 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"br\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 63 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 64 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": { + \\"onclick\\": \\"mutate2()\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"MUTATE\\", + \\"id\\": 66 + } + ], + \\"id\\": 65 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 67 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 68 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 70 + } + ], + \\"id\\": 69 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 71 + } + ], + \\"id\\": 9 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 33, + \\"attributes\\": { + \\"class\\": \\"notB\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 23, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 72 + } + }, + { + \\"parentId\\": 72, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 73 + } + }, + { + \\"parentId\\": 73, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 74 + } + }, + { + \\"parentId\\": 74, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"I1I2 VISIBLE\\", + \\"id\\": 75 + } + }, + { + \\"parentId\\": 72, + \\"nextId\\": 73, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 76 + } + }, + { + \\"parentId\\": 76, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 77 + } + }, + { + \\"parentId\\": 77, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"I1I1 VISIBLE\\", + \\"id\\": 78 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 65 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 39 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 65 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 65 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 65 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 59, + \\"attributes\\": { + \\"class\\": \\"notB\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 49, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 79 + } + }, + { + \\"parentId\\": 79, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 80 + } + }, + { + \\"parentId\\": 80, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 81 + } + }, + { + \\"parentId\\": 81, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"I1I2 VISIBLE\\", + \\"id\\": 82 + } + }, + { + \\"parentId\\": 79, + \\"nextId\\": 80, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 83 + } + }, + { + \\"parentId\\": 83, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 84 + } + }, + { + \\"parentId\\": 84, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"I1I1 VISIBLE\\", + \\"id\\": 85 + } + } + ] + } + } +]" +`; + +exports[`record integration tests should mask texts 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Mask text\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": { + \\"class\\": \\"highlight-mask\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"*****\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"highlight-mask\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"*****\\", + \\"id\\": 24 + } + ], + \\"id\\": 23 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"data-masking\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"*****\\", + \\"id\\": 32 + } + ], + \\"id\\": 31 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + } + ], + \\"id\\": 29 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + } + ], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 37 + } + ], + \\"id\\": 36 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 38 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + } +]" +`; + +exports[`record integration tests should mask texts using maskTextFn 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Mask text\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": { + \\"class\\": \\"highlight-mask\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"****1\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"highlight-mask\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"****2\\", + \\"id\\": 24 + } + ], + \\"id\\": 23 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"data-masking\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"****3\\", + \\"id\\": 32 + } + ], + \\"id\\": 31 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + } + ], + \\"id\\": 29 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + } + ], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 37 + } + ], + \\"id\\": 36 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 38 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + } +]" +`; + +exports[`record integration tests should mask value attribute with maskInputOptions 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Document\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"password\\", + \\"id\\": \\"password\\" + }, + \\"childNodes\\": [], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 21 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 24 + } + ], + \\"id\\": 23 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 25 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 18, + \\"attributes\\": { + \\"value\\": \\"*\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 18, + \\"attributes\\": { + \\"value\\": \\"**\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 18, + \\"attributes\\": { + \\"value\\": \\"***\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 18, + \\"attributes\\": { + \\"value\\": \\"****\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*****\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 18, + \\"attributes\\": { + \\"value\\": \\"*****\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"******\\", + \\"isChecked\\": false, + \\"id\\": 18 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 18, + \\"attributes\\": { + \\"value\\": \\"******\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests should nest record iframe 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"Main\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"iframe { width: 500px; height: 500px; }\\", + \\"isStyle\\": true, + \\"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\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"one\\" + }, + \\"childNodes\\": [], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 22 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 25 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 26 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 19, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 30 + } + ], + \\"rootId\\": 27, + \\"id\\": 28 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 27 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 17, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"two\\" + }, + \\"childNodes\\": [], + \\"id\\": 31 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 31, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 32, + \\"id\\": 33 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 32, + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 38 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 32, + \\"id\\": 39 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 40 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Frame 1\\", + \\"rootId\\": 32, + \\"id\\": 42 + } + ], + \\"rootId\\": 32, + \\"id\\": 41 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 43 + } + ], + \\"rootId\\": 32, + \\"id\\": 35 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 44 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n frame 1\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"three\\", + \\"frameborder\\": \\"0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 32, + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"four\\", + \\"frameborder\\": \\"0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 32, + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 50 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"svg\\", + \\"attributes\\": { + \\"xmlns\\": \\"http://www.w3.org/2000/svg\\", + \\"xmlns:xlink\\": \\"http://www.w3.org/1999/xlink\\", + \\"width\\": \\"300\\", + \\"height\\": \\"300\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 52 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"rect\\", + \\"attributes\\": { + \\"id\\": \\"el\\", + \\"width\\": \\"100\\", + \\"height\\": \\"50\\", + \\"x\\": \\"40\\", + \\"y\\": \\"20\\", + \\"fill\\": \\"red\\" + }, + \\"childNodes\\": [], + \\"isSVG\\": true, + \\"rootId\\": 32, + \\"id\\": 53 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 54 + } + ], + \\"isSVG\\": true, + \\"rootId\\": 32, + \\"id\\": 51 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"rootId\\": 32, + \\"id\\": 55 + } + ], + \\"rootId\\": 32, + \\"id\\": 45 + } + ], + \\"rootId\\": 32, + \\"id\\": 34 + } + ], + \\"id\\": 32 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 47, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 56, + \\"id\\": 58 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 56, + \\"id\\": 59 + } + ], + \\"rootId\\": 56, + \\"id\\": 57 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 56 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 49, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 60, + \\"id\\": 61 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 60, + \\"id\\": 64 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 60, + \\"id\\": 65 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 60, + \\"id\\": 66 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 60, + \\"id\\": 67 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 60, + \\"id\\": 68 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Frame 2\\", + \\"rootId\\": 60, + \\"id\\": 70 + } + ], + \\"rootId\\": 60, + \\"id\\": 69 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 60, + \\"id\\": 71 + } + ], + \\"rootId\\": 60, + \\"id\\": 63 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 60, + \\"id\\": 72 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n frame 2\\\\n \\\\n \\", + \\"rootId\\": 60, + \\"id\\": 74 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"rootId\\": 60, + \\"id\\": 76 + } + ], + \\"rootId\\": 60, + \\"id\\": 75 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"rootId\\": 60, + \\"id\\": 77 + } + ], + \\"rootId\\": 60, + \\"id\\": 73 + } + ], + \\"rootId\\": 60, + \\"id\\": 62 + } + ], + \\"id\\": 60 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 73, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"five\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 60, + \\"id\\": 78 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 78, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 79, + \\"id\\": 81 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 79, + \\"id\\": 82 + } + ], + \\"rootId\\": 79, + \\"id\\": 80 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 79 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + } +]" +`; + +exports[`record integration tests should not record blocked elements and its child nodes 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Block record\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"highlight-block\\", + \\"rr_width\\": \\"50px\\", + \\"rr_height\\": \\"50px\\" + }, + \\"childNodes\\": [], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 21 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 22 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + } +]" +`; + +exports[`record integration tests should not record blocked elements dynamically added 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Block record\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"highlight-block\\", + \\"rr_width\\": \\"50px\\", + \\"rr_height\\": \\"50px\\" + }, + \\"childNodes\\": [], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 21 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 22 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": 18, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": { + \\"class\\": \\"highlight-block\\", + \\"rr_width\\": \\"100px\\", + \\"rr_height\\": \\"100px\\" + }, + \\"childNodes\\": [], + \\"id\\": 23 + } + } + ] + } + } +]" +`; + +exports[`record integration tests should not record input events on ignored elements 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"ignore fields\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"form\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"ignore text\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\", + \\"class\\": \\"highlight-ignore\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 23 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 27 + } + ], + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 28 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 22 + } + } +]" +`; + +exports[`record integration tests should not record input values if maskAllInputs is enabled 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"form fields\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"form\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"text\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"on\\" + }, + \\"childNodes\\": [], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + } + ], + \\"id\\": 25 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"off\\", + \\"checked\\": true + }, + \\"childNodes\\": [], + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"checkbox\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"checkbox\\" + }, + \\"childNodes\\": [], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + } + ], + \\"id\\": 35 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 39 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"textarea\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"textarea\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"cols\\": \\"30\\", + \\"rows\\": \\"10\\" + }, + \\"childNodes\\": [], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 40 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 44 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"select\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"select\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"value\\": \\"*\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 50 + } + ], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 51 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"2\\", + \\"id\\": 53 + } + ], + \\"id\\": 52 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 54 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 55 + } + ], + \\"id\\": 45 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 56 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"password\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 58 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"password\\" + }, + \\"childNodes\\": [], + \\"id\\": 59 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 60 + } + ], + \\"id\\": 57 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 61 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 62 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 64 + } + ], + \\"id\\": 63 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 65 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"off\\", + \\"isChecked\\": false, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*****\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"******\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*******\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"********\\", + \\"isChecked\\": false, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*****\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"******\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*******\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"********\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*********\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**********\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***********\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"************\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*************\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"id\\": 47 + } + } +]" +`; + +exports[`record integration tests should record DOM node movement 1 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 22 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 23 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 5, + \\"id\\": 12 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 7, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 12 + } + }, + { + \\"parentId\\": 12, + \\"nextId\\": 14, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + } + }, + { + \\"parentId\\": 12, + \\"nextId\\": 19, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 14 + } + }, + { + \\"parentId\\": 14, + \\"nextId\\": 16, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + } + }, + { + \\"parentId\\": 14, + \\"nextId\\": 18, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 16 + } + }, + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 17 + } + }, + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + } + }, + { + \\"parentId\\": 12, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + } + } + ] + } + } +]" +`; + +exports[`record integration tests should record DOM node movement 2 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 22 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 23 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 5, + \\"id\\": 12 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 12, + \\"nextId\\": 14, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + } + }, + { + \\"parentId\\": 12, + \\"nextId\\": 19, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"i\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 14 + } + }, + { + \\"parentId\\": 14, + \\"nextId\\": 16, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + } + }, + { + \\"parentId\\": 14, + \\"nextId\\": 18, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"b\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 16 + } + }, + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 17 + } + }, + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + } + }, + { + \\"parentId\\": 12, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + } + }, + { + \\"parentId\\": 5, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 24 + } + }, + { + \\"parentId\\": 24, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 12 + } + } + ] + } + } +]" +`; + +exports[`record integration tests should record canvas mutations 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"canvas\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": { + \\"id\\": \\"myCanvas\\", + \\"width\\": \\"200\\", + \\"height\\": \\"100\\", + \\"style\\": \\"border: 1px solid #000000;\\", + \\"rr_dataURL\\": \\"LOOKS LIKE WE COULD NOT GET STABLE BASE64 FROM SAME IMAGE.\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 23 + } + ], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 24 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 16, + \\"type\\": 0, + \\"commands\\": [ + { + \\"property\\": \\"moveTo\\", + \\"args\\": [ + 0, + 0 + ] + }, + { + \\"property\\": \\"lineTo\\", + \\"args\\": [ + 200, + 100 + ] + }, + { + \\"property\\": \\"stroke\\", + \\"args\\": [] + } + ] + } + } +]" +`; + +exports[`record integration tests should record console messages 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Log record\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 20 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"assert\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:2:21\\" + ], + \\"payload\\": [ + \\"true\\", + \\"\\\\\\"assert\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"count\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:3:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"count\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"countReset\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:4:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"count\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"debug\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:5:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"debug\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"dir\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:6:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"dir\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"dirxml\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:7:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"dirxml\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"group\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:8:21\\" + ], + \\"payload\\": [] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"groupCollapsed\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:9:21\\" + ], + \\"payload\\": [] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"info\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:10:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"info\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"log\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:11:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"log\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"table\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:12:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"table\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"time\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:13:21\\" + ], + \\"payload\\": [] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"timeEnd\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:14:21\\" + ], + \\"payload\\": [] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"timeLog\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:15:21\\" + ], + \\"payload\\": [] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"trace\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:16:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"trace\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:17:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"warn\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"clear\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:18:21\\" + ], + \\"payload\\": [] + } + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"log\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:19:21\\" + ], + \\"payload\\": [ + \\"\\\\\\"TypeError: a message\\\\\\\\n at __puppeteer_evaluation_script__:19:25\\\\\\\\nEnd of stack for Error object\\\\\\"\\" + ] + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 21, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 22, + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 22, + \\"id\\": 25 + } + ], + \\"rootId\\": 22, + \\"id\\": 23 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 22 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"log\\", + \\"trace\\": [], + \\"payload\\": [ + \\"\\\\\\"from iframe\\\\\\"\\" + ] + } + } + } +]" +`; + +exports[`record integration tests should record dynamic CSS changes 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"react styled components\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 16 + } + ], + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": { + \\"data-styled\\": \\"active\\", + \\"data-styled-version\\": \\"5.0.1\\", + \\"_cssText\\": \\".ixzlRK { font-size: 1.5em; text-align: center; color: palevioletred; }.eOXmez { font-size: 1.5em; text-align: center; color: rebeccapurple; }.bJCmFu { padding: 4em; background: papayawhip; }\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\", + \\"isStyle\\": true, + \\"id\\": 18 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"app\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"section\\", + \\"attributes\\": { + \\"class\\": \\"sc-AxirZ bJCmFu\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"h1\\", + \\"attributes\\": { + \\"class\\": \\"sc-AxjAm ixzlRK\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Hello World!\\", + \\"id\\": 25 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"h1\\", + \\"attributes\\": { + \\"class\\": \\"sc-AxjAm eOXmez toggle\\", + \\"color\\": \\"rebeccapurple\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Hello World!\\", + \\"id\\": 27 + } + ], + \\"id\\": 26 + } + ], + \\"id\\": 23 + } + ], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/react@16/umd/react.production.min.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 29 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/react-dom@16/umd/react-dom.production.min.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 31 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 32 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/react-is@16.13.1/umd/react-is.production.min.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 33 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/styled-components@5.0.1/dist/styled-components.min.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 35 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/babel-standalone@6/babel.min.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"text/babel\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 40 + } + ], + \\"id\\": 39 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 43 + } + ], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 44 + } + ], + \\"id\\": 20 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 26 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 26 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 26 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 17, + \\"adds\\": [ + { + \\"rule\\": \\".pqkNE{font-size:1.5em;text-align:center;color:pink;}\\", + \\"index\\": 2 + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 26, + \\"attributes\\": { + \\"class\\": \\"sc-AxjAm pqkNE toggle\\", + \\"color\\": \\"pink\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests should record images inside iframe with blob url 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"Frame with image\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"four\\", + \\"frameborder\\": \\"0\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 20 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 21, + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 26 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 27 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 28 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Image with blob:url\\", + \\"rootId\\": 21, + \\"id\\": 33 + } + ], + \\"rootId\\": 21, + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 34 + } + ], + \\"rootId\\": 21, + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 21, + \\"id\\": 37 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"rootId\\": 21, + \\"id\\": 39 + } + ], + \\"rootId\\": 21, + \\"id\\": 38 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"rootId\\": 21, + \\"id\\": 40 + } + ], + \\"rootId\\": 21, + \\"id\\": 36 + } + ], + \\"rootId\\": 21, + \\"id\\": 23 + } + ], + \\"id\\": 21 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 36, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"img\\", + \\"attributes\\": { + \\"src\\": \\"blob:http://localhost:3030/...\\", + \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 21, + \\"id\\": 41 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 41, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 41, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests should record images inside iframe with blob url after iframe was reloaded 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"Frame 2\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n frame 2\\\\n \\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 21 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"five\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 26 + } + ], + \\"rootId\\": 23, + \\"id\\": 24 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 23 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 27, + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 33 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 34 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 36 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 37 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Image with blob:url\\", + \\"rootId\\": 27, + \\"id\\": 39 + } + ], + \\"rootId\\": 27, + \\"id\\": 38 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 40 + } + ], + \\"rootId\\": 27, + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 27, + \\"id\\": 43 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"rootId\\": 27, + \\"id\\": 45 + } + ], + \\"rootId\\": 27, + \\"id\\": 44 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"rootId\\": 27, + \\"id\\": 46 + } + ], + \\"rootId\\": 27, + \\"id\\": 42 + } + ], + \\"rootId\\": 27, + \\"id\\": 29 + } + ], + \\"id\\": 27 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 42, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"img\\", + \\"attributes\\": { + \\"src\\": \\"blob:http://localhost:3030/...\\", + \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 47 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 47, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 47, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests should record images with blob url 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"IE=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 8 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Image with blob:url\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 22 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 23 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"img\\", + \\"attributes\\": { + \\"src\\": \\"blob:http://localhost:3030/...\\", + \\"rr_dataURL\\": \\"data:image/png;base64,...\\" + }, + \\"childNodes\\": [], + \\"id\\": 24 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 24, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 24, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + +exports[`record integration tests should record input userTriggered values if userTriggeredOnInput is enabled 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"meta\\", + \\"attributes\\": { + \\"http-equiv\\": \\"X-UA-Compatible\\", + \\"content\\": \\"ie=edge\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"form fields\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"form\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"text\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"on\\" + }, + \\"childNodes\\": [], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + } + ], + \\"id\\": 25 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"name\\": \\"toggle\\", + \\"value\\": \\"off\\", + \\"checked\\": true + }, + \\"childNodes\\": [], + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"checkbox\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"checkbox\\" + }, + \\"childNodes\\": [], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + } + ], + \\"id\\": 35 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 39 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"textarea\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"textarea\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"cols\\": \\"30\\", + \\"rows\\": \\"10\\" + }, + \\"childNodes\\": [], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 40 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 44 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"select\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"select\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"value\\": \\"1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"1\\", + \\"selected\\": true + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 50 + } + ], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 51 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"2\\", + \\"id\\": 53 + } + ], + \\"id\\": 52 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 54 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 55 + } + ], + \\"id\\": 45 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 56 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"password\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 58 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"password\\" + }, + \\"childNodes\\": [], + \\"id\\": 59 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 60 + } + ], + \\"id\\": 57 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 61 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 62 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 64 + } + ], + \\"id\\": 63 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 65 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tes\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"test\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"userTriggered\\": true, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"off\\", + \\"isChecked\\": false, + \\"userTriggered\\": false, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"userTriggered\\": true, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*****\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"******\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*******\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"********\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 59 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tex\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"text\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"texta\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textar\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textare\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea \\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea t\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea te\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea tes\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea test\\", + \\"isChecked\\": false, + \\"userTriggered\\": true, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"1\\", + \\"isChecked\\": false, + \\"userTriggered\\": false, + \\"id\\": 47 + } + } +]" +`; + +exports[`record integration tests should record mutations in iframes accross pages 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"Frame 2\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n frame 2\\\\n \\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 21 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"five\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 26 + } + ], + \\"rootId\\": 23, + \\"id\\": 24 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 23 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 30 + } + ], + \\"rootId\\": 27, + \\"id\\": 28 + } + ], + \\"id\\": 27 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 30, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 27, + \\"id\\": 31 + } + } + ] + } + } +]" +`; + +exports[`record integration tests should record nested iframes and shadow doms 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"Frame 2\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n frame 2\\\\n \\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 21 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"five\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 25 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 26 + } + ], + \\"rootId\\": 23, + \\"id\\": 24 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 23 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 26, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 27, + \\"isShadow\\": true + } + }, + { + \\"parentId\\": 27, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 23, + \\"id\\": 28 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 28, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 29, + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 29, + \\"id\\": 32 + } + ], + \\"rootId\\": 29, + \\"id\\": 30 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 29 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 32, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 29, + \\"id\\": 33, + \\"isShadow\\": true + } + } + ] + } + } +]" +`; + +exports[`record integration tests should record shadow DOM 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"Shadow DOM Observer\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\".my-element { margin: 0px 0px 1rem; }iframe { border: 0px; width: 100%; padding: 0px; }body { max-width: 400px; margin: 1rem auto; padding: 0px 1rem; font-family: \\\\\\"comic sans ms\\\\\\"; }\\", + \\"isStyle\\": true, + \\"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\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellat odit\\\\n officiis necessitatibus laborum asperiores et adipisci dolores corporis,\\\\n vero distinctio voluptas, suscipit commodi architecto, aliquam fugit.\\\\n Nesciunt labore reiciendis blanditiis!\\\\n \\", + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"my-element\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 5, + \\"textContent\\": \\" Also could be a \\\\n \\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26, + \\"isShadow\\": true + }, + { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"body { margin: 0px; }p { border: 1px solid rgb(204, 204, 204); padding: 1rem; color: red; font-family: sans-serif; }\\", + \\"isStyle\\": true, + \\"id\\": 28 + } + ], + \\"id\\": 27, + \\"isShadow\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 29, + \\"isShadow\\": true + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Element with Shadow DOM\\", + \\"id\\": 31 + } + ], + \\"id\\": 30, + \\"isShadow\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\", + \\"id\\": 32, + \\"isShadow\\": true + } + ], + \\"id\\": 22, + \\"isShadowHost\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 33 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellat odit\\\\n officiis necessitatibus laborum asperiores et adipisci dolores corporis,\\\\n vero distinctio voluptas, suscipit commodi architecto, aliquam fugit.\\\\n Nesciunt labore reiciendis blanditiis!\\\\n \\", + \\"id\\": 35 + } + ], + \\"id\\": 34 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 38 + } + ], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 39 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 41 + } + ], + \\"id\\": 40 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 42 + } + ], + \\"id\\": 17 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 43, + \\"isShadow\\": true + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 43, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 44 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 22, + \\"id\\": 30, + \\"isShadow\\": true + } + ], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 44, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"hi\\", + \\"id\\": 45 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 44, + \\"id\\": 45 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 44, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"123\\", + \\"id\\": 46 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 44, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 47, + \\"isShadow\\": true + } + }, + { + \\"parentId\\": 47, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"nested shadow dom\\", + \\"id\\": 48 + } + } + ] + } + } +]" +`; + +exports[`record integration tests should record shadow doms polyfilled by shadydom 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/@webcomponents/shadydom@1.9.0/shadydom.min.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 11 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target3\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 25 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 28 + } + ], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 29 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 30 + } + } + ] + } + } +]" +`; + +exports[`record integration tests should record shadow doms polyfilled by synthetic-shadow 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/@lwc/synthetic-shadow@2.20.3/dist/synthetic-shadow.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 11 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 17, + \\"isShadow\\": true + } + ], + \\"id\\": 16, + \\"isShadowHost\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target3\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 25 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 28 + } + ], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 29 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 30 + } + }, + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target4\\" + }, + \\"childNodes\\": [], + \\"id\\": 31, + \\"isShadowHost\\": true + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 31, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 32, + \\"isShadow\\": true + } + } + ] + } + } +]" +`; + +exports[`record integration tests should record webgl canvas mutations 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"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\\": \\"canvas\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": { + \\"id\\": \\"myCanvas\\", + \\"width\\": \\"200\\", + \\"height\\": \\"100\\", + \\"style\\": \\"border: 1px solid #000000\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 23 + } + ], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 24 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 16, + \\"type\\": 1, + \\"commands\\": [ + { + \\"property\\": \\"clearColor\\", + \\"args\\": [ + 1, + 0, + 0, + 1 + ] + }, + { + \\"property\\": \\"clear\\", + \\"args\\": [ + 16384 + ] + } + ] + } + } +]" +`; + +exports[`record integration tests will serialize node before record 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"mutation observer\\", + \\"id\\": 8 + } + ], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 15 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 16 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 18 + } + ], + \\"id\\": 17 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 19 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 10, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 20 + } + }, + { + \\"parentId\\": 10, + \\"nextId\\": 20, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + } + }, + { + \\"parentId\\": 10, + \\"nextId\\": 21, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 22 + } + } + ] + } + } +]" +`; diff --git a/packages/rrweb/test/__snapshots__/packer.test.ts.snap b/packages/rrweb/test/__snapshots__/packer.test.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..2323b5a7af71cfb60b1d657ae462fe6ac4556885 GIT binary patch literal 165 zcmdPbSMW+LE>Q^1ODrhP$S+YSGt^PYC@Co@w$j&6&(GIO&(Tjyat!nd;NnWHD9A4= zDUMDkNKDRFNKVXC05ej{Qu9g_3=?7%Y!woeDh|y#v^q@d&`KST&_kPy4o?X5^3?GR zIkf4}szd7zPYu*M+PP=Sf5`JwFwW`{N#FfhsMysterious Button\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 8 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 7, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 9, + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Mysterious Button\\", + \\"rootId\\": 9, + \\"id\\": 14 + } + ], + \\"rootId\\": 9, + \\"id\\": 13 + } + ], + \\"rootId\\": 9, + \\"id\\": 12 + } + ], + \\"rootId\\": 9, + \\"id\\": 10 + } + ], + \\"id\\": 9 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 11, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": { + \\"_cssText\\": \\"body { background: rgb(0, 0, 0); }@media {\\\\n body { background: rgb(0, 0, 0); }\\\\n}\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 9, + \\"id\\": 15 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 15, + \\"adds\\": [ + { + \\"rule\\": \\"body { color: #fff; }\\" + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 15, + \\"adds\\": [ + { + \\"rule\\": \\"body { color: #ccc; }\\", + \\"index\\": [ + 2, + 0 + ] + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 15, + \\"removes\\": [ + { + \\"index\\": 0 + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"id\\": 15, + \\"set\\": { + \\"property\\": \\"color\\", + \\"value\\": \\"green\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 15, + \\"removes\\": [ + { + \\"index\\": [ + 1, + 0 + ] + } + ] + } + } +]" +`; + +exports[`record is safe to checkout during async callbacks 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\", + \\"size\\": \\"40\\" + }, + \\"childNodes\\": [], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 8 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 5, + \\"id\\": 7 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 5, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 9 + } + }, + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 10, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"test\\", + \\"id\\": 11 + } + } + ] + } + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"test\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + } + ], + \\"id\\": 9 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 9, + \\"id\\": 10 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 5, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10 + } + }, + { + \\"parentId\\": 10, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"test\\", + \\"id\\": 11 + } + } + ] + } + } +]" +`; + +exports[`record should record scroll position 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\", + \\"size\\": \\"40\\" + }, + \\"childNodes\\": [], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 8 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 5, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": { + \\"style\\": \\"overflow: auto; height: Npx; width: Npx;\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + } + }, + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"testtesttesttesttesttesttesttesttesttest\\", + \\"id\\": 10 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 3, + \\"id\\": 9, + \\"x\\": 10, + \\"y\\": 10 + } + } +]" +`; + +exports[`record without CSSGroupingRule support captures nested stylesheet rules 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\", + \\"size\\": \\"40\\" + }, + \\"childNodes\\": [], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 8 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 4, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": { + \\"_cssText\\": \\"@media {\\\\n body { background: rgb(0, 0, 0); }\\\\n}\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 9, + \\"adds\\": [ + { + \\"rule\\": \\"body { color: #fff; }\\", + \\"index\\": [ + 0, + 0 + ] + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 9, + \\"removes\\": [ + { + \\"index\\": [ + 0, + 0 + ] + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 9, + \\"adds\\": [ + { + \\"rule\\": \\"body { color: #ccc; }\\", + \\"index\\": [ + 0, + 0 + ] + } + ] + } + } +]" +`; diff --git a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap new file mode 100644 index 00000000..6b1f1033 --- /dev/null +++ b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap @@ -0,0 +1,242 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`replayer can fast forward past StyleSheetRule changes on virtual elements 1`] = ` +"file-frame-4 + + + + + +
+
+ +
+ + + + +file-frame-5 + + + + + + + + + + + string + + + + +file-cid-0 +@charset \\"utf-8\\"; + +.highlight-block { background: currentcolor; } + +noscript { display: none !important; } + +html.rrweb-paused *, html.rrweb-paused ::before, html.rrweb-paused ::after { animation-play-state: paused !important; } + + +file-cid-1 +@charset \\"utf-8\\"; + +.css-added-at-400 { padding: 1.3125rem; flex: 0 0 auto; width: 100%; } + + +file-cid-2 +@charset \\"utf-8\\"; + +.css-added-at-200-overwritten-at-3000 { opacity: 1; transform: translateX(0px); } + +.css-added-at-500-overwritten-at-3000 { border: 1px solid blue; } + + +file-cid-3 +@charset \\"utf-8\\"; + +.css-added-at-200 { position: fixed; top: 0px; right: 0px; left: 4rem; z-index: 15; flex-shrink: 0; height: 0.25rem; overflow: hidden; background-color: rgb(17, 171, 209); } + +.css-added-at-200.alt { height: 0.25rem; background-color: rgb(190, 232, 242); opacity: 0; transition: opacity 0.5s ease 0s; } + +.css-added-at-1000-deleted-at-2500 { display: flex; flex-direction: column; min-width: 60rem; min-height: 100vh; color: blue; } + +.css-added-at-200.alt2 { padding-left: 4rem; } +" +`; + +exports[`replayer can fast forward past StyleSheetRule deletion on virtual elements 1`] = ` +"file-frame-0 + + + + + + +" +`; + +exports[`replayer can handle removing style elements 1`] = ` +"file-frame-1 + + + + + +
+
+ +
+ + + + +file-frame-2 + + + + + + + + + +file-cid-0 +@charset \\"utf-8\\"; + +.highlight-block { background: currentcolor; } + +noscript { display: none !important; } + +html.rrweb-paused *, html.rrweb-paused ::before, html.rrweb-paused ::after { animation-play-state: paused !important; } +" +`; + +exports[`replayer replays same timestamp events in correct order (with addAction) 1`] = ` +"file-frame-1 + + + + + +
+
+ +
+ + + + +file-frame-2 + + + + + + + + Final - correct + + + + +file-cid-0 +@charset \\"utf-8\\"; + +.highlight-block { background: currentcolor; } + +noscript { display: none !important; } + +html.rrweb-paused *, html.rrweb-paused ::before, html.rrweb-paused ::after { animation-play-state: paused !important; } +" +`; + +exports[`replayer replays same timestamp events in correct order 1`] = ` +"file-frame-1 + + + + + +
+
+ +
+ + + + +file-frame-2 + + + + + + + + Final - correct + + + + +file-cid-0 +@charset \\"utf-8\\"; + +.highlight-block { background: currentcolor; } + +noscript { display: none !important; } + +html.rrweb-paused *, html.rrweb-paused ::before, html.rrweb-paused ::after { animation-play-state: paused !important; } +" +`; diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts new file mode 100644 index 00000000..26f03280 --- /dev/null +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -0,0 +1,179 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type { Page } from 'puppeteer'; +import type { eventWithTime, recordOptions } from '../../src/types'; +import { startServer, launchPuppeteer, ISuite, getServerURL } from '../utils'; + +const suites: Array< + { + title: string; + eval: string; + times?: number; // defaults to 5 + } & ({ html: string } | { url: string }) +> = [ + // { + // title: 'benchmarking external website', + // url: 'http://localhost:5050', + // eval: 'document.querySelector("button").click()', + // times: 10, + // }, + { + title: 'create 1000x10 DOM nodes', + html: 'benchmark-dom-mutation.html', + eval: 'window.workload()', + times: 10, + }, + { + title: 'create 1000x10x2 DOM nodes and remove a bunch of them', + html: 'benchmark-dom-mutation-add-and-remove.html', + eval: 'window.workload()', + times: 10, + }, +]; + +function avg(v: number[]): number { + return v.reduce((prev, cur) => prev + cur, 0) / v.length; +} + +describe('benchmark: mutation observer', () => { + jest.setTimeout(240000); + let page: ISuite['page']; + let browser: ISuite['browser']; + let server: ISuite['server']; + + beforeAll(async () => { + server = await startServer(); + browser = await launchPuppeteer({ + dumpio: true, + headless: true, + }); + }); + + afterEach(async () => { + await page.close(); + }); + + afterAll(async () => { + server.close(); + await browser.close(); + }); + + const getHtml = (fileName: string): string => { + const filePath = path.resolve(__dirname, `../html/${fileName}`); + return fs.readFileSync(filePath, 'utf8'); + }; + + const addRecordingScript = async (page: Page) => { + // const scriptUrl = `${getServerURL(server)}/rrweb-1.1.3.js`; + const scriptUrl = `${getServerURL(server)}/rrweb.js`; + await page.evaluate((url) => { + const scriptEl = document.createElement('script'); + scriptEl.src = url; + document.head.append(scriptEl); + }, scriptUrl); + await page.waitForFunction('window.rrweb'); + }; + + for (const suite of suites) { + it(suite.title, async () => { + page = await browser.newPage(); + page.on('console', (message) => + console.log(`${message.type().toUpperCase()} ${message.text()}`), + ); + + const loadPage = async () => { + if ('html' in suite) { + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, suite.html)); + } else { + await page.goto(suite.url); + } + + await addRecordingScript(page); + }; + + const getDuration = async (): Promise => { + return (await page.evaluate((triggerWorkloadScript) => { + return new Promise((resolve, reject) => { + let start = 0; + let lastEvent: eventWithTime | null; + const options: recordOptions = { + emit: (event) => { + // console.log(event.type, event.timestamp); + if (event.type !== 5 || event.data.tag !== 'FTAG') { + lastEvent = event; + return; + } + if (!lastEvent) { + reject('no events recorded'); + return; + } + resolve(lastEvent.timestamp - start); + }, + }; + const record = (window as any).rrweb.record; + record(options); + + start = Date.now(); + eval(triggerWorkloadScript); + + requestAnimationFrame(() => { + record.addCustomEvent('FTAG', {}); + }); + }); + }, suite.eval)) as number; + }; + + // generate profile.json file + const profileFilename = `profile-${new Date().toISOString()}.json`; + const tempDirectory = path.resolve(path.join(__dirname, '../../temp')); + fs.mkdirSync(tempDirectory, { recursive: true }); + const profilePath = path.resolve(tempDirectory, profileFilename); + + const client = await page.target().createCDPSession(); + await client.send('Emulation.setCPUThrottlingRate', { rate: 6 }); + + await page.tracing.start({ + path: profilePath, + screenshots: true, + categories: [ + '-*', + 'devtools.timeline', + 'v8.execute', + 'disabled-by-default-devtools.timeline', + 'disabled-by-default-devtools.timeline.frame', + 'toplevel', + 'blink.console', + 'blink.user_timing', + 'latencyInfo', + 'disabled-by-default-devtools.timeline.stack', + 'disabled-by-default-v8.cpu_profiler', + 'disabled-by-default-v8.cpu_profiler.hires', + ], + }); + await loadPage(); + await getDuration(); + await page.waitForTimeout(1000); + await page.tracing.stop(); + await client.send('Emulation.setCPUThrottlingRate', { rate: 1 }); + + // calculate durations + const times = suite.times ?? 5; + const durations: number[] = []; + for (let i = 0; i < times; i++) { + await loadPage(); + const duration = await getDuration(); + durations.push(duration); + } + + console.table([ + { + ...suite, + duration: avg(durations), + durations: durations.join(', '), + }, + ]); + console.log('profile: ', profilePath); + }); + } +}); diff --git a/packages/rrweb/test/benchmark/replay-fast-forward.test.ts b/packages/rrweb/test/benchmark/replay-fast-forward.test.ts new file mode 100644 index 00000000..4e17acdf --- /dev/null +++ b/packages/rrweb/test/benchmark/replay-fast-forward.test.ts @@ -0,0 +1,170 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type { eventWithTime, recordOptions } from '../../src/types'; +import { launchPuppeteer, ISuite } from '../utils'; + +const suites: Array<{ + title: string; + eval: string; + eventsString?: string; + times?: number; // defaults to 5 +}> = [ + { + title: 'append 70 x 70 x 70 elements', + eval: ` + () => { + return new Promise((resolve) => { + const branches = 70; + const depth = 3; + // append children for the current node + function expand(node, depth) { + if (depth == 0) return; + for (let b = 0; b < branches; b++) { + const child = document.createElement('div'); + node.appendChild(child); + expand(child, depth - 1); + } + } + const frag = document.createDocumentFragment(); + const node = document.createElement('div'); + expand(node, depth); + frag.appendChild(node); + document.body.appendChild(frag); + resolve(); + }); + }; + `, + times: 3, + }, + { + title: 'append 1000 elements and reverse their order', + eval: ` + () => { + return new Promise(async (resolve) => { + const branches = 1000; + function waitForTimeout(timeout) { + return new Promise((resolve) => setTimeout(() => resolve(), timeout)); + } + const frag = document.createDocumentFragment(); + const node = document.createElement('div'); + for (let b = 0; b < branches; b++) { + const child = document.createElement('div'); + node.appendChild(child); + child.textContent = b + 1; + } + frag.appendChild(node); + document.body.appendChild(frag); + const children = node.children; + await waitForTimeout(0); + // reverse the order of children + for (let i = children.length - 1; i >= 0; i--) + node.appendChild(node.children[i]); + resolve(); + }); + }; + `, + times: 3, + }, +]; + +function avg(v: number[]): number { + return v.reduce((prev, cur) => prev + cur, 0) / v.length; +} + +describe('benchmark: replayer fast-forward performance', () => { + jest.setTimeout(240000); + let code: ISuite['code']; + let page: ISuite['page']; + let browser: ISuite['browser']; + + beforeAll(async () => { + browser = await launchPuppeteer({ + headless: true, + args: ['--disable-dev-shm-usage'], + }); + + const bundlePath = path.resolve(__dirname, '../../dist/rrweb.min.js'); + code = fs.readFileSync(bundlePath, 'utf8'); + + for (const suite of suites) + suite.eventsString = await generateEvents(suite.eval); + }, 600_000); + + afterAll(async () => { + await browser.close(); + }); + + for (const suite of suites) { + it( + suite.title, + async () => { + suite.times = suite.times ?? 5; + const durations: number[] = []; + for (let i = 0; i < suite.times; i++) { + page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(` + + + `); + const duration = (await page.evaluate(() => { + const replayer = new (window as any).rrweb.Replayer( + (window as any).events, + ); + const start = Date.now(); + replayer.play(replayer.getMetaData().totalTime + 100); + return Date.now() - start; + })) as number; + durations.push(duration); + await page.close(); + } + + console.table([ + { + title: suite.title, + times: suite.times, + duration: avg(durations), + durations: durations.join(', '), + }, + ]); + }, + 60_000, + ); + } + + /** + * Get the recorded events after the mutation function is executed. + */ + async function generateEvents(mutateNodesFn: string): Promise { + const page = await browser.newPage(); + + await page.goto('about:blank'); + await page.setContent(` + + `); + const eventsString = (await page.evaluate((mutateNodesFn) => { + return new Promise((resolve) => { + const events: eventWithTime[] = []; + const options: recordOptions = { + emit: (event) => { + events.push(event); + }, + }; + const record = (window as any).rrweb.record; + record(options); + eval(mutateNodesFn)().then(() => { + resolve(JSON.stringify(events)); + }); + }); + }, mutateNodesFn)) as string; + + await page.close(); + return eventsString; + } +}); diff --git a/packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-image-1-snap.png b/packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-image-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..731898701b69eeca46729aa8eef2af872597d9de GIT binary patch literal 10913 zcmeAS@N?(olHy`uVBq!ia0y~yU~geyV6ov~1Bz6wRMrPljKx9jP7LeL$-HD>P+;(M zaSW-L^XB%(TqQ?_w!mjwuI5PQ9ho@0fKlXOk6Xq`)ibW*b9sf>p1r$Vt*Bh~zt{fk zj~_o~umcSOfuL`-^Y@4KLpTf%_I<0hfAhQU#=k!wzJ1$P#19oO|Mc@`rG*S1yLk&E zL}(F9B81VP;V1!NDR>EhTnPjnLZixn!9YoXY`e;wXL>B+*FVeWm%cNDLzN-sahm~z zbG_t>0NuXEpEt z%jeKQu$d)ITqqz6!V*|p*UA;FzWnh|yuCcc%?~b8CtSWZxL4NJ|GfAg7&LRIL8EKU zL+V6poXzt+6?VsVfI_6cRNH~lZ4&ZPY@hyjvr5__07`XgFYXL?JiqT3FwjyA( zMGeXgqb+K1nJ`*uLW;)GN)sFoBfipvq=wPLVYF}mdKI;Vst0Q@>>_5c6? literal 0 HcmV?d00001 diff --git a/packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-square-1-snap.png b/packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-square-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..e22bc48831104120ac0c4a19cd3b9f41dc62b426 GIT binary patch literal 10812 zcmeAS@N?(olHy`uVBq!ia0y~yU~geyV6ov~1Bz6wRMrPljKx9jP7LeL$-HD>P+;(M zaSW-L^XAUR+`|S8u8vwCOxA3k^!vd>vQoAdb7--lIu=j~*Nh&If%k?S`+Ch<=I$!Ol9Sr{Z><@wFZ*LC!v-SLD_78`D z-()Te+9?l_PN^`GBF1PUT`;N)9GE1>+-SIsh8zC8jX#Cx?YFA6&S3q2`}j>}NOoTE z@~Og}YgR?jVur!`bBYN>#$k#A4}>M4$^ljF!g&ZH#1PnIFsh7!VKhL%sbDmPfP-N) z%RtJ6(ZT^74x@#`XyGtgQ9_D@(Y!F47e@2KXkK6#Z6iSv!Dt%^91f%P!bq(bK3ttA zJB5LVg^?%5>*|{N`1FNpT7C00}Tl=sjP@{zdC_#)C4&ZDtTJ3^^VYIam z4hBkw0$$CXdGoI2^E-Laa^`{i`FH2u-FpWynBhUXylc0-o#vIc*B}v3S3j3^P6 { + 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.min.js'); + code = fs.readFileSync(bundlePath, 'utf8'); + }); + + afterEach(async () => { + await page.close(); + }); + + afterAll(async () => { + await server.close(); + await browser.close(); + }); + + const getHtml = ( + fileName: string, + options: recordOptions = {}, + ): string => { + const filePath = path.resolve(__dirname, `../html/${fileName}`); + const html = fs.readFileSync(filePath, 'utf8'); + return replaceLast( + html, + '', + ` + + + `, + ); + }; + + const fakeGoto = async (p: puppeteer.Page, url: string) => { + const intercept = async (request: puppeteer.HTTPRequest) => { + await request.respond({ + status: 200, + contentType: 'text/html', + body: ' ', // non-empty string or page will load indefinitely + }); + }; + await p.setRequestInterception(true); + p.on('request', intercept); + await p.goto(url); + p.off('request', intercept); + await p.setRequestInterception(false); + }; + + const hideMouseAnimation = async (p: puppeteer.Page) => { + await p.addStyleTag({ + content: '.replayer-mouse-tail{display: none !important;}', + }); + }; + + it('will record and replay a webgl square', async () => { + page = await browser.newPage(); + await fakeGoto(page, `${serverURL}/html/canvas-webgl-square.html`); + + await page.setContent( + getHtml.call(this, 'canvas-webgl-square.html', { recordCanvas: true }), + ); + + await waitForRAF(page); + + const snapshots: eventWithTime[] = await page.evaluate('window.snapshots'); + + page = await browser.newPage(); + + await page.goto('about:blank'); + await page.evaluate(code); + + await hideMouseAnimation(page); + await page.evaluate(`let events = ${JSON.stringify(snapshots)}`); + await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events, { + UNSAFE_replayCanvas: true, + }); + replayer.play(500); + `); + await waitForRAF(page); + + const element = await page.$('iframe'); + const frameImage = await element!.screenshot(); + + expect(frameImage).toMatchImageSnapshot(); + }); + + it('will record and replay a webgl image', async () => { + page = await browser.newPage(); + await fakeGoto(page, `${serverURL}/html/canvas-webgl-image.html`); + + await page.setContent( + getHtml.call(this, 'canvas-webgl-image.html', { recordCanvas: true }), + ); + + await page.waitForTimeout(100); + const snapshots: eventWithTime[] = await page.evaluate('window.snapshots'); + + page = await browser.newPage(); + + await page.goto('about:blank'); + await page.evaluate(code); + + await hideMouseAnimation(page); + await page.evaluate(`let events = ${JSON.stringify(snapshots)}`); + await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events, { + UNSAFE_replayCanvas: true, + }); + `); + // wait for iframe to get added and `preloadAllImages` to ge called + await page.waitForSelector('iframe'); + await page.evaluate(`replayer.play(500);`); + await waitForRAF(page); + + const element = await page.$('iframe'); + const frameImage = await element!.screenshot(); + + expect(frameImage).toMatchImageSnapshot(); + }); +}); diff --git a/packages/rrweb/test/events/canvas-in-iframe.ts b/packages/rrweb/test/events/canvas-in-iframe.ts new file mode 100644 index 00000000..972d8ec1 --- /dev/null +++ b/packages/rrweb/test/events/canvas-in-iframe.ts @@ -0,0 +1,181 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); + +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1200, + height: 500, + }, + timestamp: now + 100, + }, + { + type: EventType.FullSnapshot, + data: { + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 4 }, + { + type: 2, + tagName: 'meta', + attributes: { charset: 'utf-8' }, + childNodes: [], + id: 5, + }, + { type: 3, textContent: ' \n ', id: 6 }, + ], + id: 3, + }, + { type: 3, textContent: '\n ', id: 7 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 9 }, + { + type: 2, + tagName: 'iframe', + attributes: { id: 'target' }, + childNodes: [], + id: 19, + }, + { type: 3, textContent: '\n\n', id: 27 }, + ], + id: 8, + }, + ], + id: 2, + }, + ], + compatMode: 'BackCompat', + id: 1, + }, + initialOffset: { left: 0, top: 0 }, + }, + timestamp: now + 200, + }, + // add an iframe + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 19, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + rootId: 30, + id: 32, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + rootId: 30, + id: 33, + }, + ], + rootId: 30, + id: 31, + }, + ], + compatMode: 'BackCompat', + id: 30, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 500, + }, + // add two canvas, one is blank ans the other is filled with data + { + type: EventType.IncrementalSnapshot, + data: { + source: 0, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 33, + nextId: null, + node: { + type: 2, + tagName: 'canvas', + attributes: { + width: '10', + height: '10', + id: 'blank_canvas', + }, + childNodes: [], + rootId: 30, + id: 34, + }, + }, + { + parentId: 33, + nextId: null, + node: { + type: 2, + tagName: 'canvas', + attributes: { + width: '10', + height: '10', + rr_dataURL: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAB5JREFUKFNjZCASMBKpjmEQKvzPwIDqrEHoRozgBQC/ZQELU4DiXAAAAABJRU5ErkJggg==', + id: 'canvas_with_data', + }, + childNodes: [], + rootId: 30, + id: 35, + }, + }, + ], + }, + timestamp: now + 500, + }, +]; + +export default events; diff --git a/packages/rrweb/test/events/iframe.ts b/packages/rrweb/test/events/iframe.ts new file mode 100644 index 00000000..bfa75822 --- /dev/null +++ b/packages/rrweb/test/events/iframe.ts @@ -0,0 +1,591 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); + +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1200, + height: 500, + }, + timestamp: now + 100, + }, + { + type: EventType.FullSnapshot, + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + }, + { + id: 5, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + timestamp: now + 200, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'iframe', + attributes: { id: 'one' }, + childNodes: [], + id: 6, + }, + }, + ], + }, + timestamp: now + 500, + }, + // add iframe one + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 6, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 1, + name: 'html', + publicId: '', + systemId: '', + rootId: 7, + id: 8, + }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + rootId: 7, + id: 10, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: '\n\t\tiframe 1\n\t', + rootId: 7, + id: 13, + }, + ], + rootId: 7, + id: 12, + }, + { type: 3, textContent: '\n\t', rootId: 7, id: 14 }, + { + type: 2, + tagName: 'script', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'SCRIPT_PLACEHOLDER', + rootId: 7, + id: 16, + }, + ], + rootId: 7, + id: 15, + }, + { type: 3, textContent: '\t\n', rootId: 7, id: 17 }, + ], + rootId: 7, + id: 11, + }, + ], + rootId: 7, + id: 9, + }, + ], + id: 7, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 500, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'iframe', + attributes: { id: 'two' }, + childNodes: [], + id: 38, + }, + }, + ], + }, + timestamp: now + 1000, + }, + // add iframe two + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 38, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 1, + name: 'html', + publicId: '', + systemId: '', + rootId: 39, + id: 40, + }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', rootId: 39, id: 43 }, + { + type: 2, + tagName: 'meta', + attributes: { charset: 'UTF-8' }, + childNodes: [], + rootId: 39, + id: 44, + }, + { type: 3, textContent: '\n ', rootId: 39, id: 45 }, + { + type: 2, + tagName: 'meta', + attributes: { + name: 'viewport', + content: 'width=device-width, initial-scale=1.0', + }, + childNodes: [], + rootId: 39, + id: 46, + }, + { type: 3, textContent: '\n ', rootId: 39, id: 47 }, + { + type: 2, + tagName: 'title', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'iframe 2', + rootId: 39, + id: 49, + }, + ], + rootId: 39, + id: 48, + }, + { type: 3, textContent: '\n ', rootId: 39, id: 50 }, + ], + rootId: 39, + id: 42, + }, + { type: 3, textContent: '\n ', rootId: 39, id: 51 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: '\n iframe 2\n ', + rootId: 39, + id: 53, + }, + { + type: 2, + tagName: 'iframe', + attributes: { id: 'three', frameborder: '0' }, + childNodes: [], + rootId: 39, + id: 54, + }, + { type: 3, textContent: '\n ', rootId: 39, id: 55 }, + { + type: 2, + tagName: 'iframe', + attributes: { id: 'four', frameborder: '0' }, + childNodes: [], + rootId: 39, + id: 56, + }, + { type: 3, textContent: '\n \n\n', rootId: 39, id: 57 }, + ], + rootId: 39, + id: 52, + }, + ], + rootId: 39, + id: 41, + }, + ], + id: 39, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 1000, + }, + // add iframe three + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 54, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + rootId: 58, + id: 60, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + rootId: 58, + id: 61, + }, + ], + rootId: 58, + id: 59, + }, + ], + id: 58, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 1000, + }, + // add iframe four + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 56, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 1, + name: 'html', + publicId: '', + systemId: '', + rootId: 62, + id: 63, + }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', rootId: 62, id: 66 }, + { + type: 2, + tagName: 'meta', + attributes: { charset: 'UTF-8' }, + childNodes: [], + rootId: 62, + id: 67, + }, + { type: 3, textContent: '\n ', rootId: 62, id: 68 }, + { + type: 2, + tagName: 'meta', + attributes: { + name: 'viewport', + content: 'width=device-width, initial-scale=1.0', + }, + childNodes: [], + rootId: 62, + id: 69, + }, + { type: 3, textContent: '\n ', rootId: 62, id: 70 }, + { + type: 2, + tagName: 'title', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'iframe 4', + rootId: 62, + id: 72, + }, + ], + rootId: 62, + id: 71, + }, + { type: 3, textContent: '\n ', rootId: 62, id: 73 }, + ], + rootId: 62, + id: 65, + }, + { type: 3, textContent: '\n ', rootId: 62, id: 74 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: '\n iframe 4\n \n ', + rootId: 62, + id: 76, + }, + { + type: 2, + tagName: 'script', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'SCRIPT_PLACEHOLDER', + rootId: 62, + id: 78, + }, + ], + rootId: 62, + id: 77, + }, + { type: 3, textContent: '\n\n', rootId: 62, id: 79 }, + ], + rootId: 62, + id: 75, + }, + ], + rootId: 62, + id: 64, + }, + ], + id: 62, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 1500, + }, + // add iframe five + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 75, + nextId: null, + node: { + type: 2, + tagName: 'iframe', + attributes: { id: 'five' }, + childNodes: [], + rootId: 62, + id: 80, + }, + }, + ], + }, + timestamp: now + 2000, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 80, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + rootId: 81, + id: 83, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'script', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'SCRIPT_PLACEHOLDER', + rootId: 81, + id: 86, + }, + ], + rootId: 81, + id: 85, + }, + ], + rootId: 81, + id: 84, + }, + ], + rootId: 81, + id: 82, + }, + ], + id: 81, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 2000, + }, + // remove the html element of iframe four + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [{ parentId: 62, id: 64 }], + adds: [], + }, + timestamp: now + 2500, + }, +]; + +export default events; diff --git a/packages/rrweb/test/events/input.ts b/packages/rrweb/test/events/input.ts new file mode 100644 index 00000000..3820f965 --- /dev/null +++ b/packages/rrweb/test/events/input.ts @@ -0,0 +1,215 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 100, + }, + // full snapshot: + { + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + }, + { + id: 5, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + type: EventType.FullSnapshot, + timestamp: now + 100, + }, + // mutation that adds select elements + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'select', + childNodes: [], + id: 26, + }, + }, + { + parentId: 26, + nextId: null, + node: { + type: 2, + tagName: 'option', + attributes: { value: 'valueC' }, + childNodes: [], + id: 27, + }, + }, + { + parentId: 27, + nextId: null, + node: { type: 3, textContent: 'C', id: 28 }, + }, + { + parentId: 26, + nextId: 27, + node: { + type: 2, + tagName: 'option', + attributes: { value: 'valueB', selected: true }, + childNodes: [], + id: 29, + }, + }, + { + parentId: 26, + nextId: 29, + node: { + type: 2, + tagName: 'option', + attributes: { value: 'valueA' }, + childNodes: [], + id: 30, + }, + }, + { + parentId: 30, + nextId: null, + node: { type: 3, textContent: 'A', id: 31 }, + }, + { + parentId: 29, + nextId: null, + node: { type: 3, textContent: 'B', id: 32 }, + }, + ], + }, + timestamp: now + 1000, + }, + // input event + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Input, + text: 'valueA', + isChecked: false, + id: 26, + }, + timestamp: now + 1500, + }, + // input event + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Input, + text: 'valueC', + isChecked: false, + id: 26, + }, + timestamp: now + 2000, + }, + // mutation that adds an input element + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'input', + attributes: {}, + childNodes: [], + id: 33, + }, + }, + ], + }, + timestamp: now + 2500, + }, + // an input event + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Input, + text: 'test input', + isChecked: false, + id: 33, + }, + timestamp: now + 3000, + }, + // remove the select element + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [{ parentId: 5, id: 26 }], + adds: [], + }, + timestamp: now + 3500, + }, + // remove the input element + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [{ parentId: 5, id: 33 }], + adds: [], + }, + timestamp: now + 4000, + }, +]; + +export default events; diff --git a/packages/rrweb/test/events/ordering.ts b/packages/rrweb/test/events/ordering.ts new file mode 100644 index 00000000..083c6870 --- /dev/null +++ b/packages/rrweb/test/events/ordering.ts @@ -0,0 +1,123 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 10, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 10, + }, + // full snapshot: + { + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + }, + { + id: 100, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + id: 101, + type: 2, + tagName: 'span', + attributes: {}, + childNodes: [ + { + id: 102, + type: 3, + textContent: 'Initial', + }, + ], + }, + ], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + type: EventType.FullSnapshot, + timestamp: now + 20, + }, + // 1st mutation that modifies text content + { + data: { + adds: [], + texts: [ + { + id: 102, + value: 'Intermediate - incorrect', + } + ], + source: IncrementalSource.Mutation, + removes: [], + attributes: [], + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 30, + }, + // 2nd mutation (with same timestamp) that modifies text content + { + data: { + adds: [], + texts: [ + { + id: 102, + value: 'Final - correct', + } + ], + source: IncrementalSource.Mutation, + removes: [], + attributes: [], + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 30, + }, + // dummy - presence triggers a bug + { + data: { + adds: [], + texts: [], + source: IncrementalSource.Mutation, + removes: [], + attributes: [], + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 35, + }, +]; + +export default events; diff --git a/packages/rrweb/test/events/scroll.ts b/packages/rrweb/test/events/scroll.ts new file mode 100644 index 00000000..5069b5fe --- /dev/null +++ b/packages/rrweb/test/events/scroll.ts @@ -0,0 +1,128 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1200, + height: 500, + }, + timestamp: now + 100, + }, + // full snapshot: + { + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + }, + { + id: 5, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + type: EventType.FullSnapshot, + timestamp: now + 100, + }, + // mutation that adds two div elements + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'div', + attributes: { + id: 'container', + style: 'height: 1000px; overflow: scroll;', + }, + childNodes: [], + id: 6, + }, + }, + { + parentId: 6, + nextId: null, + node: { + type: 2, + tagName: 'div', + attributes: { + id: 'block', + style: 'height: 10000px; background-color: yellow;', + }, + childNodes: [], + id: 7, + }, + }, + ], + }, + timestamp: now + 500, + }, + // scroll event on the "#container" div + { + type: EventType.IncrementalSnapshot, + data: { source: IncrementalSource.Scroll, id: 6, x: 0, y: 2500 }, + timestamp: now + 1000, + }, + // scroll event on document + { + type: EventType.IncrementalSnapshot, + data: { source: IncrementalSource.Scroll, id: 1, x: 0, y: 250 }, + timestamp: now + 1500, + }, + // remove the "#container" div + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [{ parentId: 5, id: 6 }], + adds: [], + }, + timestamp: now + 2000, + }, +]; + +export default events; diff --git a/packages/rrweb/test/events/shadow-dom.ts b/packages/rrweb/test/events/shadow-dom.ts new file mode 100644 index 00000000..88956a83 --- /dev/null +++ b/packages/rrweb/test/events/shadow-dom.ts @@ -0,0 +1,172 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); + +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1200, + height: 500, + }, + timestamp: now + 100, + }, + { + type: EventType.FullSnapshot, + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + }, + { + id: 5, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + timestamp: now + 200, + }, + // add shadow dom elements + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [], + id: 6, + isShadowHost: true, + }, + }, + ], + }, + timestamp: now + 500, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 6, + nextId: null, + node: { + type: 2, + tagName: 'span', + attributes: {}, + childNodes: [], + id: 7, + isShadow: true, + }, + }, + { + parentId: 7, + nextId: null, + node: { type: 3, textContent: 'shadow dom one', id: 8 }, + }, + ], + }, + timestamp: now + 500, + }, + // add nested shadow dom elements + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 6, + nextId: null, + node: { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [], + id: 9, + isShadow: true, + isShadowHost: true, + }, + }, + ], + }, + timestamp: now + 1000, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 9, + nextId: null, + node: { + type: 2, + tagName: 'span', + attributes: {}, + childNodes: [], + id: 10, + isShadow: true, + }, + }, + { + parentId: 10, + nextId: null, + node: { type: 3, textContent: 'shadow dom two', id: 11 }, + }, + ], + }, + timestamp: now + 1000, + }, +]; + +export default events; diff --git a/packages/rrweb/test/events/style-sheet-rule-events.ts b/packages/rrweb/test/events/style-sheet-rule-events.ts new file mode 100644 index 00000000..19a80bbd --- /dev/null +++ b/packages/rrweb/test/events/style-sheet-rule-events.ts @@ -0,0 +1,221 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 100, + }, + // full snapshot: + { + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { + id: 101, + type: 2, + tagName: 'style', + attributes: { + 'data-meta': 'from full-snapshot, gets rule added at 500', + }, + childNodes: [ + { + id: 102, + type: 3, + isStyle: true, + textContent: + '\n.css-added-at-200-overwritten-at-3000 {\n opacity: 1;\n transform: translateX(0);\n}\n', + }, + ], + }, + { + id: 105, + type: 2, + tagName: 'style', + attributes: { + _cssText: + '.css-added-at-200 { position: fixed; top: 0px; right: 0px; left: 4rem; z-index: 15; flex-shrink: 0; height: 0.25rem; overflow: hidden; background-color: rgb(17, 171, 209); }.css-added-at-200.alt { height: 0.25rem; background-color: rgb(190, 232, 242); opacity: 0; transition: opacity 0.5s ease 0s; }.css-added-at-200.alt2 { padding-left: 4rem; }', + 'data-emotion': 'css', + }, + childNodes: [ + { id: 106, type: 3, isStyle: true, textContent: '' }, + ], + }, + ], + }, + { + id: 107, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + id: 108, + type: 2, + tagName: 'a', + attributes: { + class: 'css-added-at-1000-deleted-at-2500', + }, + childNodes: [ + { + id: 109, + type: 3, + textContent: 'string', + }, + ], + }, + ], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + type: EventType.FullSnapshot, + timestamp: now + 100, + }, + // mutation that adds stylesheet + { + data: { + adds: [ + { + node: { + id: 255, + type: 2, + tagName: 'style', + attributes: { 'data-jss': '', 'data-meta': 'Col, Themed, Dynamic' }, + childNodes: [], + }, + nextId: 101, + parentId: 4, + }, + { + node: { + id: 256, + type: 3, + isStyle: true, + textContent: + '\n.css-added-at-400 {\n padding: 1.3125rem;\n flex: none;\n width: 100%;\n}\n', + }, + nextId: null, + parentId: 255, + }, + ], + texts: [], + source: IncrementalSource.Mutation, + removes: [], + attributes: [], + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 400, + }, + // mutation that adds style rule to existing stylesheet + { + data: { + id: 101, + adds: [ + { + rule: + '.css-added-at-500-overwritten-at-3000 {border: 1px solid blue;}', + index: 1, + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 500, + }, + // adds StyleSheetRule + { + data: { + id: 105, + adds: [ + { + rule: + '.css-added-at-1000-deleted-at-2500{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;min-width:60rem;min-height:100vh;color:blue;}', + index: 2, + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 1000, + }, + { + data: { + id: 105, + removes: [ + { + index: 2, + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 2500, + }, + // overwrite all contents of stylesheet + { + data: { + texts: [ + { + id: 102, + value: '.all-css-overwritten-at-3000 { color: indigo; }', + }, + ], + attributes: [], + removes: [], + adds: [], + source: IncrementalSource.Mutation, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 3000, + }, + { + data: { + id: 101, + adds: [ + { + rule: '.css-added-at-3100{color:blue;}', + index: 1, + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 3100, + }, +]; + +export default events; diff --git a/packages/rrweb/test/events/style-sheet-text-mutation.ts b/packages/rrweb/test/events/style-sheet-text-mutation.ts new file mode 100644 index 00000000..c795f6be --- /dev/null +++ b/packages/rrweb/test/events/style-sheet-text-mutation.ts @@ -0,0 +1,178 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 100, + }, + // full snapshot + { + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { + id: 101, + type: 2, + tagName: 'style', + attributes: {}, + childNodes: [ + { + id: 102, + type: 3, + isStyle: true, + textContent: '\n.css-added-at-100 {color: yellow;}\n', + }, + ], + }, + ], + }, + { + id: 107, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + type: EventType.FullSnapshot, + timestamp: now + 100, + }, + // mutation that adds an element + { + data: { + adds: [ + { + node: { + id: 108, + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [], + }, + nextId: null, + parentId: 107, + }, + ], + texts: [], + source: IncrementalSource.Mutation, + removes: [], + attributes: [], + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 500, + }, + // adds a StyleSheetRule by inserting + { + data: { + id: 101, + adds: [ + { + rule: '.css-added-at-1000-overwritten-at-1500 {color:red;}', + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 1000, + }, + // adds a StyleSheetRule by adding a text + { + data: { + adds: [ + { + node: { + type: 3, + textContent: '.css-added-at-1500-deleted-at-2500 {color: yellow;}', + id: 109, + }, + nextId: null, + parentId: 101, + }, + ], + texts: [], + source: IncrementalSource.Mutation, + removes: [], + attributes: [], + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 1500, + }, + // adds a StyleSheetRule by inserting + { + data: { + id: 101, + adds: [ + { + rule: '.css-added-at-2000-overwritten-at-2500 {color: blue;}', + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 2000, + }, + // deletes a StyleSheetRule by removing the text + { + data: { + texts: [], + attributes: [], + removes: [{ parentId: 101, id: 109 }], + adds: [], + source: IncrementalSource.Mutation, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 2500, + }, + // adds a StyleSheetRule by inserting + { + data: { + id: 101, + adds: [ + { + rule: '.css-added-at-3000 {color: red;}', + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 3000, + }, +]; + +export default events; diff --git a/packages/rrweb/test/events/webgl.ts b/packages/rrweb/test/events/webgl.ts new file mode 100644 index 00000000..513c1ade --- /dev/null +++ b/packages/rrweb/test/events/webgl.ts @@ -0,0 +1,118 @@ +export default [ + { + type: 4, + data: { + href: '', + width: 1600, + height: 900, + }, + timestamp: 1636379531385, + }, + { + type: 2, + data: { + node: { + 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: 'canvas', id: 11 }], + id: 10, + }, + { type: 3, textContent: '\n ', id: 12 }, + ], + id: 4, + }, + { type: 3, textContent: '\n ', id: 13 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', id: 15 }, + { + type: 2, + tagName: 'canvas', + attributes: { + id: 'myCanvas', + width: '200', + height: '100', + style: 'border: 1px solid #000000', + }, + childNodes: [{ type: 3, textContent: '\n ', id: 17 }], + id: 16, + }, + { type: 3, textContent: '\n ', id: 18 }, + { + type: 2, + tagName: 'script', + attributes: {}, + childNodes: [ + { type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 20 }, + ], + id: 19, + }, + { type: 3, textContent: '\n \n\n', id: 21 }, + ], + id: 14, + }, + ], + id: 3, + }, + ], + id: 1, + }, + initialOffset: { left: 0, top: 0 }, + }, + timestamp: 1636379531389, + }, + { + type: 3, + data: { + source: 9, + id: 16, + type: 1, + property: 'clearColor', + args: [1, 0, 0, 1], + }, + timestamp: 1636379532355, + }, + { + type: 3, + data: { source: 9, id: 16, type: 1, property: 'clear', args: [16384] }, + timestamp: 1636379532356, + }, +]; diff --git a/packages/rrweb/test/html/assets/robot.png b/packages/rrweb/test/html/assets/robot.png new file mode 100644 index 0000000000000000000000000000000000000000..cc486cc8b70680b0dfd72828b6530e0a4e9183ad GIT binary patch literal 11004 zcmVMG57cTg)be7&LF-a&Iq(N9eY0ckxvbk2cx7z2w0ZIEpA(uQbq_V zgpyh-t=ipA&dZOK*Vj7>&F&&&7QkiMEq$Y4yw)0&#%Xl*rW^mo2S33m7?6cr==-j1 zb4saYao4dBQE8KABF%(lS=}V!j9xXiCm-}FEeIi(^#E@alGhMpjAOYUe)q4~mPILW z#xtde!O-Fqpxut7Q~;pSAfn7QkB|3Sp`tS z>qYtIXiyw$;%0_Z-%)t${gEON)tu^KuKlL5P&#I<2WS%TI(oIwAPsrNt#AccVS_n zR-H#QanvCg5<<|x>sIEQVXkCZ3rH!YKn#FmSxK5DX$AmNX-a7i03s@-;v}h7=Y>cJ z0ijb=oDgz3)xecCR|3Qs<2qh3ECGPUIS@iAwQSyvQ$*BSAsWUgrPSh_QA#LDlcd{i z10n)2N}3Cc6c}Dc(Q}1;l7KNf=NEE*h=7Qwl_rFAI&qq2T4|$^QNkGUUDt7JN+e0HYNSjE?waJChM1uU{W;Qh`1Lg z9HcQilc^sBo?|(#ZQEA8z7&Q5W&GUKtmFC{H*AYqb)zK#5NVV~4H37s7Jz8G7IpRI zwLSo@w7K@$0Xfpcmvb zsg%~OC=t>iAzEwK=fn|c-hh*WD+>|!c6sF{Q7Tg z+OqMb0|&KM8#isKH`)u;TAFl?)>3OfR{+3vbBTaTqhxTrKR0UiHlLUO3|Btz5C8*9 zSUW#IO%3dO?%DdnRI6DZ9b4u2q33#9%Y3nrDX3NF2&kl+I<7Ogp)5r5h9HDD=fKBe zcC%BTS(th8M+dIHX6NsG@>4+&e)qc%ojrd}ir5&HC2_l1%d%8y4O(v8wSfi(_3Ph2 z{)Aj{kd)9Qi(JR^Ah&w;#_^$Xq0|k#Z@%~5_jJ3_;bX_+R5Tj3c56w=EN!)h*9`Y> z=@W^d^bLY|MM3r$0dX3wU$bRJZr$vmsfC%De4!}PwB6}cD&;%xyldj@nR62-*RNhz zE|ud}yE<20v2IOR$stj%zXSOXTceYM7fBe+GItyfp44iUR$d4z2ly@f97+4B!o`Wm{7W~uz2tN53FCmZQ|frNBZ36Zxk`& z=LSg#rIb<1C|&+r0z@hymB>WWj#`ZI_uT#q|LRvhYa;WRPyZeOXstJF*~V?_$blCq z<>kKeQnO*Z-tBk2Ey(8yL25K5&}z4f<;tGhZoBrH>z0nx^H$lwWi}fsbGoHfnxxA&pYYvbJ4)&DzbIo`33b zB~=_nW807!e3FPTPgjhM&dx6isU6F*IC<-xZ##1MK)FyncIs&V>fy!3S&Lu! zMi!+E(PSbKN`_v3&Dh43L#qbMLzO~bCEph%@i%_(-+uF#e!J7^2EJb|RgxsCFV!lQ zzNvHPrB-j*dsm!v_doqqqgLY|+DIw2RwvG! zb%h_;PGxmDj^g>nIjyy2Sy2?-c+*YSU4Q-a&peSUl#NoJ?};pxDw&&^q9FI~y}QwB zFU&8*NxEv)$}krY#55C9s+(@T{r~>2e|+xz8A{y9N(VLsSt{ve^mzWe(UlOAiIg(y zrX9C_=>CuV(mnsn4V$m(E0!oDX_g9^8Dap*L*IM2RPb-V?RJAE7Y0EPAb>$kv&8qq z```WEBuO5B>`~jblPJ!Tbmi)`<-UsVx?8sG48y?p{cfkzX}A0!EEV&VF+xylER_1c z|Lwm$aq1ZAAaqN9iD{#ZL4em2aeq2ULTDzk_2ZjA_v@d1?``i`Jvi!FOca9ngedHJKfB!(GugBnUUAwK z_w3re=lr?zStk9!cReplv$JPUGj1`?ils7u86F)!di2PFr=KFO+ieJ&6dd37LW?le z$^ia+E%l!cl2RgM_G|C^jWq+~&mBH=^!UlyYOPr53miLD3K0z&&-1_a;Dha@#Sgys z1B=ZzXN*#65RKNpZ54gDzf=eVUmLw)^VWOc^`7;cHpOwY>y}&MBq>+QjvKUEEs+UmF=8Us$Zhai>*l#!b?m@2EuQ`|`}8 z2&Tt-`B^mk(`jdj#&+Cw-j?rweloC=VS(aoX z&V)oX2trERaTez0{*Qn9vBibC(a{xQm={9Ss*5|Xxqj>RZ8z?^pTwzL({?zT+t3VP$lQi16 zdfSe5JLc;1-MHgJ=!#%yeBBmJe1eCR`wR2E~(Am5I?rdjbjOa(PNblru^xWt=gKJGSlEHf1c!gj5O;5K##^U!5OW zF}hHldujhuhY!7Q_RQ(I`I&mXRw?)Gefv9396!=*)%Ne(chCLr;kKP7$@uu%iE|V4 z3$uVG$Q7IO)5ZQl&Mn{1_4W4`^CcI`Ij@*uOX`dg_ESI-AjW9l4L|vjPgjB<(Q2?* zzT>((YMtf}zVqEBxxCB- zsN||^clVV`#bU8oDoswFxiEE#v!K7Qx+CV5d6__Z5hS5$mcHZG`**C_s1XUFJ?e}> z+_n4WBPY&QYxU6M>o#mYedgSb9XmH~*~~d-JvM_uPKW`t80Cx+N(`dXMr#wr38mDv z?M#R)6+Imasg%-$kcIjAVj=JO!ImxCc3yMi-0aNcxihwFA3pHX<}Fv<|DFe`iwjrn z*m>t&Z%;ERr9wl3z#ZuCFBVEj>B7{R>u=gqES3g`h7Z25@0(xvPZLKE9em+OOZ8@D ztYWCRR4y$0s6k^{_UPaW0Ky0X00fZQP)eUabalmY-H zu`wDE5Gep7#OBO(tSl2~kWwm@avjI9Ev>aOMjKo> ze&}P4Ye!N17hm|3R$Qa>{aE*h{DcfFLpeq0OolV;eVYlu}NdJ%dJj zo)>q!QT*}|1tA0o2!pZGLTiGE2HTyOb5<?Uj|7CF~)d~U&s{@Km*|b4k0dQ zQmN5WY0LUGKX~|?M_$^`7`1InNR_3j)+QCQXO@Ua2@pyZf-oqQ=A8MSqqPA5+qQcI z70Q?~Af(QujMG%4!sa{(eb;p?#(PbZT{rLf+~@zxj%%;}o3DN;jWbFqV@!(7_dUn4 z8Kb!%%;j=P99?t$?lY&3H|mS))~@=@=l|X6wd>MUG&`M~)i)5XR7PG(nU_)uQpQ}{ zd0CSH(ik8_01S)S(Kzv zoWx0rXc%XV(H;RbNwaP@CX`yXO@ZWcq3zm)P|LDKniNarPyFtuvn)Gu_#h(szMmwK zZ86t%EZZW43=Iwho;Nf+HZVL=2&{YWxhGB2PP?rQYGo#-CKg+>oLQIlq01n((V?FU z{Qvytgdk#; ziEcO6+PJO*0NP-d$uv!+6y-25Xn+vT83OivSP1F+;r;J^|LM~w(j@EU+$`w^p6l4Q zWpmE0p~1n4iPP_S-!E+6zSU?=7&C~7P+w{vo7$hMCIxn>nq4+mrPW}0Ftja=*u#wg z2t-JY2tbHq+n(ofZW)AbxBbkMk1y5g%}$g^g$AWiTInRqx=Gp*GEPu2Xa@M1zP*X>E)#hB79lSh;dlK3{4!n`xTm^SO4bX& z22pFHwH8t(N!n<2s?{20jB!?vQk!!rWRhgP-KIeVLOAE#;;!SyQ55FFa=FAU9!G88 z^M#PcwhTgF|G>HFg@OKx)?kb=2-VusGe7=eZoGm_DI=v^&LWq5l9cmhxEPNN006N$ zV~k3vmDXtzTbvohB#AZZx!LI-{_y+H?R)OZR z94EbQ4}d9voz}; z7-}rlZHrr+F~%I*$^}8ElboBHaa~&}ZGcWrPQ3W|le`hG39o5PfNT4`{tyQM1SEu5 zmb07@01N`BL?I%gF-8gz#ZjqTQQE{&*J!P^>2|uEPMa|rN3m_!EY2C{w(Z!C!x$%& zFh-3*%9z$hNtvWsqtR-#+DR1Ijw6+{ZAu6MK&7-nBZbVQ)Jmh)#u!8}TDMy*tw!P` z?(6I0-0pUoz846os7)NLE0yx8iRr#_iBUobnV+34kLUXPOVg9n)tP4hnjruLAj{^8 zfc0h-E_&mo-a;>yq-h#=y96Lfvp7irz!-zttX{L$5BwkqIcGqK6hbK_gdiY|F-l1( zWhRxBDoZn^wU7#d-J_R;wbKP<1EY4Bn1GGB}&R}w=+LCOBq|YVS|z? zNs_^#;q&Ltu2@yDEuLh;_g$%s?*+$BO(%l@25ZW*-~G@fPC2} zdsdnWp)STF0)U8@NYQbUG?(g}kvJ7m904H|NZgHrFbKmiU&y4T2^NaPBD_8n~SMiH;v-6Zt$Ftz#snN2z zQcA0h1$B`xj+de@A^_=hI`eZE05MHP-0c!T0BCnw7mFU!+c6SK9LKQ{6CjjQ%d$AP zT-PH6j6noYO6S5LP7W9QDDS-q}gTUL~$wqv_OmWrigXU>nU7#kTG;Y4L3vu)S*na1oD zAQ2g5jdml=RN(MlI5NwIi-=HNTx>2ZdajqGnGmVc${=*wZLPI2XtXwH2qjt>DJ3Er zqdm`cY&(kMG!?$@NhvLhD{TP5vUw&9XEY20V-OJ>$MpiKwUSauk)=sotIpLHX0tSI zG}?y_ym;H5JEW9x6qicH$w}UAH(WOqQc9(Qz!Oq6n~kFf59$`DpvNTOstQ?=6nf4vq;tUZfrH*6w_w`LoP9s8XZoXVD6$<%Y zr{ZOw>}j$Sx0;>iXsKWTFUDAOZGL{P{{6puX!F%O!d$K!b)=LAi4Y==BOx+0%R(tZ zBoqke964i@5+M1UU+^48NThYM6B7VxQ{B~CYh#pUTbxq>V2t{{2Vj(xS}Q3s&ksk( zR#C>JkjIZ6Idl5d=FMA_lu;BH3I)!srKPIx7c!v?VlMQuOgOevU1}Bz1&cA)aoh1c z%`eB4Wld5_(=4eqYviIgA{q_=jDPxH{eE?B`pB~f4?h3=@bFN#+s@KRYm+2#k|c<| zL~MHP?MzAlAaGnaOH(wcq)L;x zw`m4|Bu#zxr$6`m4XREK4a>9vS%Y<3C`+ zwm56Hnpu_sKy`6p&ALrmD>P=gRzyStoG}8RmCEHq)bPk-KYsB4{nq|{&&4WFw&OdFQ!4kLJ9FyUXP&(M&bvEG zhX~zHcjfr1qlXXf*xAPz%YPP?XZ@vZqQc3`L-~I1@*L&ZKNEqk$zHRT~+EYn&m-JAS02!Lp^ER&Rd<*Q%$?C*basaC(V1Yv;s(huPIj*>xCO`-qSbSU#Sdk-@ffP z|H-dvV;qW(!<9B#Yh{Fy#^RJS&luf{JO)uKZP1`WrPW*CddHqUZ;7IAbz!kuU945B ztyW{|{P}vdy12M#IZTLTes*!Gx}^Q4=6?TcTiEzZAn?>nTFN-JYDC4^ZP5R^hc z^7r2v9a}jtFx=}!3=EH!%6(@~pIo_mL!8tNnk-sf5|7&;`V%Prb_0Eg>u$b0EoaCMMTr9dl3l$qR~nt$cr@wP(lLF z4SjE*zvBDO$3FH?e)PyAoUrBYtIX6;x1+t-dA zIr853zrWp$J=ddzW?2?>y90wGM-INUaqG_B(F zLO`LUiml(UDen{uZ@5uOO&JqXBEl`V-!(Nm zQyN%l<)qCSA;kCngD*YjI991|AW34S6%sPHF#qgRKO%&jJbrl1Eh}Yj3l6Uq0$_1_ zetPoGx7;x>Jbdo_1xji2dH6J9ld>y`VTdUl&=DBw4!~r%85Uc5d?A z`LBQDZyK${^Biv5xqRX5@q`!QtXl;bb zcHOjlaA;U+%@_qj(DYz|Ha%nP%|c$hKd1NJpg{xJt9UP7tJ7n~^g6SbtOYK8NYI$I z8@F!QyuH)ue)-E^xG;4d4ZisN(?<_Jcl(|9`9Ua@j^m`+YR=Bh?tA>l=g*$ne(R01 zbFKD5!w>zrsY&MN>aEsLf8XtU_OeO^wcNOM>%^%u`Juel>Q%+SD}g@n)?e7U?V5JC z%?ay;5CA}9bni_Mu`y;@gRj8($~%LWTf3z9by7l*EaT&xGtPTo86dPP@^$*QYpD*Ua+uw3)rGM~0{n5WJjug_1j9LB)bVMmx&i9x3;DG}#I<9A17Gu2E zdNx{@<-iFg?rHwH<%hGRt^-G;byBB8}rIzgkVQ%ZyJEmvO z42_MRK7Fdw=|oXiDCN0MF<;IVijM060Hqe^*2?h}FCExl8Z12U%O702P$jV*8((?! z#OWP7uQ_&p_VFj5%=ZOe$%(t&mx*z&CPolrl<(%&_HQYBmB0>c<|ru%p^US7y(VO4 zl+rz8MKov-kpOCgXbhz^%LL;L!APw|nx;u2mCUkKWSJ1D$TBJA>eXx4uit2KO9+vr z=~BIRVd{K0YImZ}*%PO1%iVL=+Xse6YV}$ehMY3jwo8@%a%I4Aea86t>FI~Q@t2ic z@aw<%d#yxtyX`a+2lnqfarDK-YL%kC;oZ9gw#h4KgFoSu$S9Aq?t!VNDdjHrLMQL> zz@Z*B)MgGf6cuW%l~h8c1`Q$@L`s>~s?n(B`ihf>&s66ZI?blm(hqF{&Gr4m{s!bQocbkWPEaXw~g@azPlUsxvh)Jv~`0l!}$X z$G-c`i6aNfyLSD>Uw`S;>Eo0DB`ixiNvAcwX?@??oFZLf=qp)1KN)xskP^l?LqwsH znv8mdA|Z?t$_QhGqXCEeW4V<^sVIt;7G|4HBM1w}4;;SXAMKv}{+a1h=e@vPzh$Fk z+W^2QWt<}-XMB#$C24x{)UgxC4{y8b+KubCc&?|kO4D?3Xt-D?X%AVy^Xzw@Sify+ zxm?jk6QGo`IPSJujfu&#XHOonIPa$E%;9G(&VbVT+^iQCuf1cJH%zlEG5>(P{#vBo z9>0MRMtfI102rgSkrFeYW_|zWk--tpS(aoCsHRb>V%5K)Z}#|1+=%Xe=e2eT%wfqlQl0U&vB!m#4 zfWU}l4EVn9#=hfv9Ie)T#||yRC}nKh_8r@{Ut6s%aPBb9(=<^+_(7gf!Y!Lpk_&RJ zAXHk-%uc3hylQ-{?*~dL*Y^v9l|%dY_d@#2JYR&Cgz zwPBV+2(#^4p+CRk>NSeSN~lYb=H~H*7Rp7U#Jz7eb*L)q}pkXq_wM zuiv)YbzO^FewYJ9Av4!={UFRzVcV`z3IHr_5keJ_wfdqEVq|26W!bIP(u$$AkOCqy z!jNE?%W=jg&YUi-FRi?3%w$Li>%5>(s9wJc6v@vO#WFk{WyPhAl+mtca3z9en0LFNd#gkL# zEN*YwuxaPDH_o4)ItB*-01?AnP8-7<*QTz>QYADH06;>hGzlg^mI)58^~gUziIe~$ z`cBTa9L_nRyih8gKXAIa&DCwL13xJD4=v2jZ{K;1=lfb~04Nj+QX69+6Qai{p@dSx2@nF55=wrC z@sZavGJ${;D5H!rP6+XX&}bFKDK!?Sb~#sR%{FR{s#cP5?s=i@*xa@mw^ET2!h}ep zZYNEWW}}{^DFF<_f*np8*Fvd8yY-YJg8DSiOHX3z6Xt!HouHbqhvt7b$g9Hcz!je>U qTg~2LrAnpQZV>`Mi|_sS `${i + 1}: ${l}`) + .join('\n'), + ); + gl.deleteShader(shader); + return null; + } + + return shader; + } + + /** + * Creates a program, attaches shaders, binds attrib locations, links the + * program and calls useProgram. + * @param {WebGLShader[]} shaders The shaders to attach + * @param {string[]} [opt_attribs] An array of attribs names. Locations will be assigned by index if not passed in + * @param {number[]} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. By default it just prints an error to the console + * on error. If you want something else pass an callback. It's passed an error message. + * @memberOf module:webgl-utils + */ + function createProgram( + gl, + shaders, + opt_attribs, + opt_locations, + opt_errorCallback, + ) { + const errFn = opt_errorCallback || error; + const program = gl.createProgram(); + shaders.forEach(function (shader) { + gl.attachShader(program, shader); + }); + if (opt_attribs) { + opt_attribs.forEach(function (attrib, ndx) { + gl.bindAttribLocation( + program, + opt_locations ? opt_locations[ndx] : ndx, + attrib, + ); + }); + } + gl.linkProgram(program); + + // Check the link status + const linked = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!linked) { + // something went wrong with the link + const lastError = gl.getProgramInfoLog(program); + errFn('Error in program linking:' + lastError); + + gl.deleteProgram(program); + return null; + } + return program; + } + + /** + * Loads a shader from a script tag. + * @param {WebGLRenderingContext} gl The WebGLRenderingContext to use. + * @param {string} scriptId The id of the script tag. + * @param {number} opt_shaderType The type of shader. If not passed in it will + * be derived from the type of the script tag. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. + * @return {WebGLShader} The created shader. + */ + function createShaderFromScript( + gl, + scriptId, + opt_shaderType, + opt_errorCallback, + ) { + let shaderSource = ''; + let shaderType; + const shaderScript = document.getElementById(scriptId); + if (!shaderScript) { + throw '*** Error: unknown script element' + scriptId; + } + shaderSource = shaderScript.text; + + if (!opt_shaderType) { + if (shaderScript.type === 'x-shader/x-vertex') { + shaderType = gl.VERTEX_SHADER; + } else if (shaderScript.type === 'x-shader/x-fragment') { + shaderType = gl.FRAGMENT_SHADER; + } else if ( + shaderType !== gl.VERTEX_SHADER && + shaderType !== gl.FRAGMENT_SHADER + ) { + throw '*** Error: unknown shader type'; + } + } + + return loadShader( + gl, + shaderSource, + opt_shaderType ? opt_shaderType : shaderType, + opt_errorCallback, + ); + } + + const defaultShaderType = ['VERTEX_SHADER', 'FRAGMENT_SHADER']; + + /** + * Creates a program from 2 script tags. + * + * @param {WebGLRenderingContext} gl The WebGLRenderingContext + * to use. + * @param {string[]} shaderScriptIds Array of ids of the script + * tags for the shaders. The first is assumed to be the + * vertex shader, the second the fragment shader. + * @param {string[]} [opt_attribs] An array of attribs names. Locations will be assigned by index if not passed in + * @param {number[]} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. By default it just prints an error to the console + * on error. If you want something else pass an callback. It's passed an error message. + * @return {WebGLProgram} The created program. + * @memberOf module:webgl-utils + */ + function createProgramFromScripts( + gl, + shaderScriptIds, + opt_attribs, + opt_locations, + opt_errorCallback, + ) { + const shaders = []; + for (let ii = 0; ii < shaderScriptIds.length; ++ii) { + shaders.push( + createShaderFromScript( + gl, + shaderScriptIds[ii], + gl[defaultShaderType[ii]], + opt_errorCallback, + ), + ); + } + return createProgram( + gl, + shaders, + opt_attribs, + opt_locations, + opt_errorCallback, + ); + } + + /** + * Creates a program from 2 sources. + * + * @param {WebGLRenderingContext} gl The WebGLRenderingContext + * to use. + * @param {string[]} shaderSourcess Array of sources for the + * shaders. The first is assumed to be the vertex shader, + * the second the fragment shader. + * @param {string[]} [opt_attribs] An array of attribs names. Locations will be assigned by index if not passed in + * @param {number[]} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. By default it just prints an error to the console + * on error. If you want something else pass an callback. It's passed an error message. + * @return {WebGLProgram} The created program. + * @memberOf module:webgl-utils + */ + function createProgramFromSources( + gl, + shaderSources, + opt_attribs, + opt_locations, + opt_errorCallback, + ) { + const shaders = []; + for (let ii = 0; ii < shaderSources.length; ++ii) { + shaders.push( + loadShader( + gl, + shaderSources[ii], + gl[defaultShaderType[ii]], + opt_errorCallback, + ), + ); + } + return createProgram( + gl, + shaders, + opt_attribs, + opt_locations, + opt_errorCallback, + ); + } + + /** + * Returns the corresponding bind point for a given sampler type + */ + function getBindPointForSamplerType(gl, type) { + if (type === gl.SAMPLER_2D) return gl.TEXTURE_2D; // eslint-disable-line + if (type === gl.SAMPLER_CUBE) return gl.TEXTURE_CUBE_MAP; // eslint-disable-line + return undefined; + } + + /** + * @typedef {Object.} Setters + */ + + /** + * Creates setter functions for all uniforms of a shader + * program. + * + * @see {@link module:webgl-utils.setUniforms} + * + * @param {WebGLProgram} program the program to create setters for. + * @returns {Object.} an object with a setter by name for each uniform + * @memberOf module:webgl-utils + */ + function createUniformSetters(gl, program) { + let textureUnit = 0; + + /** + * Creates a setter for a uniform of the given program with it's + * location embedded in the setter. + * @param {WebGLProgram} program + * @param {WebGLUniformInfo} uniformInfo + * @returns {function} the created setter. + */ + function createUniformSetter(program, uniformInfo) { + const location = gl.getUniformLocation(program, uniformInfo.name); + const type = uniformInfo.type; + // Check if this uniform is an array + const isArray = + uniformInfo.size > 1 && uniformInfo.name.substr(-3) === '[0]'; + if (type === gl.FLOAT && isArray) { + return function (v) { + gl.uniform1fv(location, v); + }; + } + if (type === gl.FLOAT) { + return function (v) { + gl.uniform1f(location, v); + }; + } + if (type === gl.FLOAT_VEC2) { + return function (v) { + gl.uniform2fv(location, v); + }; + } + if (type === gl.FLOAT_VEC3) { + return function (v) { + gl.uniform3fv(location, v); + }; + } + if (type === gl.FLOAT_VEC4) { + return function (v) { + gl.uniform4fv(location, v); + }; + } + if (type === gl.INT && isArray) { + return function (v) { + gl.uniform1iv(location, v); + }; + } + if (type === gl.INT) { + return function (v) { + gl.uniform1i(location, v); + }; + } + if (type === gl.INT_VEC2) { + return function (v) { + gl.uniform2iv(location, v); + }; + } + if (type === gl.INT_VEC3) { + return function (v) { + gl.uniform3iv(location, v); + }; + } + if (type === gl.INT_VEC4) { + return function (v) { + gl.uniform4iv(location, v); + }; + } + if (type === gl.BOOL) { + return function (v) { + gl.uniform1iv(location, v); + }; + } + if (type === gl.BOOL_VEC2) { + return function (v) { + gl.uniform2iv(location, v); + }; + } + if (type === gl.BOOL_VEC3) { + return function (v) { + gl.uniform3iv(location, v); + }; + } + if (type === gl.BOOL_VEC4) { + return function (v) { + gl.uniform4iv(location, v); + }; + } + if (type === gl.FLOAT_MAT2) { + return function (v) { + gl.uniformMatrix2fv(location, false, v); + }; + } + if (type === gl.FLOAT_MAT3) { + return function (v) { + gl.uniformMatrix3fv(location, false, v); + }; + } + if (type === gl.FLOAT_MAT4) { + return function (v) { + gl.uniformMatrix4fv(location, false, v); + }; + } + if ((type === gl.SAMPLER_2D || type === gl.SAMPLER_CUBE) && isArray) { + const units = []; + for (let ii = 0; ii < info.size; ++ii) { + units.push(textureUnit++); + } + return (function (bindPoint, units) { + return function (textures) { + gl.uniform1iv(location, units); + textures.forEach(function (texture, index) { + gl.activeTexture(gl.TEXTURE0 + units[index]); + gl.bindTexture(bindPoint, texture); + }); + }; + })(getBindPointForSamplerType(gl, type), units); + } + if (type === gl.SAMPLER_2D || type === gl.SAMPLER_CUBE) { + return (function (bindPoint, unit) { + return function (texture) { + gl.uniform1i(location, unit); + gl.activeTexture(gl.TEXTURE0 + unit); + gl.bindTexture(bindPoint, texture); + }; + })(getBindPointForSamplerType(gl, type), textureUnit++); + } + throw 'unknown type: 0x' + type.toString(16); // we should never get here. + } + + const uniformSetters = {}; + const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); + + for (let ii = 0; ii < numUniforms; ++ii) { + const uniformInfo = gl.getActiveUniform(program, ii); + if (!uniformInfo) { + break; + } + let name = uniformInfo.name; + // remove the array suffix. + if (name.substr(-3) === '[0]') { + name = name.substr(0, name.length - 3); + } + const setter = createUniformSetter(program, uniformInfo); + uniformSetters[name] = setter; + } + return uniformSetters; + } + + /** + * Set uniforms and binds related textures. + * + * Example: + * + * let programInfo = createProgramInfo( + * gl, ["some-vs", "some-fs"]); + * + * let tex1 = gl.createTexture(); + * let tex2 = gl.createTexture(); + * + * ... assume we setup the textures with data ... + * + * let uniforms = { + * u_someSampler: tex1, + * u_someOtherSampler: tex2, + * u_someColor: [1,0,0,1], + * u_somePosition: [0,1,1], + * u_someMatrix: [ + * 1,0,0,0, + * 0,1,0,0, + * 0,0,1,0, + * 0,0,0,0, + * ], + * }; + * + * gl.useProgram(program); + * + * This will automatically bind the textures AND set the + * uniforms. + * + * setUniforms(programInfo.uniformSetters, uniforms); + * + * For the example above it is equivalent to + * + * let texUnit = 0; + * gl.activeTexture(gl.TEXTURE0 + texUnit); + * gl.bindTexture(gl.TEXTURE_2D, tex1); + * gl.uniform1i(u_someSamplerLocation, texUnit++); + * gl.activeTexture(gl.TEXTURE0 + texUnit); + * gl.bindTexture(gl.TEXTURE_2D, tex2); + * gl.uniform1i(u_someSamplerLocation, texUnit++); + * gl.uniform4fv(u_someColorLocation, [1, 0, 0, 1]); + * gl.uniform3fv(u_somePositionLocation, [0, 1, 1]); + * gl.uniformMatrix4fv(u_someMatrix, false, [ + * 1,0,0,0, + * 0,1,0,0, + * 0,0,1,0, + * 0,0,0,0, + * ]); + * + * Note it is perfectly reasonable to call `setUniforms` multiple times. For example + * + * let uniforms = { + * u_someSampler: tex1, + * u_someOtherSampler: tex2, + * }; + * + * let moreUniforms { + * u_someColor: [1,0,0,1], + * u_somePosition: [0,1,1], + * u_someMatrix: [ + * 1,0,0,0, + * 0,1,0,0, + * 0,0,1,0, + * 0,0,0,0, + * ], + * }; + * + * setUniforms(programInfo.uniformSetters, uniforms); + * setUniforms(programInfo.uniformSetters, moreUniforms); + * + * @param {Object.|module:webgl-utils.ProgramInfo} setters the setters returned from + * `createUniformSetters` or a ProgramInfo from {@link module:webgl-utils.createProgramInfo}. + * @param {Object.} an object with values for the + * uniforms. + * @memberOf module:webgl-utils + */ + function setUniforms(setters, ...values) { + setters = setters.uniformSetters || setters; + for (const uniforms of values) { + Object.keys(uniforms).forEach(function (name) { + const setter = setters[name]; + if (setter) { + setter(uniforms[name]); + } + }); + } + } + + /** + * Creates setter functions for all attributes of a shader + * program. You can pass this to {@link module:webgl-utils.setBuffersAndAttributes} to set all your buffers and attributes. + * + * @see {@link module:webgl-utils.setAttributes} for example + * @param {WebGLProgram} program the program to create setters for. + * @return {Object.} an object with a setter for each attribute by name. + * @memberOf module:webgl-utils + */ + function createAttributeSetters(gl, program) { + const attribSetters = {}; + + function createAttribSetter(index) { + return function (b) { + if (b.value) { + gl.disableVertexAttribArray(index); + switch (b.value.length) { + case 4: + gl.vertexAttrib4fv(index, b.value); + break; + case 3: + gl.vertexAttrib3fv(index, b.value); + break; + case 2: + gl.vertexAttrib2fv(index, b.value); + break; + case 1: + gl.vertexAttrib1fv(index, b.value); + break; + default: + throw new Error( + 'the length of a float constant value must be between 1 and 4!', + ); + } + } else { + gl.bindBuffer(gl.ARRAY_BUFFER, b.buffer); + gl.enableVertexAttribArray(index); + gl.vertexAttribPointer( + index, + b.numComponents || b.size, + b.type || gl.FLOAT, + b.normalize || false, + b.stride || 0, + b.offset || 0, + ); + } + }; + } + + const numAttribs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); + for (let ii = 0; ii < numAttribs; ++ii) { + const attribInfo = gl.getActiveAttrib(program, ii); + if (!attribInfo) { + break; + } + const index = gl.getAttribLocation(program, attribInfo.name); + attribSetters[attribInfo.name] = createAttribSetter(index); + } + + return attribSetters; + } + + /** + * Sets attributes and binds buffers (deprecated... use {@link module:webgl-utils.setBuffersAndAttributes}) + * + * Example: + * + * let program = createProgramFromScripts( + * gl, ["some-vs", "some-fs"]); + * + * let attribSetters = createAttributeSetters(program); + * + * let positionBuffer = gl.createBuffer(); + * let texcoordBuffer = gl.createBuffer(); + * + * let attribs = { + * a_position: {buffer: positionBuffer, numComponents: 3}, + * a_texcoord: {buffer: texcoordBuffer, numComponents: 2}, + * }; + * + * gl.useProgram(program); + * + * This will automatically bind the buffers AND set the + * attributes. + * + * setAttributes(attribSetters, attribs); + * + * Properties of attribs. For each attrib you can add + * properties: + * + * * type: the type of data in the buffer. Default = gl.FLOAT + * * normalize: whether or not to normalize the data. Default = false + * * stride: the stride. Default = 0 + * * offset: offset into the buffer. Default = 0 + * + * For example if you had 3 value float positions, 2 value + * float texcoord and 4 value uint8 colors you'd setup your + * attribs like this + * + * let attribs = { + * a_position: {buffer: positionBuffer, numComponents: 3}, + * a_texcoord: {buffer: texcoordBuffer, numComponents: 2}, + * a_color: { + * buffer: colorBuffer, + * numComponents: 4, + * type: gl.UNSIGNED_BYTE, + * normalize: true, + * }, + * }; + * + * @param {Object.|model:webgl-utils.ProgramInfo} setters Attribute setters as returned from createAttributeSetters or a ProgramInfo as returned {@link module:webgl-utils.createProgramInfo} + * @param {Object.} attribs AttribInfos mapped by attribute name. + * @memberOf module:webgl-utils + * @deprecated use {@link module:webgl-utils.setBuffersAndAttributes} + */ + function setAttributes(setters, attribs) { + setters = setters.attribSetters || setters; + Object.keys(attribs).forEach(function (name) { + const setter = setters[name]; + if (setter) { + setter(attribs[name]); + } + }); + } + + /** + * Creates a vertex array object and then sets the attributes + * on it + * + * @param {WebGLRenderingContext} gl The WebGLRenderingContext + * to use. + * @param {Object.} setters Attribute setters as returned from createAttributeSetters + * @param {Object.} attribs AttribInfos mapped by attribute name. + * @param {WebGLBuffer} [indices] an optional ELEMENT_ARRAY_BUFFER of indices + */ + function createVAOAndSetAttributes(gl, setters, attribs, indices) { + const vao = gl.createVertexArray(); + gl.bindVertexArray(vao); + setAttributes(setters, attribs); + if (indices) { + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indices); + } + // We unbind this because otherwise any change to ELEMENT_ARRAY_BUFFER + // like when creating buffers for other stuff will mess up this VAO's binding + gl.bindVertexArray(null); + return vao; + } + + /** + * Creates a vertex array object and then sets the attributes + * on it + * + * @param {WebGLRenderingContext} gl The WebGLRenderingContext + * to use. + * @param {Object.| module:webgl-utils.ProgramInfo} programInfo as returned from createProgramInfo or Attribute setters as returned from createAttributeSetters + * @param {module:webgl-utils:BufferInfo} bufferInfo BufferInfo as returned from createBufferInfoFromArrays etc... + * @param {WebGLBuffer} [indices] an optional ELEMENT_ARRAY_BUFFER of indices + */ + function createVAOFromBufferInfo(gl, programInfo, bufferInfo) { + return createVAOAndSetAttributes( + gl, + programInfo.attribSetters || programInfo, + bufferInfo.attribs, + bufferInfo.indices, + ); + } + + /** + * @typedef {Object} ProgramInfo + * @property {WebGLProgram} program A shader program + * @property {Object} uniformSetters: object of setters as returned from createUniformSetters, + * @property {Object} attribSetters: object of setters as returned from createAttribSetters, + * @memberOf module:webgl-utils + */ + + /** + * Creates a ProgramInfo from 2 sources. + * + * A ProgramInfo contains + * + * programInfo = { + * program: WebGLProgram, + * uniformSetters: object of setters as returned from createUniformSetters, + * attribSetters: object of setters as returned from createAttribSetters, + * } + * + * @param {WebGLRenderingContext} gl The WebGLRenderingContext + * to use. + * @param {string[]} shaderSourcess Array of sources for the + * shaders or ids. The first is assumed to be the vertex shader, + * the second the fragment shader. + * @param {string[]} [opt_attribs] An array of attribs names. Locations will be assigned by index if not passed in + * @param {number[]} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. By default it just prints an error to the console + * on error. If you want something else pass an callback. It's passed an error message. + * @return {module:webgl-utils.ProgramInfo} The created program. + * @memberOf module:webgl-utils + */ + function createProgramInfo( + gl, + shaderSources, + opt_attribs, + opt_locations, + opt_errorCallback, + ) { + shaderSources = shaderSources.map(function (source) { + const script = document.getElementById(source); + return script ? script.text : source; + }); + const program = webglUtils.createProgramFromSources( + gl, + shaderSources, + opt_attribs, + opt_locations, + opt_errorCallback, + ); + if (!program) { + return null; + } + const uniformSetters = createUniformSetters(gl, program); + const attribSetters = createAttributeSetters(gl, program); + return { + program: program, + uniformSetters: uniformSetters, + attribSetters: attribSetters, + }; + } + + /** + * Sets attributes and buffers including the `ELEMENT_ARRAY_BUFFER` if appropriate + * + * Example: + * + * let programInfo = createProgramInfo( + * gl, ["some-vs", "some-fs"]); + * + * let arrays = { + * position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, + * texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, + * }; + * + * let bufferInfo = createBufferInfoFromArrays(gl, arrays); + * + * gl.useProgram(programInfo.program); + * + * This will automatically bind the buffers AND set the + * attributes. + * + * setBuffersAndAttributes(programInfo.attribSetters, bufferInfo); + * + * For the example above it is equivilent to + * + * gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + * gl.enableVertexAttribArray(a_positionLocation); + * gl.vertexAttribPointer(a_positionLocation, 3, gl.FLOAT, false, 0, 0); + * gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); + * gl.enableVertexAttribArray(a_texcoordLocation); + * gl.vertexAttribPointer(a_texcoordLocation, 4, gl.FLOAT, false, 0, 0); + * + * @param {WebGLRenderingContext} gl A WebGLRenderingContext. + * @param {Object.} setters Attribute setters as returned from `createAttributeSetters` + * @param {module:webgl-utils.BufferInfo} buffers a BufferInfo as returned from `createBufferInfoFromArrays`. + * @memberOf module:webgl-utils + */ + function setBuffersAndAttributes(gl, setters, buffers) { + setAttributes(setters, buffers.attribs); + if (buffers.indices) { + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices); + } + } + + // Add your prefix here. + const browserPrefixes = ['', 'MOZ_', 'OP_', 'WEBKIT_']; + + /** + * Given an extension name like WEBGL_compressed_texture_s3tc + * returns the supported version extension, like + * WEBKIT_WEBGL_compressed_teture_s3tc + * @param {string} name Name of extension to look for + * @return {WebGLExtension} The extension or undefined if not + * found. + * @memberOf module:webgl-utils + */ + function getExtensionWithKnownPrefixes(gl, name) { + for (let ii = 0; ii < browserPrefixes.length; ++ii) { + const prefixedName = browserPrefixes[ii] + name; + const ext = gl.getExtension(prefixedName); + if (ext) { + return ext; + } + } + return undefined; + } + + /** + * Resize a canvas to match the size its displayed. + * @param {HTMLCanvasElement} canvas The canvas to resize. + * @param {number} [multiplier] amount to multiply by. + * Pass in window.devicePixelRatio for native pixels. + * @return {boolean} true if the canvas was resized. + * @memberOf module:webgl-utils + */ + function resizeCanvasToDisplaySize(canvas, multiplier) { + multiplier = multiplier || 1; + const width = (canvas.clientWidth * multiplier) | 0; + const height = (canvas.clientHeight * multiplier) | 0; + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + return true; + } + return false; + } + + // Add `push` to a typed array. It just keeps a 'cursor' + // and allows use to `push` values into the array so we + // don't have to manually compute offsets + function augmentTypedArray(typedArray, numComponents) { + let cursor = 0; + typedArray.push = function () { + for (let ii = 0; ii < arguments.length; ++ii) { + const value = arguments[ii]; + if ( + value instanceof Array || + (value.buffer && value.buffer instanceof ArrayBuffer) + ) { + for (let jj = 0; jj < value.length; ++jj) { + typedArray[cursor++] = value[jj]; + } + } else { + typedArray[cursor++] = value; + } + } + }; + typedArray.reset = function (opt_index) { + cursor = opt_index || 0; + }; + typedArray.numComponents = numComponents; + Object.defineProperty(typedArray, 'numElements', { + get: function () { + return (this.length / this.numComponents) | 0; + }, + }); + return typedArray; + } + + /** + * creates a typed array with a `push` function attached + * so that you can easily *push* values. + * + * `push` can take multiple arguments. If an argument is an array each element + * of the array will be added to the typed array. + * + * Example: + * + * let array = createAugmentedTypedArray(3, 2); // creates a Float32Array with 6 values + * array.push(1, 2, 3); + * array.push([4, 5, 6]); + * // array now contains [1, 2, 3, 4, 5, 6] + * + * Also has `numComponents` and `numElements` properties. + * + * @param {number} numComponents number of components + * @param {number} numElements number of elements. The total size of the array will be `numComponents * numElements`. + * @param {constructor} opt_type A constructor for the type. Default = `Float32Array`. + * @return {ArrayBuffer} A typed array. + * @memberOf module:webgl-utils + */ + function createAugmentedTypedArray(numComponents, numElements, opt_type) { + const Type = opt_type || Float32Array; + return augmentTypedArray( + new Type(numComponents * numElements), + numComponents, + ); + } + + function createBufferFromTypedArray(gl, array, type, drawType) { + type = type || gl.ARRAY_BUFFER; + const buffer = gl.createBuffer(); + gl.bindBuffer(type, buffer); + gl.bufferData(type, array, drawType || gl.STATIC_DRAW); + return buffer; + } + + function allButIndices(name) { + return name !== 'indices'; + } + + function createMapping(obj) { + const mapping = {}; + Object.keys(obj) + .filter(allButIndices) + .forEach(function (key) { + mapping['a_' + key] = key; + }); + return mapping; + } + + function getGLTypeForTypedArray(gl, typedArray) { + if (typedArray instanceof Int8Array) { + return gl.BYTE; + } // eslint-disable-line + if (typedArray instanceof Uint8Array) { + return gl.UNSIGNED_BYTE; + } // eslint-disable-line + if (typedArray instanceof Int16Array) { + return gl.SHORT; + } // eslint-disable-line + if (typedArray instanceof Uint16Array) { + return gl.UNSIGNED_SHORT; + } // eslint-disable-line + if (typedArray instanceof Int32Array) { + return gl.INT; + } // eslint-disable-line + if (typedArray instanceof Uint32Array) { + return gl.UNSIGNED_INT; + } // eslint-disable-line + if (typedArray instanceof Float32Array) { + return gl.FLOAT; + } // eslint-disable-line + throw 'unsupported typed array type'; + } + + // This is really just a guess. Though I can't really imagine using + // anything else? Maybe for some compression? + function getNormalizationForTypedArray(typedArray) { + if (typedArray instanceof Int8Array) { + return true; + } // eslint-disable-line + if (typedArray instanceof Uint8Array) { + return true; + } // eslint-disable-line + return false; + } + + function isArrayBuffer(a) { + return a.buffer && a.buffer instanceof ArrayBuffer; + } + + function guessNumComponentsFromName(name, length) { + let numComponents; + if (name.indexOf('coord') >= 0) { + numComponents = 2; + } else if (name.indexOf('color') >= 0) { + numComponents = 4; + } else { + numComponents = 3; // position, normals, indices ... + } + + if (length % numComponents > 0) { + throw 'can not guess numComponents. You should specify it.'; + } + + return numComponents; + } + + function makeTypedArray(array, name) { + if (isArrayBuffer(array)) { + return array; + } + + if (array.data && isArrayBuffer(array.data)) { + return array.data; + } + + if (Array.isArray(array)) { + array = { + data: array, + }; + } + + if (!array.numComponents) { + array.numComponents = guessNumComponentsFromName(name, array.length); + } + + let type = array.type; + if (!type) { + if (name === 'indices') { + type = Uint16Array; + } + } + const typedArray = createAugmentedTypedArray( + array.numComponents, + (array.data.length / array.numComponents) | 0, + type, + ); + typedArray.push(array.data); + return typedArray; + } + + /** + * @typedef {Object} AttribInfo + * @property {number} [numComponents] the number of components for this attribute. + * @property {number} [size] the number of components for this attribute. + * @property {number} [type] the type of the attribute (eg. `gl.FLOAT`, `gl.UNSIGNED_BYTE`, etc...) Default = `gl.FLOAT` + * @property {boolean} [normalized] whether or not to normalize the data. Default = false + * @property {number} [offset] offset into buffer in bytes. Default = 0 + * @property {number} [stride] the stride in bytes per element. Default = 0 + * @property {WebGLBuffer} buffer the buffer that contains the data for this attribute + * @memberOf module:webgl-utils + */ + + /** + * Creates a set of attribute data and WebGLBuffers from set of arrays + * + * Given + * + * let arrays = { + * position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, + * texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, + * normal: { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], }, + * color: { numComponents: 4, data: [255, 255, 255, 255, 255, 0, 0, 255, 0, 0, 255, 255], type: Uint8Array, }, + * indices: { numComponents: 3, data: [0, 1, 2, 1, 2, 3], }, + * }; + * + * returns something like + * + * let attribs = { + * a_position: { numComponents: 3, type: gl.FLOAT, normalize: false, buffer: WebGLBuffer, }, + * a_texcoord: { numComponents: 2, type: gl.FLOAT, normalize: false, buffer: WebGLBuffer, }, + * a_normal: { numComponents: 3, type: gl.FLOAT, normalize: false, buffer: WebGLBuffer, }, + * a_color: { numComponents: 4, type: gl.UNSIGNED_BYTE, normalize: true, buffer: WebGLBuffer, }, + * }; + * + * @param {WebGLRenderingContext} gl The webgl rendering context. + * @param {Object.} arrays The arrays + * @param {Object.} [opt_mapping] mapping from attribute name to array name. + * if not specified defaults to "a_name" -> "name". + * @return {Object.} the attribs + * @memberOf module:webgl-utils + */ + function createAttribsFromArrays(gl, arrays, opt_mapping) { + const mapping = opt_mapping || createMapping(arrays); + const attribs = {}; + Object.keys(mapping).forEach(function (attribName) { + const bufferName = mapping[attribName]; + const origArray = arrays[bufferName]; + if (origArray.value) { + attribs[attribName] = { + value: origArray.value, + }; + } else { + const array = makeTypedArray(origArray, bufferName); + attribs[attribName] = { + buffer: createBufferFromTypedArray(gl, array), + numComponents: + origArray.numComponents || + array.numComponents || + guessNumComponentsFromName(bufferName), + type: getGLTypeForTypedArray(gl, array), + normalize: getNormalizationForTypedArray(array), + }; + } + }); + return attribs; + } + + function getArray(array) { + return array.length ? array : array.data; + } + + const texcoordRE = /coord|texture/i; + const colorRE = /color|colour/i; + + function guessNumComponentsFromName(name, length) { + let numComponents; + if (texcoordRE.test(name)) { + numComponents = 2; + } else if (colorRE.test(name)) { + numComponents = 4; + } else { + numComponents = 3; // position, normals, indices ... + } + + if (length % numComponents > 0) { + throw new Error( + `Can not guess numComponents for attribute '${name}'. Tried ${numComponents} but ${length} values is not evenly divisible by ${numComponents}. You should specify it.`, + ); + } + + return numComponents; + } + + function getNumComponents(array, arrayName) { + return ( + array.numComponents || + array.size || + guessNumComponentsFromName(arrayName, getArray(array).length) + ); + } + + /** + * tries to get the number of elements from a set of arrays. + */ + const positionKeys = ['position', 'positions', 'a_position']; + function getNumElementsFromNonIndexedArrays(arrays) { + let key; + for (const k of positionKeys) { + if (k in arrays) { + key = k; + break; + } + } + key = key || Object.keys(arrays)[0]; + const array = arrays[key]; + const length = getArray(array).length; + const numComponents = getNumComponents(array, key); + const numElements = length / numComponents; + if (length % numComponents > 0) { + throw new Error( + `numComponents ${numComponents} not correct for length ${length}`, + ); + } + return numElements; + } + + /** + * @typedef {Object} BufferInfo + * @property {number} numElements The number of elements to pass to `gl.drawArrays` or `gl.drawElements`. + * @property {WebGLBuffer} [indices] The indices `ELEMENT_ARRAY_BUFFER` if any indices exist. + * @property {Object.} attribs The attribs approriate to call `setAttributes` + * @memberOf module:webgl-utils + */ + + /** + * Creates a BufferInfo from an object of arrays. + * + * This can be passed to {@link module:webgl-utils.setBuffersAndAttributes} and to + * {@link module:webgl-utils:drawBufferInfo}. + * + * Given an object like + * + * let arrays = { + * position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, + * texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, + * normal: { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], }, + * indices: { numComponents: 3, data: [0, 1, 2, 1, 2, 3], }, + * }; + * + * Creates an BufferInfo like this + * + * bufferInfo = { + * numElements: 4, // or whatever the number of elements is + * indices: WebGLBuffer, // this property will not exist if there are no indices + * attribs: { + * a_position: { buffer: WebGLBuffer, numComponents: 3, }, + * a_normal: { buffer: WebGLBuffer, numComponents: 3, }, + * a_texcoord: { buffer: WebGLBuffer, numComponents: 2, }, + * }, + * }; + * + * The properties of arrays can be JavaScript arrays in which case the number of components + * will be guessed. + * + * let arrays = { + * position: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], + * texcoord: [0, 0, 0, 1, 1, 0, 1, 1], + * normal: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], + * indices: [0, 1, 2, 1, 2, 3], + * }; + * + * They can also by TypedArrays + * + * let arrays = { + * position: new Float32Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0]), + * texcoord: new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]), + * normal: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]), + * indices: new Uint16Array([0, 1, 2, 1, 2, 3]), + * }; + * + * Or augmentedTypedArrays + * + * let positions = createAugmentedTypedArray(3, 4); + * let texcoords = createAugmentedTypedArray(2, 4); + * let normals = createAugmentedTypedArray(3, 4); + * let indices = createAugmentedTypedArray(3, 2, Uint16Array); + * + * positions.push([0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0]); + * texcoords.push([0, 0, 0, 1, 1, 0, 1, 1]); + * normals.push([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]); + * indices.push([0, 1, 2, 1, 2, 3]); + * + * let arrays = { + * position: positions, + * texcoord: texcoords, + * normal: normals, + * indices: indices, + * }; + * + * For the last example it is equivalent to + * + * let bufferInfo = { + * attribs: { + * a_position: { numComponents: 3, buffer: gl.createBuffer(), }, + * a_texcoods: { numComponents: 2, buffer: gl.createBuffer(), }, + * a_normals: { numComponents: 3, buffer: gl.createBuffer(), }, + * }, + * indices: gl.createBuffer(), + * numElements: 6, + * }; + * + * gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_position.buffer); + * gl.bufferData(gl.ARRAY_BUFFER, arrays.position, gl.STATIC_DRAW); + * gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_texcoord.buffer); + * gl.bufferData(gl.ARRAY_BUFFER, arrays.texcoord, gl.STATIC_DRAW); + * gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_normal.buffer); + * gl.bufferData(gl.ARRAY_BUFFER, arrays.normal, gl.STATIC_DRAW); + * gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferInfo.indices); + * gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, arrays.indices, gl.STATIC_DRAW); + * + * @param {WebGLRenderingContext} gl A WebGLRenderingContext + * @param {Object.} arrays Your data + * @param {Object.} [opt_mapping] an optional mapping of attribute to array name. + * If not passed in it's assumed the array names will be mapped to an attribute + * of the same name with "a_" prefixed to it. An other words. + * + * let arrays = { + * position: ..., + * texcoord: ..., + * normal: ..., + * indices: ..., + * }; + * + * bufferInfo = createBufferInfoFromArrays(gl, arrays); + * + * Is the same as + * + * let arrays = { + * position: ..., + * texcoord: ..., + * normal: ..., + * indices: ..., + * }; + * + * let mapping = { + * a_position: "position", + * a_texcoord: "texcoord", + * a_normal: "normal", + * }; + * + * bufferInfo = createBufferInfoFromArrays(gl, arrays, mapping); + * + * @return {module:webgl-utils.BufferInfo} A BufferInfo + * @memberOf module:webgl-utils + */ + function createBufferInfoFromArrays(gl, arrays, opt_mapping) { + const bufferInfo = { + attribs: createAttribsFromArrays(gl, arrays, opt_mapping), + }; + let indices = arrays.indices; + if (indices) { + indices = makeTypedArray(indices, 'indices'); + bufferInfo.indices = createBufferFromTypedArray( + gl, + indices, + gl.ELEMENT_ARRAY_BUFFER, + ); + bufferInfo.numElements = indices.length; + } else { + bufferInfo.numElements = getNumElementsFromNonIndexedArrays(arrays); + } + + return bufferInfo; + } + + /** + * Creates buffers from typed arrays + * + * Given something like this + * + * let arrays = { + * positions: [1, 2, 3], + * normals: [0, 0, 1], + * } + * + * returns something like + * + * buffers = { + * positions: WebGLBuffer, + * normals: WebGLBuffer, + * } + * + * If the buffer is named 'indices' it will be made an ELEMENT_ARRAY_BUFFER. + * + * @param {WebGLRenderingContext} gl A WebGLRenderingContext. + * @param {Object} arrays + * @return {Object} returns an object with one WebGLBuffer per array + * @memberOf module:webgl-utils + */ + function createBuffersFromArrays(gl, arrays) { + const buffers = {}; + Object.keys(arrays).forEach(function (key) { + const type = + key === 'indices' ? gl.ELEMENT_ARRAY_BUFFER : gl.ARRAY_BUFFER; + const array = makeTypedArray(arrays[key], name); + buffers[key] = createBufferFromTypedArray(gl, array, type); + }); + + // hrm + if (arrays.indices) { + buffers.numElements = arrays.indices.length; + } else if (arrays.position) { + buffers.numElements = arrays.position.length / 3; + } + + return buffers; + } + + /** + * Calls `gl.drawElements` or `gl.drawArrays`, whichever is appropriate + * + * normally you'd call `gl.drawElements` or `gl.drawArrays` yourself + * but calling this means if you switch from indexed data to non-indexed + * data you don't have to remember to update your draw call. + * + * @param {WebGLRenderingContext} gl A WebGLRenderingContext + * @param {module:webgl-utils.BufferInfo} bufferInfo as returned from createBufferInfoFromArrays + * @param {enum} [primitiveType] eg (gl.TRIANGLES, gl.LINES, gl.POINTS, gl.TRIANGLE_STRIP, ...) + * @param {number} [count] An optional count. Defaults to bufferInfo.numElements + * @param {number} [offset] An optional offset. Defaults to 0. + * @memberOf module:webgl-utils + */ + function drawBufferInfo(gl, bufferInfo, primitiveType, count, offset) { + const indices = bufferInfo.indices; + primitiveType = primitiveType === undefined ? gl.TRIANGLES : primitiveType; + const numElements = count === undefined ? bufferInfo.numElements : count; + offset = offset === undefined ? 0 : offset; + if (indices) { + gl.drawElements(primitiveType, numElements, gl.UNSIGNED_SHORT, offset); + } else { + gl.drawArrays(primitiveType, offset, numElements); + } + } + + /** + * @typedef {Object} DrawObject + * @property {module:webgl-utils.ProgramInfo} programInfo A ProgramInfo as returned from createProgramInfo + * @property {module:webgl-utils.BufferInfo} bufferInfo A BufferInfo as returned from createBufferInfoFromArrays + * @property {Object} uniforms The values for the uniforms + * @memberOf module:webgl-utils + */ + + /** + * Draws a list of objects + * @param {WebGLRenderingContext} gl A WebGLRenderingContext + * @param {DrawObject[]} objectsToDraw an array of objects to draw. + * @memberOf module:webgl-utils + */ + function drawObjectList(gl, objectsToDraw) { + let lastUsedProgramInfo = null; + let lastUsedBufferInfo = null; + + objectsToDraw.forEach(function (object) { + const programInfo = object.programInfo; + const bufferInfo = object.bufferInfo; + let bindBuffers = false; + + if (programInfo !== lastUsedProgramInfo) { + lastUsedProgramInfo = programInfo; + gl.useProgram(programInfo.program); + bindBuffers = true; + } + + // Setup all the needed attributes. + if (bindBuffers || bufferInfo !== lastUsedBufferInfo) { + lastUsedBufferInfo = bufferInfo; + setBuffersAndAttributes(gl, programInfo.attribSetters, bufferInfo); + } + + // Set the uniforms. + setUniforms(programInfo.uniformSetters, object.uniforms); + + // Draw + drawBufferInfo(gl, bufferInfo); + }); + } + + function glEnumToString(gl, v) { + const results = []; + for (const key in gl) { + if (gl[key] === v) { + results.push(key); + } + } + return results.length ? results.join(' | ') : `0x${v.toString(16)}`; + } + + const isIE = /*@cc_on!@*/ false || !!document.documentMode; + // Edge 20+ + const isEdge = !isIE && !!window.StyleMedia; + if (isEdge) { + // Hack for Edge. Edge's WebGL implmentation is crap still and so they + // only respond to "experimental-webgl". I don't want to clutter the + // examples with that so his hack works around it + HTMLCanvasElement.prototype.getContext = (function (origFn) { + return function () { + let args = arguments; + const type = args[0]; + if (type === 'webgl') { + args = [].slice.call(arguments); + args[0] = 'experimental-webgl'; + } + return origFn.apply(this, args); + }; + })(HTMLCanvasElement.prototype.getContext); + } + + return { + createAugmentedTypedArray: createAugmentedTypedArray, + createAttribsFromArrays: createAttribsFromArrays, + createBuffersFromArrays: createBuffersFromArrays, + createBufferInfoFromArrays: createBufferInfoFromArrays, + createAttributeSetters: createAttributeSetters, + createProgram: createProgram, + createProgramFromScripts: createProgramFromScripts, + createProgramFromSources: createProgramFromSources, + createProgramInfo: createProgramInfo, + createUniformSetters: createUniformSetters, + createVAOAndSetAttributes: createVAOAndSetAttributes, + createVAOFromBufferInfo: createVAOFromBufferInfo, + drawBufferInfo: drawBufferInfo, + drawObjectList: drawObjectList, + glEnumToString: glEnumToString, + getExtensionWithKnownPrefixes: getExtensionWithKnownPrefixes, + resizeCanvasToDisplaySize: resizeCanvasToDisplaySize, + setAttributes: setAttributes, + setBuffersAndAttributes: setBuffersAndAttributes, + setUniforms: setUniforms, + }; +}); diff --git a/packages/rrweb/test/html/benchmark-dom-mutation-add-and-remove.html b/packages/rrweb/test/html/benchmark-dom-mutation-add-and-remove.html new file mode 100644 index 00000000..49efad44 --- /dev/null +++ b/packages/rrweb/test/html/benchmark-dom-mutation-add-and-remove.html @@ -0,0 +1,46 @@ + + + + diff --git a/packages/rrweb/test/html/benchmark-dom-mutation.html b/packages/rrweb/test/html/benchmark-dom-mutation.html new file mode 100644 index 00000000..2921964c --- /dev/null +++ b/packages/rrweb/test/html/benchmark-dom-mutation.html @@ -0,0 +1,27 @@ + + + + diff --git a/test/html/block.html b/packages/rrweb/test/html/block.html similarity index 83% rename from test/html/block.html rename to packages/rrweb/test/html/block.html index 6fee77f7..4cef3cb6 100644 --- a/test/html/block.html +++ b/packages/rrweb/test/html/block.html @@ -7,7 +7,7 @@ Block record -
+
diff --git a/packages/rrweb/test/html/blocked-unblocked.html b/packages/rrweb/test/html/blocked-unblocked.html new file mode 100644 index 00000000..ff87e546 --- /dev/null +++ b/packages/rrweb/test/html/blocked-unblocked.html @@ -0,0 +1,88 @@ + + Uber Application for Codegen Testing + + + + + +
+

+ Verify that block class bugs are fixed +

+
+
+
+ +
+


+
+ +
+


+ +
+


+
+
+ +
+


+
+ +
+


+ +
+ diff --git a/packages/rrweb/test/html/canvas-webgl-image.html b/packages/rrweb/test/html/canvas-webgl-image.html new file mode 100644 index 00000000..96bc3103 --- /dev/null +++ b/packages/rrweb/test/html/canvas-webgl-image.html @@ -0,0 +1,149 @@ + + + + + + + Document + + + + + + + + + + + + diff --git a/packages/rrweb/test/html/canvas-webgl-square.html b/packages/rrweb/test/html/canvas-webgl-square.html new file mode 100644 index 00000000..0388cd94 --- /dev/null +++ b/packages/rrweb/test/html/canvas-webgl-square.html @@ -0,0 +1,115 @@ + + + + + + canvas webgl square + + + + + + + + + diff --git a/packages/rrweb/test/html/canvas-webgl.html b/packages/rrweb/test/html/canvas-webgl.html new file mode 100644 index 00000000..52aacabc --- /dev/null +++ b/packages/rrweb/test/html/canvas-webgl.html @@ -0,0 +1,27 @@ + + + + + + canvas + + + + + + + diff --git a/test/html/canvas.html b/packages/rrweb/test/html/canvas.html similarity index 100% rename from test/html/canvas.html rename to packages/rrweb/test/html/canvas.html diff --git a/packages/rrweb/test/html/form.html b/packages/rrweb/test/html/form.html new file mode 100644 index 00000000..a89f11ff --- /dev/null +++ b/packages/rrweb/test/html/form.html @@ -0,0 +1,38 @@ + + + + + + + form fields + + + +
+ + + + + + + +
+ + diff --git a/packages/rrweb/test/html/frame-image-blob-url.html b/packages/rrweb/test/html/frame-image-blob-url.html new file mode 100644 index 00000000..038ced16 --- /dev/null +++ b/packages/rrweb/test/html/frame-image-blob-url.html @@ -0,0 +1,11 @@ + + + + + + Frame with image + + + + + diff --git a/packages/rrweb/test/html/frame1.html b/packages/rrweb/test/html/frame1.html new file mode 100644 index 00000000..36a4d335 --- /dev/null +++ b/packages/rrweb/test/html/frame1.html @@ -0,0 +1,21 @@ + + + + + + Frame 1 + + + frame 1 + + + + + + + diff --git a/packages/rrweb/test/html/frame2.html b/packages/rrweb/test/html/frame2.html new file mode 100644 index 00000000..6344438c --- /dev/null +++ b/packages/rrweb/test/html/frame2.html @@ -0,0 +1,18 @@ + + + + + + Frame 2 + + + frame 2 + + + diff --git a/test/html/ignore.html b/packages/rrweb/test/html/ignore.html similarity index 67% rename from test/html/ignore.html rename to packages/rrweb/test/html/ignore.html index 91e0652d..30c4a29c 100644 --- a/test/html/ignore.html +++ b/packages/rrweb/test/html/ignore.html @@ -9,8 +9,7 @@
- - +
diff --git a/packages/rrweb/test/html/image-blob-url.html b/packages/rrweb/test/html/image-blob-url.html new file mode 100644 index 00000000..4dd3f608 --- /dev/null +++ b/packages/rrweb/test/html/image-blob-url.html @@ -0,0 +1,21 @@ + + + + + + + Image with blob:url + + + + + diff --git a/test/html/log.html b/packages/rrweb/test/html/log.html similarity index 100% rename from test/html/log.html rename to packages/rrweb/test/html/log.html diff --git a/packages/rrweb/test/html/main.html b/packages/rrweb/test/html/main.html new file mode 100644 index 00000000..9363103a --- /dev/null +++ b/packages/rrweb/test/html/main.html @@ -0,0 +1,25 @@ + + + + + + Main + + + + + + + diff --git a/packages/rrweb/test/html/mask-text.html b/packages/rrweb/test/html/mask-text.html new file mode 100644 index 00000000..f27e656d --- /dev/null +++ b/packages/rrweb/test/html/mask-text.html @@ -0,0 +1,20 @@ + + + + + + + Mask text + + +

mask1

+
+ mask2 +
+
+
+
mask3
+
+
+ + diff --git a/test/html/move-node.html b/packages/rrweb/test/html/move-node.html similarity index 89% rename from test/html/move-node.html rename to packages/rrweb/test/html/move-node.html index bbe607b7..cf08e234 100644 --- a/test/html/move-node.html +++ b/packages/rrweb/test/html/move-node.html @@ -1,3 +1,4 @@ +
diff --git a/test/html/mutation-observer.html b/packages/rrweb/test/html/mutation-observer.html similarity index 58% rename from test/html/mutation-observer.html rename to packages/rrweb/test/html/mutation-observer.html index d5b84059..9d149c5a 100644 --- a/test/html/mutation-observer.html +++ b/packages/rrweb/test/html/mutation-observer.html @@ -1,6 +1,8 @@ +

mutation observer

- \ No newline at end of file + + diff --git a/packages/rrweb/test/html/password.html b/packages/rrweb/test/html/password.html new file mode 100644 index 00000000..59ab9331 --- /dev/null +++ b/packages/rrweb/test/html/password.html @@ -0,0 +1,18 @@ + + + + + + + Document + + + + + + diff --git a/packages/rrweb/test/html/polyfilled-shadowdom-mutation.html b/packages/rrweb/test/html/polyfilled-shadowdom-mutation.html new file mode 100644 index 00000000..0f7ec6e0 --- /dev/null +++ b/packages/rrweb/test/html/polyfilled-shadowdom-mutation.html @@ -0,0 +1,24 @@ + + + + + + +
+
+
+ + + diff --git a/test/html/react-styled-components.html b/packages/rrweb/test/html/react-styled-components.html similarity index 100% rename from test/html/react-styled-components.html rename to packages/rrweb/test/html/react-styled-components.html diff --git a/test/html/select2.html b/packages/rrweb/test/html/select2.html similarity index 100% rename from test/html/select2.html rename to packages/rrweb/test/html/select2.html diff --git a/packages/rrweb/test/html/shadow-dom.html b/packages/rrweb/test/html/shadow-dom.html new file mode 100644 index 00000000..bf4c6837 --- /dev/null +++ b/packages/rrweb/test/html/shadow-dom.html @@ -0,0 +1,83 @@ + + + + + + Shadow DOM Observer + + + +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellat odit + officiis necessitatibus laborum asperiores et adipisci dolores corporis, + vero distinctio voluptas, suscipit commodi architecto, aliquam fugit. + Nesciunt labore reiciendis blanditiis! +

+ +
+ +
+ +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellat odit + officiis necessitatibus laborum asperiores et adipisci dolores corporis, + vero distinctio voluptas, suscipit commodi architecto, aliquam fugit. + Nesciunt labore reiciendis blanditiis! +

+ + + + diff --git a/test/html/shuffle.html b/packages/rrweb/test/html/shuffle.html similarity index 100% rename from test/html/shuffle.html rename to packages/rrweb/test/html/shuffle.html diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts new file mode 100644 index 00000000..fc1afeb7 --- /dev/null +++ b/packages/rrweb/test/integration.test.ts @@ -0,0 +1,791 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type * as puppeteer from 'puppeteer'; +import { + assertSnapshot, + startServer, + getServerURL, + launchPuppeteer, + waitForRAF, + replaceLast, + generateRecordSnippet, + ISuite, +} from './utils'; +import { + recordOptions, + eventWithTime, + EventType, + RecordPlugin, +} from '../src/types'; +import { visitSnapshot, NodeType } from '@highlight-run/rrweb-snapshot'; + +describe('record integration tests', function (this: ISuite) { + jest.setTimeout(10_000); + + const getHtml = ( + fileName: string, + options: recordOptions = {}, + ): string => { + const filePath = path.resolve(__dirname, `./html/${fileName}`); + const html = fs.readFileSync(filePath, 'utf8'); + return replaceLast( + html, + '', + ` + + + `, + ); + }; + + let server: ISuite['server']; + let serverURL: string; + let code: ISuite['code']; + let browser: ISuite['browser']; + + beforeAll(async () => { + server = await startServer(); + serverURL = getServerURL(server); + browser = await launchPuppeteer(); + + const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); + const pluginsCode = [ + path.resolve(__dirname, '../dist/plugins/console-record.min.js'), + ] + .map((p) => fs.readFileSync(p, 'utf8')) + .join(); + code = fs.readFileSync(bundlePath, 'utf8') + pluginsCode; + }); + + afterAll(async () => { + await browser.close(); + server.close(); + }); + + it('can record form interactions', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'form.html')); + + await page.type('input[type="text"]', 'test'); + await page.click('input[type="radio"]'); + await page.click('input[type="checkbox"]'); + await page.type('textarea', 'textarea test'); + await page.select('select', '1'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('can record childList mutations', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'mutation-observer.html')); + + await page.evaluate(() => { + const li = document.createElement('li'); + const ul = document.querySelector('ul') as HTMLUListElement; + ul.appendChild(li); + document.body.removeChild(ul); + const p = document.querySelector('p') as HTMLParagraphElement; + p.appendChild(document.createElement('span')); + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('can record character data muatations', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'mutation-observer.html')); + + await page.evaluate(() => { + const li = document.createElement('li'); + const ul = document.querySelector('ul') as HTMLUListElement; + ul.appendChild(li); + li.innerText = 'new list item'; + li.innerText = 'new list item edit'; + document.body.removeChild(ul); + const p = document.querySelector('p') as HTMLParagraphElement; + p.innerText = 'mutated'; + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('can record attribute mutation', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'mutation-observer.html')); + + await page.evaluate(() => { + const li = document.createElement('li'); + const ul = document.querySelector('ul') as HTMLUListElement; + ul.appendChild(li); + li.setAttribute('foo', 'bar'); + document.body.removeChild(ul); + document.body.setAttribute('test', 'true'); + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('can record node mutations', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'select2.html'), { + waitUntil: 'networkidle0', + }); + + // toggle the select box + await page.click('.select2-container', { clickCount: 2, delay: 100 }); + // test storage of !important style + await page.evaluate( + 'document.getElementById("select2-drop").setAttribute("style", document.getElementById("select2-drop").style.cssText + "color:black !important")', + ); + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('can freeze mutations', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'mutation-observer.html', { recordCanvas: true }), + ); + + await page.evaluate(() => { + const li = document.createElement('li'); + const ul = document.querySelector('ul') as HTMLUListElement; + ul.appendChild(li); + li.setAttribute('foo', 'bar'); + document.body.setAttribute('test', 'true'); + }); + await page.evaluate('rrweb.freezePage()'); + await page.evaluate(() => { + document.body.setAttribute('test', 'bad'); + const canvas = document.querySelector('canvas') as HTMLCanvasElement; + const gl = canvas.getContext('webgl') as WebGLRenderingContext; + gl.getExtension('bad'); + const ul = document.querySelector('ul') as HTMLUListElement; + const li = document.createElement('li'); + li.setAttribute('bad-attr', 'bad'); + li.innerText = 'bad text'; + ul.appendChild(li); + document.body.removeChild(ul); + }); + + await waitForRAF(page); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should not record input events on ignored elements', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'ignore.html')); + + await page.type('.highlight-ignore', 'secret'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should not record input values if maskAllInputs is enabled', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'form.html', { maskAllInputs: true }), + ); + + await page.type('input[type="text"]', 'test'); + await page.click('input[type="radio"]'); + await page.click('input[type="checkbox"]'); + await page.type('input[type="password"]', 'password'); + await page.type('textarea', 'textarea test'); + await page.select('select', '1'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('can use maskInputOptions to configure which type of inputs should be masked', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'form.html', { + maskInputOptions: { + text: false, + textarea: false, + password: true, + }, + }), + ); + + await page.type('input[type="text"]', 'test'); + await page.click('input[type="radio"]'); + await page.click('input[type="checkbox"]'); + await page.type('textarea', 'textarea test'); + await page.type('input[type="password"]', 'password'); + await page.select('select', '1'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should mask value attribute with maskInputOptions', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'password.html', { + maskInputOptions: { + password: true, + }, + }), + ); + + await page.type('input[type="password"]', 'secr3t'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record input userTriggered values if userTriggeredOnInput is enabled', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'form.html', { userTriggeredOnInput: true }), + ); + + await page.type('input[type="text"]', 'test'); + await page.click('input[type="radio"]'); + await page.click('input[type="checkbox"]'); + await page.type('input[type="password"]', 'password'); + await page.type('textarea', 'textarea test'); + await page.select('select', '1'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should not record blocked elements and its child nodes', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'block.html')); + + await page.type('input', 'should not be record'); + await page.evaluate(`document.getElementById('text').innerText = '1'`); + await page.click('#text'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should not record blocked elements dynamically added', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'block.html')); + + await page.evaluate(() => { + const el = document.createElement('button'); + el.className = 'highlight-block'; + el.style.width = '100px'; + el.style.height = '100px'; + el.innerText = 'Should not be recorded'; + + const nextElement = document.querySelector('.highlight-block')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('mutations should work when blocked class is unblocked', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about: blank'); + await page.setContent(getHtml.call(this, 'blocked-unblocked.html')); + + const elements1 = await page.$x('/html/body/div[1]/button'); + await elements1[0].click(); + + const elements2 = await page.$x('/html/body/div[2]/button'); + await elements2[0].click(); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record DOM node movement 1', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'move-node.html')); + + await page.evaluate(() => { + const div = document.querySelector('div')!; + const p = document.querySelector('p')!; + const span = document.querySelector('span')!; + document.body.removeChild(span); + p.appendChild(span); + p.removeChild(span); + div.appendChild(span); + }); + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record DOM node movement 2', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'move-node.html')); + + await page.evaluate(() => { + const div = document.createElement('div'); + const span = document.querySelector('span')!; + document.body.appendChild(div); + div.appendChild(span); + }); + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record dynamic CSS changes', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'react-styled-components.html')); + await page.click('.toggle'); + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record canvas mutations', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'canvas.html', { + recordCanvas: true, + }), + ); + await waitForRAF(page); + const snapshots = await page.evaluate('window.snapshots'); + for (const event of snapshots) { + if (event.type === EventType.FullSnapshot) { + visitSnapshot(event.data.node, (n) => { + if (n.type === NodeType.Element && n.attributes.rr_dataURL) { + n.attributes.rr_dataURL = `LOOKS LIKE WE COULD NOT GET STABLE BASE64 FROM SAME IMAGE.`; + } + }); + } + } + assertSnapshot(snapshots); + }); + + it('should record webgl canvas mutations', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'canvas-webgl.html', { + recordCanvas: true, + }), + ); + await page.waitForTimeout(50); + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('will serialize node before record', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'mutation-observer.html')); + + await page.evaluate(() => { + const ul = document.querySelector('ul') as HTMLUListElement; + let count = 3; + while (count > 0) { + count--; + const li = document.createElement('li'); + ul.appendChild(li); + } + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('will defer missing next node mutation', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'shuffle.html')); + + const text = await page.evaluate(() => { + const els = Array.prototype.slice.call(document.querySelectorAll('li')); + const parent = document.querySelector('ul')!; + parent.removeChild(els[3]); + parent.removeChild(els[2]); + parent.removeChild(els[1]); + parent.removeChild(els[0]); + parent.insertBefore(els[3], els[4]); + parent.insertBefore(els[2], els[4]); + parent.insertBefore(els[1], els[4]); + parent.insertBefore(els[0], els[4]); + return parent.innerText; + }); + + expect(text).toEqual('4\n3\n2\n1\n5'); + }); + + it('should record console messages', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml('log.html', { + plugins: ('[rrwebConsoleRecord.getRecordConsolePlugin()]' as unknown) as RecordPlugin[], + }), + ); + + await page.evaluate(() => { + console.assert(0 === 0, 'assert'); + console.count('count'); + console.countReset('count'); + console.debug('debug'); + console.dir('dir'); + console.dirxml('dirxml'); + console.group(); + console.groupCollapsed(); + console.info('info'); + console.log('log'); + console.table('table'); + console.time(); + console.timeEnd(); + console.timeLog(); + console.trace('trace'); + console.warn('warn'); + console.clear(); + console.log(new TypeError('a message')); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + }); + + await page.frames()[1].evaluate(() => { + console.log('from iframe'); + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should nest record iframe', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto(`${serverURL}/html`); + await page.setContent(getHtml.call(this, 'main.html')); + + await page.waitForSelector('#two'); + const frameIdTwo = await page.frames()[2]; + await frameIdTwo.waitForSelector('#four'); + const frameIdFour = frameIdTwo.childFrames()[1]; + await frameIdFour.waitForSelector('#five'); + + await page.waitForTimeout(50); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record images with blob url', async () => { + const page: puppeteer.Page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html`); + page.setContent( + getHtml.call(this, 'image-blob-url.html', { inlineImages: true }), + ); + await page.waitForResponse(`${serverURL}/html/assets/robot.png`); + await page.waitForSelector('img'); // wait for image to get added + await waitForRAF(page); // wait for image to be captured + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record images inside iframe with blob url', async () => { + const page: puppeteer.Page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html`); + await page.setContent( + getHtml.call(this, 'frame-image-blob-url.html', { inlineImages: true }), + ); + await page.waitForResponse(`${serverURL}/html/assets/robot.png`); + await page.waitForTimeout(50); // wait for image to get added + await waitForRAF(page); // wait for image to be captured + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record images inside iframe with blob url after iframe was reloaded', async () => { + const page: puppeteer.Page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + await page.goto(`${serverURL}/html`); + await page.setContent( + getHtml.call(this, 'frame2.html', { inlineImages: true }), + ); + await page.waitForSelector('iframe'); // wait for iframe to get added + await waitForRAF(page); // wait for iframe to load + page.evaluate(() => { + const iframe = document.querySelector('iframe')!; + iframe.setAttribute('src', '/html/image-blob-url.html'); + }); + await page.waitForResponse(`${serverURL}/html/assets/robot.png`); // wait for image to get loaded + await page.waitForTimeout(50); // wait for image to get added + await waitForRAF(page); // wait for image to be captured + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record shadow DOM', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'shadow-dom.html')); + + await page.evaluate(() => { + const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + const el = document.querySelector('.my-element') as HTMLDivElement; + const shadowRoot = el.shadowRoot as ShadowRoot; + shadowRoot.appendChild(document.createElement('p')); + sleep(1) + .then(() => { + shadowRoot.lastChild!.appendChild(document.createElement('p')); + return sleep(1); + }) + .then(() => { + const firstP = shadowRoot.querySelector('p') as HTMLParagraphElement; + shadowRoot.removeChild(firstP); + return sleep(1); + }) + .then(() => { + (shadowRoot.lastChild!.childNodes[0] as HTMLElement).innerText = 'hi'; + return sleep(1); + }) + .then(() => { + (shadowRoot.lastChild!.childNodes[0] as HTMLElement).innerText = + '123'; + const nestedShadowElement = shadowRoot.lastChild! + .childNodes[0] as HTMLElement; + nestedShadowElement.attachShadow({ + mode: 'open', + }); + nestedShadowElement.shadowRoot!.appendChild( + document.createElement('span'), + ); + (nestedShadowElement.shadowRoot!.lastChild as HTMLElement).innerText = + 'nested shadow dom'; + }); + }); + await page.waitForTimeout(50); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record nested iframes and shadow doms', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'frame2.html')); + + await page.waitForTimeout(10); // wait till frame was added to dom + await waitForRAF(page); // wait till browser loaded contents of frame + + await page.evaluate(() => { + // get contentDocument of iframe five + const contentDocument1 = document.querySelector('iframe')! + .contentDocument!; + // create shadow dom #1 + contentDocument1.body.attachShadow({ mode: 'open' }); + contentDocument1.body.shadowRoot!.appendChild( + document.createElement('div'), + ); + const div = contentDocument1.body.shadowRoot!.childNodes[0]; + const iframe = contentDocument1.createElement('iframe'); + // append an iframe to shadow dom #1 + div.appendChild(iframe); + }); + + await waitForRAF(page); // wait till browser loaded contents of frame + + page.evaluate(() => { + const iframe: HTMLIFrameElement = document + .querySelector('iframe')! + .contentDocument!.body.shadowRoot!.querySelector('iframe')!; + + const contentDocument2 = iframe.contentDocument!; + // create shadow dom #2 in the iframe + contentDocument2.body.attachShadow({ mode: 'open' }); + contentDocument2.body.shadowRoot!.appendChild( + document.createElement('span'), + ); + }); + await waitForRAF(page); // wait for events to get created + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should record mutations in iframes accross pages', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto(`${serverURL}/html`); + page.on('console', (msg) => console.log(msg.text())); + await page.setContent(getHtml.call(this, 'frame2.html')); + + await page.waitForSelector('iframe'); // wait for iframe to get added + await waitForRAF(page); // wait for iframe to load + + page.evaluate((serverURL) => { + const iframe = document.querySelector('iframe')!; + iframe.setAttribute('src', `${serverURL}/html`); // load new page + }, serverURL); + + await page.waitForResponse(`${serverURL}/html`); // wait for iframe to load pt1 + await waitForRAF(page); // wait for iframe to load pt2 + + await page.evaluate(() => { + const iframeDocument = document.querySelector('iframe')!.contentDocument!; + const div = iframeDocument.createElement('div'); + iframeDocument.body.appendChild(div); + }); + + await waitForRAF(page); // wait for snapshot to be updated + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + // https://github.com/webcomponents/polyfills/tree/master/packages/shadydom + it('should record shadow doms polyfilled by shadydom', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + // insert shadydom script + replaceLast( + getHtml.call(this, 'polyfilled-shadowdom-mutation.html'), + '', + ` + + + + `, + ), + ); + await page.evaluate(() => { + const target3 = document.querySelector('#target3'); + target3?.attachShadow({ + mode: 'open', + }); + target3?.shadowRoot?.appendChild(document.createElement('span')); + }); + await waitForRAF(page); // wait till browser sent snapshots + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + // https://github.com/salesforce/lwc/tree/master/packages/%40lwc/synthetic-shadow + it('should record shadow doms polyfilled by synthetic-shadow', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + // insert lwc's synthetic-shadow script + replaceLast( + getHtml.call(this, 'polyfilled-shadowdom-mutation.html'), + '', + ` + + + + `, + ), + ); + await page.evaluate(() => { + const target3 = document.querySelector('#target3'); + // create a shadow dom with synthetic shadow + // https://github.com/salesforce/lwc/blob/v2.20.3/packages/@lwc/synthetic-shadow/src/faux-shadow/element.ts#L81-L87 + target3?.attachShadow({ + mode: 'open', + '$$lwc-synthetic-mode': true, + } as ShadowRootInit); + target3?.shadowRoot?.appendChild(document.createElement('span')); + const target4 = document.createElement('div'); + target4.id = 'target4'; + // create a native shadow dom + document.body.appendChild(target4); + target4.attachShadow({ + mode: 'open', + }); + target4.shadowRoot?.appendChild(document.createElement('ul')); + }); + await waitForRAF(page); // wait till browser sent snapshots + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should mask texts', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'mask-text.html', { + maskTextSelector: '[data-masking="true"]', + }), + ); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should mask texts using maskTextFn', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'mask-text.html', { + maskTextSelector: '[data-masking="true"]', + maskTextFn: (t: string) => t.replace(/[a-z]/g, '*'), + }), + ); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('can mask character data mutations', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'mutation-observer.html')); + + await page.evaluate(() => { + const li = document.createElement('li'); + const ul = document.querySelector('ul') as HTMLUListElement; + const p = document.querySelector('p') as HTMLParagraphElement; + [li, p].forEach((element) => { + element.className = 'highlight-mask'; + }); + ul.appendChild(li); + li.innerText = 'new list item'; + p.innerText = 'mutated'; + }); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); +}); diff --git a/test/machine.test.ts b/packages/rrweb/test/machine.test.ts similarity index 90% rename from test/machine.test.ts rename to packages/rrweb/test/machine.test.ts index 8e4e27fc..1260a5e6 100644 --- a/test/machine.test.ts +++ b/packages/rrweb/test/machine.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'chai'; import { discardPriorSnapshots } from '../src/replay/machine'; import { sampleEvents } from './utils'; import { EventType } from '../src/types'; @@ -17,7 +16,7 @@ const nextNextEvents = nextEvents.map((e) => ({ describe('get last session', () => { it('will return all the events when there is only one session', () => { - expect(discardPriorSnapshots(events, events[0].timestamp)).to.deep.equal(events); + expect(discardPriorSnapshots(events, events[0].timestamp)).toEqual(events); }); it('will return last session when there is more than one in the events', () => { @@ -27,7 +26,7 @@ describe('get last session', () => { multiple, nextNextEvents[nextNextEvents.length - 1].timestamp, ), - ).to.deep.equal(nextNextEvents); + ).toEqual(nextNextEvents); }); it('will return last session when baseline time is future time', () => { @@ -37,11 +36,11 @@ describe('get last session', () => { multiple, nextNextEvents[nextNextEvents.length - 1].timestamp + 1000, ), - ).to.deep.equal(nextNextEvents); + ).toEqual(nextNextEvents); }); it('will return all sessions when baseline time is prior time', () => { - expect(discardPriorSnapshots(events, events[0].timestamp - 1000)).to.deep.equal( + expect(discardPriorSnapshots(events, events[0].timestamp - 1000)).toEqual( events, ); }); diff --git a/test/packer.test.ts b/packages/rrweb/test/packer.test.ts similarity index 70% rename from test/packer.test.ts rename to packages/rrweb/test/packer.test.ts index 7b4dda54..08b35c6c 100644 --- a/test/packer.test.ts +++ b/packages/rrweb/test/packer.test.ts @@ -1,5 +1,3 @@ -import { expect } from 'chai'; -import { matchSnapshot } from './utils'; import { pack, unpack } from '../src/packer'; import { eventWithTime, EventType } from '../src/types'; import { MARK } from '../src/packer/base'; @@ -13,30 +11,36 @@ const event: eventWithTime = { describe('pack', () => { it('can pack event', () => { const packedData = pack(event); - const result = matchSnapshot(packedData, __filename, 'pack'); - expect(result.pass).to.true; + expect(packedData).toMatchSnapshot(); }); }); describe('unpack', () => { it('is compatible with unpacked data 1', () => { const result = unpack((event as unknown) as string); - expect(result).to.deep.equal(event); + expect(result).toEqual(event); }); it('is compatible with unpacked data 2', () => { const result = unpack(JSON.stringify(event)); - expect(result).to.deep.equal(event); + expect(result).toEqual(event); }); it('stop on unknown data format', () => { - expect(() => unpack('[""]')).to.throw(''); + const consoleSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + expect(() => unpack('[""]')).toThrow(''); + + expect(consoleSpy).toHaveBeenCalled(); + jest.resetAllMocks(); }); it('can unpack packed data', () => { const packedData = pack(event); const result = unpack(packedData); - expect(result).to.deep.equal({ + expect(result).toEqual({ ...event, v: MARK, }); diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts new file mode 100644 index 00000000..ba764b5e --- /dev/null +++ b/packages/rrweb/test/record.test.ts @@ -0,0 +1,635 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type * as puppeteer from 'puppeteer'; +import { + recordOptions, + listenerHandler, + eventWithTime, + EventType, + IncrementalSource, + styleSheetRuleData, +} from '../src/types'; +import { assertSnapshot, launchPuppeteer, waitForRAF } from './utils'; + +interface ISuite { + code: string; + browser: puppeteer.Browser; + page: puppeteer.Page; + events: eventWithTime[]; +} + +interface IWindow extends Window { + rrweb: { + record: ( + options: recordOptions, + ) => listenerHandler | undefined; + addCustomEvent(tag: string, payload: T): void; + }; + emit: (e: eventWithTime) => undefined; +} + +const setup = function (this: ISuite, content: string): ISuite { + const ctx = {} as ISuite; + + beforeAll(async () => { + ctx.browser = await launchPuppeteer({ + devtools: true, + }); + + const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); + // const bundlePath = path.resolve(__dirname, '../dist/rrweb-all.js'); + ctx.code = fs.readFileSync(bundlePath, 'utf8'); + }); + + beforeEach(async () => { + ctx.page = await ctx.browser.newPage(); + await ctx.page.goto('about:blank'); + await ctx.page.setContent(content); + await ctx.page.evaluate(ctx.code); + ctx.events = []; + await ctx.page.exposeFunction('emit', (e: eventWithTime) => { + if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) { + return; + } + ctx.events.push(e); + }); + + ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + }); + + afterEach(async () => { + // await ctx.page.waitForTimeout(60000); + await ctx.page.close(); + }); + + afterAll(async () => { + await ctx.browser.close(); + }); + + return ctx; +}; + +describe('record', function (this: ISuite) { + jest.setTimeout(180_000); + // jest.setTimeout(10_000); + + const ctx: ISuite = setup.call( + this, + ` + + + + + + + `, + ); + + it('will only have one full snapshot without checkout config', async () => { + await ctx.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + emit: ((window as unknown) as IWindow).emit, + }); + }); + let count = 30; + while (count--) { + await ctx.page.type('input', 'a'); + } + await ctx.page.waitForTimeout(10); + expect(ctx.events.length).toEqual(33); + expect( + ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta) + .length, + ).toEqual(1); + expect( + ctx.events.filter( + (event: eventWithTime) => event.type === EventType.FullSnapshot, + ).length, + ).toEqual(1); + }); + + it('can checkout full snapshot by count', async () => { + await ctx.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + emit: ((window as unknown) as IWindow).emit, + checkoutEveryNth: 10, + }); + }); + let count = 30; + while (count--) { + await ctx.page.type('input', 'a'); + } + await ctx.page.waitForTimeout(10); + expect(ctx.events.length).toEqual(39); + expect( + ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta) + .length, + ).toEqual(4); + expect( + ctx.events.filter( + (event: eventWithTime) => event.type === EventType.FullSnapshot, + ).length, + ).toEqual(4); + expect(ctx.events[1].type).toEqual(EventType.FullSnapshot); + expect(ctx.events[13].type).toEqual(EventType.FullSnapshot); + expect(ctx.events[25].type).toEqual(EventType.FullSnapshot); + expect(ctx.events[37].type).toEqual(EventType.FullSnapshot); + }); + + it('can checkout full snapshot by time', async () => { + await ctx.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + emit: ((window as unknown) as IWindow).emit, + checkoutEveryNms: 500, + }); + }); + await ctx.page.type('input', 'a'); + await ctx.page.waitForTimeout(300); + expect( + ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta) + .length, + ).toEqual(1); // before first automatic snapshot + expect( + ctx.events.filter( + (event: eventWithTime) => event.type === EventType.FullSnapshot, + ).length, + ).toEqual(1); // before first automatic snapshot + await ctx.page.waitForTimeout(200); + await ctx.page.type('input', 'a'); + await ctx.page.waitForTimeout(10); + expect( + ctx.events.filter((event: eventWithTime) => event.type === EventType.Meta) + .length, + ).toEqual(2); + expect( + ctx.events.filter( + (event: eventWithTime) => event.type === EventType.FullSnapshot, + ).length, + ).toEqual(2); + }); + + it('is safe to checkout during async callbacks', async () => { + await ctx.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + emit: ((window as unknown) as IWindow).emit, + checkoutEveryNth: 2, + }); + const p = document.createElement('p'); + const span = document.createElement('span'); + setTimeout(() => { + document.body.appendChild(p); + p.appendChild(span); + document.body.removeChild(document.querySelector('input')!); + }, 0); + setTimeout(() => { + span.innerText = 'test'; + }, 10); + setTimeout(() => { + p.removeChild(span); + document.body.appendChild(span); + }, 10); + }); + await ctx.page.waitForTimeout(100); + assertSnapshot(ctx.events); + }); + + it('should record scroll position', async () => { + await ctx.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + emit: ((window as unknown) as IWindow).emit, + }); + const p = document.createElement('p'); + p.innerText = 'testtesttesttesttesttesttesttesttesttest'; + p.setAttribute('style', 'overflow: auto; height: 1px; width: 1px;'); + document.body.appendChild(p); + p.scrollTop = 10; + p.scrollLeft = 10; + }); + await waitForRAF(ctx.page); + assertSnapshot(ctx.events); + }); + + it('can add custom event', async () => { + await ctx.page.evaluate(() => { + const { record, addCustomEvent } = ((window as unknown) as IWindow).rrweb; + record({ + emit: ((window as unknown) as IWindow).emit, + }); + addCustomEvent('tag1', 1); + addCustomEvent<{ a: string }>('tag2', { + a: 'b', + }); + }); + await ctx.page.waitForTimeout(50); + assertSnapshot(ctx.events); + }); + + it('captures stylesheet rules', 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.insertRule('body { background: #000; }'); + const ruleIdx1 = styleSheet.insertRule('body { background: #111; }'); + styleSheet.deleteRule(ruleIdx1); + // end: pre-serialization + setTimeout(() => { + styleSheet.insertRule('body { color: #fff; }'); + }, 0); + setTimeout(() => { + styleSheet.deleteRule(ruleIdx0); + }, 5); + setTimeout(() => { + styleSheet.insertRule('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([ + { + rule: 'body { color: #fff; }', + }, + ]); + expect(removeRuleCount).toEqual(1); + assertSnapshot(ctx.events); + }); + + const captureNestedStylesheetRulesTest = 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; + styleSheet.insertRule('@media {}'); + const atMediaRule = styleSheet.cssRules[0] as CSSMediaRule; + + const ruleIdx0 = atMediaRule.insertRule('body { background: #000; }', 0); + const ruleIdx1 = atMediaRule.insertRule('body { background: #111; }', 0); + atMediaRule.deleteRule(ruleIdx1); + setTimeout(() => { + atMediaRule.insertRule('body { color: #fff; }', 0); + }, 0); + setTimeout(() => { + atMediaRule.deleteRule(ruleIdx0); + }, 5); + setTimeout(() => { + atMediaRule.insertRule('body { color: #ccc; }', 0); + }, 10); + }); + await ctx.page.waitForTimeout(50); + const styleSheetRuleEvents = ctx.events.filter( + (e) => + e.type === EventType.IncrementalSnapshot && + e.data.source === IncrementalSource.StyleSheetRule, + ); + const addRuleCount = styleSheetRuleEvents.filter((e) => + Boolean((e.data as styleSheetRuleData).adds), + ).length; + const removeRuleCount = styleSheetRuleEvents.filter((e) => + Boolean((e.data as styleSheetRuleData).removes), + ).length; + // sync insert/delete should be ignored + expect(addRuleCount).toEqual(2); + expect(removeRuleCount).toEqual(1); + assertSnapshot(ctx.events); + }; + it('captures nested stylesheet rules', captureNestedStylesheetRulesTest); + + describe('without CSSGroupingRule support', () => { + // Safari currently doesn't support CSSGroupingRule, let's test without that + // https://caniuse.com/?search=CSSGroupingRule + beforeEach(async () => { + await ctx.page.evaluate(() => { + /* @ts-ignore: override CSSGroupingRule */ + CSSGroupingRule = undefined; + }); + // load a fresh rrweb recorder without CSSGroupingRule + await ctx.page.evaluate(ctx.code); + }); + it('captures nested stylesheet rules', captureNestedStylesheetRulesTest); + }); + + it('captures style property changes', 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; + styleSheet.insertRule('body { background: #000; }'); + setTimeout(() => { + (styleSheet.cssRules[0] as CSSStyleRule).style.setProperty( + 'color', + 'green', + ); + (styleSheet.cssRules[0] as CSSStyleRule).style.removeProperty( + 'background', + ); + }, 0); + }); + await ctx.page.waitForTimeout(50); + assertSnapshot(ctx.events); + }); + + it('captures inserted style text nodes correctly', async () => { + await ctx.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + + const styleEl = document.createElement(`style`); + styleEl.append(document.createTextNode('div { color: red; }')); + styleEl.append(document.createTextNode('section { color: blue; }')); + document.head.appendChild(styleEl); + + record({ + emit: ((window as unknown) as IWindow).emit, + }); + + styleEl.append(document.createTextNode('span { color: orange; }')); + styleEl.append(document.createTextNode('h1 { color: pink; }')); + }); + await waitForRAF(ctx.page); + assertSnapshot(ctx.events); + }); + + it('captures stylesheets with `blob:` url', async () => { + await ctx.page.evaluate(() => { + const link1 = document.createElement('link'); + link1.setAttribute('rel', 'stylesheet'); + link1.setAttribute( + 'href', + URL.createObjectURL( + new Blob(['body { color: pink; }'], { + type: 'text/css', + }), + ), + ); + document.head.appendChild(link1); + }); + await waitForRAF(ctx.page); + await ctx.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + + record({ + inlineStylesheet: true, + emit: ((window as unknown) as IWindow).emit, + }); + }); + await waitForRAF(ctx.page); + assertSnapshot(ctx.events); + }); + + it('captures stylesheets in iframes with `blob:` url', async () => { + await ctx.page.evaluate(() => { + const iframe = document.createElement('iframe'); + iframe.setAttribute('src', 'about:blank'); + document.body.appendChild(iframe); + + const linkEl = document.createElement('link'); + linkEl.setAttribute('rel', 'stylesheet'); + linkEl.setAttribute( + 'href', + URL.createObjectURL( + new Blob(['body { color: pink; }'], { + type: 'text/css', + }), + ), + ); + const iframeDoc = iframe.contentDocument!; + iframeDoc.head.appendChild(linkEl); + }); + await waitForRAF(ctx.page); + await ctx.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + + record({ + inlineStylesheet: true, + emit: ((window as unknown) as IWindow).emit, + }); + }); + await waitForRAF(ctx.page); + assertSnapshot(ctx.events); + }); + + it('captures stylesheets that are still loading', async () => { + await ctx.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + + record({ + inlineStylesheet: true, + emit: ((window as unknown) as IWindow).emit, + }); + + const link1 = document.createElement('link'); + link1.setAttribute('rel', 'stylesheet'); + link1.setAttribute( + 'href', + URL.createObjectURL( + new Blob(['body { color: pink; }'], { + type: 'text/css', + }), + ), + ); + document.head.appendChild(link1); + }); + + // `blob:` URLs are not available immediately, so we need to wait for the browser to load them + await waitForRAF(ctx.page); + + assertSnapshot(ctx.events); + }); + + it('captures stylesheets in iframes that are still loading', async () => { + await ctx.page.evaluate(() => { + const iframe = document.createElement('iframe'); + iframe.setAttribute('src', 'about:blank'); + document.body.appendChild(iframe); + const iframeDoc = iframe.contentDocument!; + + const { record } = ((window as unknown) as IWindow).rrweb; + + record({ + inlineStylesheet: true, + emit: ((window as unknown) as IWindow).emit, + }); + + const linkEl = document.createElement('link'); + linkEl.setAttribute('rel', 'stylesheet'); + linkEl.setAttribute( + 'href', + URL.createObjectURL( + new Blob(['body { color: pink; }'], { + type: 'text/css', + }), + ), + ); + iframeDoc.head.appendChild(linkEl); + }); + + // `blob:` URLs are not available immediately, so we need to wait for the browser to load them + await waitForRAF(ctx.page); + + assertSnapshot(ctx.events); + }); + + it('captures CORS stylesheets that are still loading', async () => { + const corsStylesheetURL = + 'https://cdn.jsdelivr.net/npm/pure@2.85.0/index.css'; + + // do not `await` the following function, otherwise `waitForResponse` _might_ not be called + void ctx.page.evaluate((corsStylesheetURL) => { + const { record } = ((window as unknown) as IWindow).rrweb; + + record({ + inlineStylesheet: true, + emit: ((window as unknown) as IWindow).emit, + }); + + const link1 = document.createElement('link'); + link1.setAttribute('rel', 'stylesheet'); + link1.setAttribute('href', corsStylesheetURL); + document.head.appendChild(link1); + }, corsStylesheetURL); + + await ctx.page.waitForResponse(corsStylesheetURL); // wait for stylesheet to be loaded + await waitForRAF(ctx.page); // wait for rrweb to emit events + + assertSnapshot(ctx.events); + }); +}); + +describe('record iframes', function (this: ISuite) { + jest.setTimeout(180_000); + + const ctx: ISuite = setup.call( + this, + ` + + + +