Fix adapt css with split (#1600)
Fix for #1575 where postcss was raising an exception * adapt the entire CSS as a whole in one pass with postcss, rather than adapting each split part separately * break up the postcss output again and assign to individual text nodes (kind of inverse of splitCssText at record side) * impose an upper bound of 30 iterations on the substring searches to preempt possible pathological behavior * add tests to demonstrate the scenario and prevent regression More technical details: * Fix algorithm; checks against `ix_end` within loop were incorrect when `ix_start` was bigger than zero. * Fix that length check against wrong array was causing 'should record style mutations with multiple child nodes and replay them correctly' test to fail. Note on last point: I haven't looked into things more deeply than that the test was complaining about missing .length after `replayer.pause(1000);`
This commit is contained in:
6
.changeset/fix-adapt-css.md
Normal file
6
.changeset/fix-adapt-css.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"rrweb": patch
|
||||||
|
"rrweb-snapshot": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
#1575 Fix that postcss could fall over when trying to process css content split arbitrarily
|
||||||
@@ -102,15 +102,44 @@ export function applyCssSplits(
|
|||||||
// unexpected: remerge the last two so that we don't discard any css
|
// unexpected: remerge the last two so that we don't discard any css
|
||||||
cssTextSplits.splice(-2, 2, cssTextSplits.slice(-2).join(''));
|
cssTextSplits.splice(-2, 2, cssTextSplits.slice(-2).join(''));
|
||||||
}
|
}
|
||||||
|
let adaptedCss = '';
|
||||||
|
if (hackCss) {
|
||||||
|
adaptedCss = adaptCssForReplay(cssTextSplits.join(''), cache);
|
||||||
|
}
|
||||||
|
let startIndex = 0;
|
||||||
for (let i = 0; i < childTextNodes.length; i++) {
|
for (let i = 0; i < childTextNodes.length; i++) {
|
||||||
|
if (i === cssTextSplits.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
const childTextNode = childTextNodes[i];
|
const childTextNode = childTextNodes[i];
|
||||||
const cssTextSection = cssTextSplits[i];
|
if (!hackCss) {
|
||||||
if (childTextNode && cssTextSection) {
|
childTextNode.textContent = cssTextSplits[i];
|
||||||
// id will be assigned when these child nodes are
|
} else if (i < cssTextSplits.length - 1) {
|
||||||
// iterated over in buildNodeWithSN
|
let endIndex = startIndex;
|
||||||
childTextNode.textContent = hackCss
|
let endSearch = cssTextSplits[i + 1].length;
|
||||||
? adaptCssForReplay(cssTextSection, cache)
|
|
||||||
: cssTextSection;
|
// don't do hundreds of searches, in case a mismatch
|
||||||
|
// is caused close to start of string
|
||||||
|
endSearch = Math.min(endSearch, 30);
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
for (; endSearch > 2; endSearch--) {
|
||||||
|
const searchBit = cssTextSplits[i + 1].substring(0, endSearch);
|
||||||
|
const searchIndex = adaptedCss.substring(startIndex).indexOf(searchBit);
|
||||||
|
found = searchIndex !== -1;
|
||||||
|
if (found) {
|
||||||
|
endIndex += searchIndex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
// something went wrong, put a similar sized chunk in the right place
|
||||||
|
endIndex += cssTextSplits[i].length;
|
||||||
|
}
|
||||||
|
childTextNode.textContent = adaptedCss.substring(startIndex, endIndex);
|
||||||
|
startIndex = endIndex;
|
||||||
|
} else {
|
||||||
|
childTextNode.textContent = adaptedCss.substring(startIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,6 +289,52 @@ describe('applyCssSplits css rejoiner', function () {
|
|||||||
expect((sn3.childNodes[2] as textNode).textContent).toEqual('');
|
expect((sn3.childNodes[2] as textNode).textContent).toEqual('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies css splits correctly when split parts are invalid by themselves', () => {
|
||||||
|
const badFirstHalf = 'a:hov';
|
||||||
|
const badSecondHalf = 'er { color: red; }';
|
||||||
|
const markedCssText = [badFirstHalf, badSecondHalf].join('/* rr_split */');
|
||||||
|
applyCssSplits(sn, markedCssText, true, mockLastUnusedArg);
|
||||||
|
expect(
|
||||||
|
(sn.childNodes[0] as textNode).textContent +
|
||||||
|
(sn.childNodes[1] as textNode).textContent,
|
||||||
|
).toEqual('a:hover,\na.\\:hover { color: red; }');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies css splits correctly when split parts are invalid by themselves x3', () => {
|
||||||
|
let sn3 = {
|
||||||
|
type: NodeType.Element,
|
||||||
|
tagName: 'style',
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
type: NodeType.Text,
|
||||||
|
textContent: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NodeType.Text,
|
||||||
|
textContent: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NodeType.Text,
|
||||||
|
textContent: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as serializedElementNodeWithId;
|
||||||
|
const badStartThird = '.a:hover { background-color';
|
||||||
|
const badMidThird = ': red; } input:hover {';
|
||||||
|
const badEndThird = 'border: 1px solid purple; }';
|
||||||
|
const markedCssText = [badStartThird, badMidThird, badEndThird].join(
|
||||||
|
'/* rr_split */',
|
||||||
|
);
|
||||||
|
applyCssSplits(sn3, markedCssText, true, mockLastUnusedArg);
|
||||||
|
expect((sn3.childNodes[0] as textNode).textContent).toEqual(
|
||||||
|
badStartThird.replace('.a:hover', '.a:hover,\n.a.\\:hover'),
|
||||||
|
);
|
||||||
|
expect((sn3.childNodes[1] as textNode).textContent).toEqual(
|
||||||
|
badMidThird.replace('input:hover', 'input:hover,\ninput.\\:hover'),
|
||||||
|
);
|
||||||
|
expect((sn3.childNodes[2] as textNode).textContent).toEqual(badEndThird);
|
||||||
|
});
|
||||||
|
|
||||||
it('maintains entire css text when there are too few child nodes', () => {
|
it('maintains entire css text when there are too few child nodes', () => {
|
||||||
let sn1 = {
|
let sn1 = {
|
||||||
type: NodeType.Element,
|
type: NodeType.Element,
|
||||||
|
|||||||
Reference in New Issue
Block a user