bugfix: Sort attributes to make rr_* attributes handled last (#970)

* Sort attributes to make `rr_*` attributes handled last

`rr_dataURL` overwrites `src` attribute, because of this we need to evaluate `rr_dataURL` last so it doesn't accidentally get overwritten again.

* Update packages/rrweb-snapshot/src/rebuild.ts

* Refactor handling of rr_* attributes

Be a little more strict when it comes attribute types
This commit is contained in:
Justin Halsall
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 81ed51c1e0
commit 65ff0efefd
4 changed files with 254 additions and 182 deletions

View File

@@ -1,93 +1,128 @@
/**
* @jest-environment jsdom
*/
import * as fs from 'fs';
import * as path from 'path';
import { addHoverClass, createCache } from '../src/rebuild';
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('add hover class to hover selector related rules', function () {
describe('rebuild', function () {
let cache: ReturnType<typeof createCache>;
let mirror: Mirror;
beforeEach(() => {
mirror = createMirror();
cache = createCache();
});
it('will do nothing to css text without :hover', () => {
const cssText = 'body { color: white }';
expect(addHoverClass(cssText, cache)).toEqual(cssText);
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);
});
});
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 }',
);
});
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 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 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 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 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 :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 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 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('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('will ignore :hover in css value', () => {
const cssText = '.a::after { content: ":hover" }';
expect(addHoverClass(cssText, cache)).toEqual(cssText);
});
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 }',
);
});
// this benchmark is unreliable when run in parallel with other tests
it.skip('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('will ignore :hover in css value', () => {
const cssText = '.a::after { content: ":hover" }';
expect(addHoverClass(cssText, cache)).toEqual(cssText);
});
it('should be a lot faster to add a hover class to a previously processed css string', () => {
const factor = 100;
// this benchmark is unreliable when run in parallel with other tests
it.skip('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);
});
let cssText = fs.readFileSync(
path.resolve(__dirname, './css/benchmark.css'),
'utf8',
);
it('should be a lot faster to add a hover class to a previously processed css string', () => {
const factor = 100;
const start = process.hrtime();
addHoverClass(cssText, cache);
const end = process.hrtime(start);
let cssText = fs.readFileSync(
path.resolve(__dirname, './css/benchmark.css'),
'utf8',
);
const cachedStart = process.hrtime();
addHoverClass(cssText, cache);
const cachedEnd = process.hrtime(cachedStart);
const start = process.hrtime();
addHoverClass(cssText, cache);
const end = process.hrtime(start);
expect(getDuration(cachedEnd) * factor).toBeLessThan(getDuration(end));
const cachedStart = process.hrtime();
addHoverClass(cssText, cache);
const cachedEnd = process.hrtime(cachedStart);
expect(getDuration(cachedEnd) * factor).toBeLessThan(getDuration(end));
});
});
});