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:
@@ -5,6 +5,8 @@ import {
|
|||||||
tagMap,
|
tagMap,
|
||||||
elementNode,
|
elementNode,
|
||||||
BuildCache,
|
BuildCache,
|
||||||
|
attributes,
|
||||||
|
legacyAttributes,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { isElement, Mirror } from './utils';
|
import { isElement, Mirror } from './utils';
|
||||||
|
|
||||||
@@ -142,137 +144,163 @@ function buildNode(
|
|||||||
} else {
|
} else {
|
||||||
node = doc.createElement(tagName);
|
node = doc.createElement(tagName);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Attribute names start with `rr_` are internal attributes added by rrweb.
|
||||||
|
* They often overwrite other attributes on the element.
|
||||||
|
* We need to parse them last so they can overwrite conflicting attributes.
|
||||||
|
*/
|
||||||
|
const specialAttributes: attributes = {};
|
||||||
for (const name in n.attributes) {
|
for (const name in n.attributes) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(n.attributes, name)) {
|
if (!Object.prototype.hasOwnProperty.call(n.attributes, name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let value = n.attributes[name];
|
let value = n.attributes[name];
|
||||||
if (tagName === 'option' && name === 'selected' && value === false) {
|
if (
|
||||||
|
tagName === 'option' &&
|
||||||
|
name === 'selected' &&
|
||||||
|
(value as legacyAttributes[typeof name]) === false
|
||||||
|
) {
|
||||||
// legacy fix (TODO: if `value === false` can be generated for other attrs,
|
// legacy fix (TODO: if `value === false` can be generated for other attrs,
|
||||||
// should we also omit those other attrs from build ?)
|
// should we also omit those other attrs from build ?)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
value =
|
|
||||||
typeof value === 'boolean' || typeof value === 'number' ? '' : value;
|
/**
|
||||||
// attribute names start with rr_ are internal attributes added by rrweb
|
* Boolean attributes are considered to be true if they're present on the element at all.
|
||||||
if (!name.startsWith('rr_')) {
|
* We should set value to the empty string ("") or the attribute's name, with no leading or trailing whitespace.
|
||||||
const isTextarea = tagName === 'textarea' && name === 'value';
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute#parameters
|
||||||
const isRemoteOrDynamicCss =
|
*/
|
||||||
tagName === 'style' && name === '_cssText';
|
if (value === true) value = '';
|
||||||
if (isRemoteOrDynamicCss && hackCss) {
|
|
||||||
value = addHoverClass(value, cache);
|
if (name.startsWith('rr_')) {
|
||||||
}
|
specialAttributes[name] = value;
|
||||||
if (isTextarea || isRemoteOrDynamicCss) {
|
continue;
|
||||||
const child = doc.createTextNode(value);
|
}
|
||||||
// https://github.com/rrweb-io/rrweb/issues/112
|
|
||||||
for (const c of Array.from(node.childNodes)) {
|
const isTextarea = tagName === 'textarea' && name === 'value';
|
||||||
if (c.nodeType === node.TEXT_NODE) {
|
const isRemoteOrDynamicCss = tagName === 'style' && name === '_cssText';
|
||||||
node.removeChild(c);
|
if (isRemoteOrDynamicCss && hackCss && typeof value === 'string') {
|
||||||
}
|
value = addHoverClass(value, cache);
|
||||||
|
}
|
||||||
|
if ((isTextarea || isRemoteOrDynamicCss) && typeof value === 'string') {
|
||||||
|
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);
|
}
|
||||||
|
node.appendChild(child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (n.isSVG && name === 'xlink:href') {
|
||||||
|
node.setAttributeNS(
|
||||||
|
'http://www.w3.org/1999/xlink',
|
||||||
|
name,
|
||||||
|
value.toString(),
|
||||||
|
);
|
||||||
|
} 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.toString());
|
||||||
|
} 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.toString());
|
||||||
continue;
|
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.toString());
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// skip invalid attribute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
for (const name in specialAttributes) {
|
||||||
if (n.isSVG && name === 'xlink:href') {
|
const value = specialAttributes[name];
|
||||||
node.setAttributeNS('http://www.w3.org/1999/xlink', name, value);
|
// handle internal attributes
|
||||||
} else if (
|
if (tagName === 'canvas' && name === 'rr_dataURL') {
|
||||||
name === 'onload' ||
|
const image = document.createElement('img');
|
||||||
name === 'onclick' ||
|
image.onload = () => {
|
||||||
name.substring(0, 7) === 'onmouse'
|
const ctx = (node as HTMLCanvasElement).getContext('2d');
|
||||||
) {
|
if (ctx) {
|
||||||
// Rename some of the more common atttributes from https://www.w3schools.com/tags/ref_eventattributes.asp
|
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||||
// 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.src = value.toString();
|
||||||
|
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.toString();
|
||||||
|
} 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.toString();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'rr_width') {
|
if (name === 'rr_width') {
|
||||||
(node as HTMLElement).style.width = value;
|
(node as HTMLElement).style.width = value.toString();
|
||||||
} else if (name === 'rr_height') {
|
} else if (name === 'rr_height') {
|
||||||
(node as HTMLElement).style.height = value;
|
(node as HTMLElement).style.height = value.toString();
|
||||||
} else if (name === 'rr_mediaCurrentTime') {
|
} else if (
|
||||||
(node as HTMLMediaElement).currentTime = n.attributes
|
name === 'rr_mediaCurrentTime' &&
|
||||||
.rr_mediaCurrentTime as number;
|
typeof value === 'number'
|
||||||
} else if (name === 'rr_mediaState') {
|
) {
|
||||||
switch (value) {
|
(node as HTMLMediaElement).currentTime = value;
|
||||||
case 'played':
|
} else if (name === 'rr_mediaState') {
|
||||||
(node as HTMLMediaElement)
|
switch (value) {
|
||||||
.play()
|
case 'played':
|
||||||
.catch((e) => console.warn('media playback error', e));
|
(node as HTMLMediaElement)
|
||||||
break;
|
.play()
|
||||||
case 'paused':
|
.catch((e) => console.warn('media playback error', e));
|
||||||
(node as HTMLMediaElement).pause();
|
break;
|
||||||
break;
|
case 'paused':
|
||||||
default:
|
(node as HTMLMediaElement).pause();
|
||||||
}
|
break;
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -676,6 +676,7 @@ function serializeElementNode(
|
|||||||
// form fields
|
// form fields
|
||||||
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
||||||
const value = (n as HTMLInputElement | HTMLTextAreaElement).value;
|
const value = (n as HTMLInputElement | HTMLTextAreaElement).value;
|
||||||
|
const checked = (n as HTMLInputElement).checked;
|
||||||
if (
|
if (
|
||||||
attributes.type !== 'radio' &&
|
attributes.type !== 'radio' &&
|
||||||
attributes.type !== 'checkbox' &&
|
attributes.type !== 'checkbox' &&
|
||||||
@@ -690,8 +691,8 @@ function serializeElementNode(
|
|||||||
maskInputOptions,
|
maskInputOptions,
|
||||||
maskInputFn,
|
maskInputFn,
|
||||||
});
|
});
|
||||||
} else if ((n as HTMLInputElement).checked) {
|
} else if (checked) {
|
||||||
attributes.checked = (n as HTMLInputElement).checked;
|
attributes.checked = checked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tagName === 'option') {
|
if (tagName === 'option') {
|
||||||
|
|||||||
@@ -21,8 +21,16 @@ export type documentTypeNode = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type attributes = {
|
export type attributes = {
|
||||||
[key: string]: string | number | boolean;
|
[key: string]: string | number | true;
|
||||||
};
|
};
|
||||||
|
export type legacyAttributes = {
|
||||||
|
/**
|
||||||
|
* @deprecated old bug in rrweb was causing these to always be set
|
||||||
|
* @see https://github.com/rrweb-io/rrweb/pull/651
|
||||||
|
*/
|
||||||
|
selected: false;
|
||||||
|
};
|
||||||
|
|
||||||
export type elementNode = {
|
export type elementNode = {
|
||||||
type: NodeType.Element;
|
type: NodeType.Element;
|
||||||
tagName: string;
|
tagName: string;
|
||||||
|
|||||||
@@ -1,93 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
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]) {
|
function getDuration(hrtime: [number, number]) {
|
||||||
const [seconds, nanoseconds] = hrtime;
|
const [seconds, nanoseconds] = hrtime;
|
||||||
return seconds * 1000 + nanoseconds / 1000000;
|
return seconds * 1000 + nanoseconds / 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('add hover class to hover selector related rules', function () {
|
describe('rebuild', function () {
|
||||||
let cache: ReturnType<typeof createCache>;
|
let cache: ReturnType<typeof createCache>;
|
||||||
|
let mirror: Mirror;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mirror = createMirror();
|
||||||
cache = createCache();
|
cache = createCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will do nothing to css text without :hover', () => {
|
describe('rr_dataURL', function () {
|
||||||
const cssText = 'body { color: white }';
|
it('should rebuild dataURL', function () {
|
||||||
expect(addHoverClass(cssText, cache)).toEqual(cssText);
|
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', () => {
|
describe('add hover class to hover selector related rules', function () {
|
||||||
const cssText = '.a:hover { color: white }';
|
it('will do nothing to css text without :hover', () => {
|
||||||
expect(addHoverClass(cssText, cache)).toEqual(
|
const cssText = 'body { color: white }';
|
||||||
'.a:hover, .a.\\:hover { color: white }',
|
expect(addHoverClass(cssText, cache)).toEqual(cssText);
|
||||||
);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('can add hover class when there is multi selector', () => {
|
it('can add hover class to css text', () => {
|
||||||
const cssText = '.a, .b:hover, .c { color: white }';
|
const cssText = '.a:hover { color: white }';
|
||||||
expect(addHoverClass(cssText, cache)).toEqual(
|
expect(addHoverClass(cssText, cache)).toEqual(
|
||||||
'.a, .b:hover, .b.\\:hover, .c { color: white }',
|
'.a:hover, .a.\\:hover { color: white }',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can add hover class when there is a multi selector with the same prefix', () => {
|
it('can add hover class when there is multi selector', () => {
|
||||||
const cssText = '.a:hover, .a:hover::after { color: white }';
|
const cssText = '.a, .b:hover, .c { color: white }';
|
||||||
expect(addHoverClass(cssText, cache)).toEqual(
|
expect(addHoverClass(cssText, cache)).toEqual(
|
||||||
'.a:hover, .a.\\:hover, .a:hover::after, .a.\\:hover::after { color: white }',
|
'.a, .b:hover, .b.\\:hover, .c { color: white }',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can add hover class when :hover is not the end of selector', () => {
|
it('can add hover class when there is a multi selector with the same prefix', () => {
|
||||||
const cssText = 'div:hover::after { color: white }';
|
const cssText = '.a:hover, .a:hover::after { color: white }';
|
||||||
expect(addHoverClass(cssText, cache)).toEqual(
|
expect(addHoverClass(cssText, cache)).toEqual(
|
||||||
'div:hover::after, div.\\:hover::after { color: white }',
|
'.a:hover, .a.\\:hover, .a:hover::after, .a.\\:hover::after { color: white }',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can add hover class when the selector has multi :hover', () => {
|
it('can add hover class when :hover is not the end of selector', () => {
|
||||||
const cssText = 'a:hover b:hover { color: white }';
|
const cssText = 'div:hover::after { color: white }';
|
||||||
expect(addHoverClass(cssText, cache)).toEqual(
|
expect(addHoverClass(cssText, cache)).toEqual(
|
||||||
'a:hover b:hover, a.\\:hover b.\\:hover { color: white }',
|
'div:hover::after, div.\\:hover::after { color: white }',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will ignore :hover in css value', () => {
|
it('can add hover class when the selector has multi :hover', () => {
|
||||||
const cssText = '.a::after { content: ":hover" }';
|
const cssText = 'a:hover b:hover { color: white }';
|
||||||
expect(addHoverClass(cssText, cache)).toEqual(cssText);
|
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('will ignore :hover in css value', () => {
|
||||||
it.skip('benchmark', () => {
|
const cssText = '.a::after { content: ":hover" }';
|
||||||
const cssText = fs.readFileSync(
|
expect(addHoverClass(cssText, cache)).toEqual(cssText);
|
||||||
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', () => {
|
// this benchmark is unreliable when run in parallel with other tests
|
||||||
const factor = 100;
|
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(
|
it('should be a lot faster to add a hover class to a previously processed css string', () => {
|
||||||
path.resolve(__dirname, './css/benchmark.css'),
|
const factor = 100;
|
||||||
'utf8',
|
|
||||||
);
|
|
||||||
|
|
||||||
const start = process.hrtime();
|
let cssText = fs.readFileSync(
|
||||||
addHoverClass(cssText, cache);
|
path.resolve(__dirname, './css/benchmark.css'),
|
||||||
const end = process.hrtime(start);
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
const cachedStart = process.hrtime();
|
const start = process.hrtime();
|
||||||
addHoverClass(cssText, cache);
|
addHoverClass(cssText, cache);
|
||||||
const cachedEnd = process.hrtime(cachedStart);
|
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));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user