Compact style mutation (#464)
* Don't store the full style attribute change, as small mutations to single style properties result in storage of a rewrite for the full style attribute, which may be very large. Had an example of a website using http://schillmania.com/projects/snowstorm/ where many direct style changes were happening every second across many 'snowflake' elements, with each attribute change looking like: "style":"color: rgb(255, 255, 255); position: absolute; width: 8px; height: 8px; font-family: arial, verdana; overflow: hidden; font-weight: normal; z-index: 0; display: block; bottom: auto; opacity: 1; padding: 0px; margin: 0px; font-size: 10px; line-height: 10px; text-align: center; vertical-align: baseline; left: 242.807px; top: 85.7332px;" even though maybe just the left/top position had been changed * More compact storage for the much more common attribute value without an `!important` flag - saves 6 chars per style attr in the json :) * Fix bug: attributes weren't getting removed after changes to treatment of 'style' attributes
This commit is contained in:
@@ -449,6 +449,7 @@ export default class MutationBuffer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'attributes': {
|
case 'attributes': {
|
||||||
|
const target = (m.target as HTMLElement);
|
||||||
let value = (m.target as HTMLElement).getAttribute(m.attributeName!);
|
let value = (m.target as HTMLElement).getAttribute(m.attributeName!);
|
||||||
if (m.attributeName === 'value') {
|
if (m.attributeName === 'value') {
|
||||||
value = maskInputValue({
|
value = maskInputValue({
|
||||||
@@ -472,6 +473,34 @@ export default class MutationBuffer {
|
|||||||
};
|
};
|
||||||
this.attributes.push(item);
|
this.attributes.push(item);
|
||||||
}
|
}
|
||||||
|
if (m.attributeName === 'style') {
|
||||||
|
const old = this.doc.createElement('span');
|
||||||
|
old.setAttribute('style', m.oldValue);
|
||||||
|
if (item.attributes['style'] === undefined) {
|
||||||
|
item.attributes['style'] = {};
|
||||||
|
}
|
||||||
|
for (let i=0; i<target.style.length; i++) {
|
||||||
|
let pname = target.style[i];
|
||||||
|
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 == '') {
|
||||||
|
item.attributes['style'][pname] = newValue;
|
||||||
|
} else {
|
||||||
|
item.attributes['style'][pname] = [newValue, newPriority];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i=0; i<old.style.length; i++) {
|
||||||
|
let pname = old.style[i];
|
||||||
|
if (target.style.getPropertyValue(pname) === '' ||
|
||||||
|
!target.style.getPropertyValue(pname) // covering potential non-standard browsers
|
||||||
|
) {
|
||||||
|
item.attributes['style'][pname] = false; // delete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
// overwrite attribute if the mutations was triggered in same time
|
// overwrite attribute if the mutations was triggered in same time
|
||||||
item.attributes[m.attributeName!] = transformAttribute(
|
item.attributes[m.attributeName!] = transformAttribute(
|
||||||
this.doc,
|
this.doc,
|
||||||
@@ -479,6 +508,7 @@ export default class MutationBuffer {
|
|||||||
m.attributeName!,
|
m.attributeName!,
|
||||||
value!,
|
value!,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'childList': {
|
case 'childList': {
|
||||||
|
|||||||
@@ -1310,12 +1310,11 @@ export class Replayer {
|
|||||||
for (const attributeName in mutation.attributes) {
|
for (const attributeName in mutation.attributes) {
|
||||||
if (typeof attributeName === 'string') {
|
if (typeof attributeName === 'string') {
|
||||||
const value = mutation.attributes[attributeName];
|
const value = mutation.attributes[attributeName];
|
||||||
try {
|
if (value === null) {
|
||||||
if (value !== null) {
|
|
||||||
((target as Node) as Element).setAttribute(attributeName, value);
|
|
||||||
} else {
|
|
||||||
((target as Node) as Element).removeAttribute(attributeName);
|
((target as Node) as Element).removeAttribute(attributeName);
|
||||||
}
|
} else if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
((target as Node) as Element).setAttribute(attributeName, value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.config.showWarning) {
|
if (this.config.showWarning) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -1324,6 +1323,17 @@ export class Replayer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (attributeName === 'style') {
|
||||||
|
for (var s in value) {
|
||||||
|
if (value[s] === false) {
|
||||||
|
((target as Node) as Element).style.removeProperty(s);
|
||||||
|
} else if (Array.isArray(value[s])) {
|
||||||
|
((target as Node) as Element).style.setProperty(s, value[s][0], value[s][1]);
|
||||||
|
} else {
|
||||||
|
((target as Node) as Element).style.setProperty(s, value[s]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -294,16 +294,20 @@ export type textMutation = {
|
|||||||
value: string | null;
|
value: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type styleAttributeValue = {
|
||||||
|
[key:string]: [string, string] | string | false;
|
||||||
|
};
|
||||||
|
|
||||||
export type attributeCursor = {
|
export type attributeCursor = {
|
||||||
node: Node;
|
node: Node;
|
||||||
attributes: {
|
attributes: {
|
||||||
[key: string]: string | null;
|
[key: string]: string | styleAttributeValue | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type attributeMutation = {
|
export type attributeMutation = {
|
||||||
id: number;
|
id: number;
|
||||||
attributes: {
|
attributes: {
|
||||||
[key: string]: string | null;
|
[key: string]: string | styleAttributeValue | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7428,14 +7428,24 @@ exports[`select2 1`] = `
|
|||||||
\\"id\\": 36,
|
\\"id\\": 36,
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"id\\": \\"select2-drop\\",
|
\\"id\\": \\"select2-drop\\",
|
||||||
\\"style\\": \\"left: Npx; width: Npx; top: Npx; bottom: auto; display: block;\\",
|
\\"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\\"
|
\\"class\\": \\"select2-drop select2-display-none select2-with-searchbox select2-drop-active\\"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"id\\": 70,
|
\\"id\\": 70,
|
||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"style\\": \\"\\"
|
\\"style\\": {
|
||||||
|
\\"display\\": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -7850,6 +7860,28 @@ exports[`select2 1`] = `
|
|||||||
\\"type\\": 0,
|
\\"type\\": 0,
|
||||||
\\"id\\": 70
|
\\"id\\": 70
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 0,
|
||||||
|
\\"texts\\": [],
|
||||||
|
\\"attributes\\": [
|
||||||
|
{
|
||||||
|
\\"id\\": 36,
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"style\\": {
|
||||||
|
\\"color\\": [
|
||||||
|
\\"black\\",
|
||||||
|
\\"important\\"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"removes\\": [],
|
||||||
|
\\"adds\\": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -184,7 +184,8 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
|
|
||||||
// toggle the select box
|
// toggle the select box
|
||||||
await page.click('.select2-container', { clickCount: 2, delay: 100 });
|
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');
|
const snapshots = await page.evaluate('window.snapshots');
|
||||||
assertSnapshot(snapshots, __filename, 'select2');
|
assertSnapshot(snapshots, __filename, 'select2');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,14 +75,20 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
|
|||||||
s.data.source === IncrementalSource.Mutation
|
s.data.source === IncrementalSource.Mutation
|
||||||
) {
|
) {
|
||||||
s.data.attributes.forEach((a) => {
|
s.data.attributes.forEach((a) => {
|
||||||
if (
|
if ('style' in a.attributes && a.attributes.style && typeof a.attributes.style === 'object') {
|
||||||
'style' in a.attributes &&
|
for (const [k, v] of Object.entries(a.attributes.style)) {
|
||||||
coordinatesReg.test(a.attributes.style!)
|
if (Array.isArray(v)) {
|
||||||
) {
|
if (coordinatesReg.test(k + ': ' + v[0])) {
|
||||||
a.attributes.style = a.attributes.style!.replace(
|
// TODO: could round the number here instead depending on what's coming out of various test envs
|
||||||
coordinatesReg,
|
a.attributes.style[k] = ['Npx', v[1]];
|
||||||
'$1: Npx',
|
}
|
||||||
);
|
} else if (typeof v === 'string') {
|
||||||
|
if (coordinatesReg.test(k + ': ' + v)) {
|
||||||
|
a.attributes.style[k] = 'Npx';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
s.data.adds.forEach((add) => {
|
s.data.adds.forEach((add) => {
|
||||||
@@ -97,6 +103,7 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
|
|||||||
'$1: Npx',
|
'$1: Npx',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
delete s.timestamp;
|
delete s.timestamp;
|
||||||
|
|||||||
Reference in New Issue
Block a user