From 0c62d31002f376458f52cc3a3f8faf3d8843a5ad Mon Sep 17 00:00:00 2001 From: Karl-Aksel Puulmann Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] Allow blocking elements by selector (#50) * Extract method (isElementBlocked) and add tests * Add blockSelector argument to snapshot If blockSelector is passed, it will be matched against the element. Reasoning: Mutating class names can get messy, so providing another hook helps keep code clean by using data-attributes instead. --- package.json | 2 ++ src/snapshot.ts | 40 ++++++++++++++++++++++++++++++---------- test/snapshot.test.ts | 27 ++++++++++++++++++++++++++- typings/snapshot.d.ts | 5 +++-- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 94adce4f..0c4b1f5f 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,14 @@ "homepage": "https://github.com/rrweb-io/rrweb-snapshot#readme", "devDependencies": { "@types/chai": "^4.1.4", + "@types/jsdom": "^16.2.4", "@types/mocha": "^5.2.5", "@types/node": "^10.11.3", "@types/puppeteer": "^1.12.4", "chai": "^4.1.2", "cross-env": "^5.2.0", "jest-snapshot": "^23.6.0", + "jsdom": "^16.4.0", "mocha": "^5.2.0", "puppeteer": "^1.15.0", "rollup": "^0.66.4", diff --git a/src/snapshot.ts b/src/snapshot.ts index c3190794..9db891ba 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -162,10 +162,34 @@ export function transformAttribute( } } +export function _isBlockedElement( + element: HTMLElement, + blockClass: string | RegExp, + blockSelector: string | null, +): boolean { + if (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; + } + } else { + element.classList.forEach((className) => { + if (blockClass.test(className)) { + return true; + } + }); + } + if (blockSelector) { + return element.matches(blockSelector) + } + + return false; +} + function serializeNode( n: Node, doc: Document, blockClass: string | RegExp, + blockSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions = {}, recordCanvas: boolean, @@ -184,16 +208,7 @@ function serializeNode( systemId: (n as DocumentType).systemId, }; case n.ELEMENT_NODE: - let needBlock = false; - if (typeof blockClass === 'string') { - needBlock = (n as HTMLElement).classList.contains(blockClass); - } else { - (n as HTMLElement).classList.forEach((className) => { - if (blockClass.test(className)) { - needBlock = true; - } - }); - } + const needBlock = _isBlockedElement(n as HTMLElement, blockClass, blockSelector); const tagName = getValidTagName((n as HTMLElement).tagName); let attributes: attributes = {}; for (const { name, value } of Array.from((n as HTMLElement).attributes)) { @@ -406,6 +421,7 @@ export function serializeNodeWithId( doc: Document, map: idNodeMap, blockClass: string | RegExp, + blockSelector: string | null, skipChild = false, inlineStylesheet = true, maskInputOptions?: MaskInputOptions, @@ -417,6 +433,7 @@ export function serializeNodeWithId( n, doc, blockClass, + blockSelector, inlineStylesheet, maskInputOptions, recordCanvas || false, @@ -472,6 +489,7 @@ export function serializeNodeWithId( doc, map, blockClass, + blockSelector, skipChild, inlineStylesheet, maskInputOptions, @@ -494,6 +512,7 @@ function snapshot( maskAllInputsOrOptions: boolean | MaskInputOptions, slimDOMSensibleOrOptions: boolean | SlimDOMOptions, recordCanvas?: boolean, + blockSelector: string | null = null, ): [serializedNodeWithId | null, idNodeMap] { const idNodeMap: idNodeMap = {}; const maskInputOptions: MaskInputOptions = @@ -543,6 +562,7 @@ function snapshot( n, idNodeMap, blockClass, + blockSelector, false, inlineStylesheet, maskInputOptions, diff --git a/test/snapshot.test.ts b/test/snapshot.test.ts index 2116813a..5c3000d0 100644 --- a/test/snapshot.test.ts +++ b/test/snapshot.test.ts @@ -1,6 +1,7 @@ import 'mocha'; +import { JSDOM } from 'jsdom'; import { expect } from 'chai'; -import { absoluteToStylesheet } from '../src/snapshot'; +import { absoluteToStylesheet, _isBlockedElement } from '../src/snapshot'; describe('absolute url to stylesheet', () => { const href = 'http://localhost/css/style.css'; @@ -83,3 +84,27 @@ describe('absolute url to stylesheet', () => { expect(absoluteToStylesheet(`url('')`, href)).to.equal(`url('')`); }); }); + +describe('isBlockedElement()', () => { + const subject = (html: string, opt: any = {}) => + _isBlockedElement(render(html), 'rr-block', opt.blockSelector) + + const render = (html: string): HTMLElement => + JSDOM.fragment(html).querySelector('div')! + + it('can handle empty elements', () => { + expect(subject('
')).to.equal(false) + }) + + it('blocks prohibited className', () => { + expect(subject('
')).to.equal(true) + }) + + it('does not block random data selector', () => { + expect(subject('
')).to.equal(false) + }) + + it('blocks blocked selector', () => { + expect(subject('
', { blockSelector: '[data-rr-block]' })).to.equal(true) + }) +}) diff --git a/typings/snapshot.d.ts b/typings/snapshot.d.ts index 988f7982..d99ef4d6 100644 --- a/typings/snapshot.d.ts +++ b/typings/snapshot.d.ts @@ -3,8 +3,9 @@ export declare const IGNORED_NODE = -2; export declare function absoluteToStylesheet(cssText: string | null, href: string): string; export declare function absoluteToDoc(doc: Document, attributeValue: string): string; export declare function transformAttribute(doc: Document, name: string, value: string): string; -export declare function serializeNodeWithId(n: Node | INode, doc: Document, map: idNodeMap, blockClass: string | RegExp, skipChild?: boolean, inlineStylesheet?: boolean, maskInputOptions?: MaskInputOptions, slimDOMOptions?: SlimDOMOptions, recordCanvas?: boolean, preserveWhiteSpace?: boolean): serializedNodeWithId | null; -declare function snapshot(n: Document, blockClass: string | RegExp | undefined, inlineStylesheet: boolean | undefined, maskAllInputsOrOptions: boolean | MaskInputOptions, slimDOMSensibleOrOptions: boolean | SlimDOMOptions, recordCanvas?: boolean): [serializedNodeWithId | null, idNodeMap]; +export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null): boolean; +export declare function serializeNodeWithId(n: Node | INode, doc: Document, map: idNodeMap, blockClass: string | RegExp, blockSelector: string | null, skipChild?: boolean, inlineStylesheet?: boolean, maskInputOptions?: MaskInputOptions, slimDOMOptions?: SlimDOMOptions, recordCanvas?: boolean, preserveWhiteSpace?: boolean): serializedNodeWithId | null; +declare function snapshot(n: Document, blockClass: string | RegExp | undefined, inlineStylesheet: boolean | undefined, maskAllInputsOrOptions: boolean | MaskInputOptions, slimDOMSensibleOrOptions: boolean | SlimDOMOptions, recordCanvas?: boolean, blockSelector?: string | null): [serializedNodeWithId | null, idNodeMap]; export declare function visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void; export declare function cleanupSnapshot(): void; export default snapshot;