Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion packages/rrweb-snapshot/src/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
tagMap,
elementNode,
BuildCache,
attributes,
} from './types';
import { isElement, Mirror } from './utils';

Expand Down Expand Up @@ -116,6 +117,31 @@ export function createCache(): BuildCache {
};
}

/**
* `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: {
Expand All @@ -142,7 +168,7 @@ function buildNode(
} else {
node = doc.createElement(tagName);
}
for (const name in n.attributes) {
for (const name in sortAttributes(n.attributes)) {
if (!Object.prototype.hasOwnProperty.call(n.attributes, name)) {
continue;
}
Expand Down
183 changes: 109 additions & 74 deletions packages/rrweb-snapshot/test/rebuild.test.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,127 @@
/**
* @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 }',
);
});

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));
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));
});
});
});