From d350da8552d8616dd118ee550bdfbce082986562 Mon Sep 17 00:00:00 2001 From: Jeff Nguyen Date: Thu, 25 Jul 2024 10:25:00 -0400 Subject: [PATCH] fix: nested stylesheets should have absolute URLs (#1533) * Replace relative URLs with absolute URLs when stringifying stylesheets * Add test to show desired behavior for imported stylesheets from seperate directory * Rename `absoluteToStylesheet` to `absolutifyURLs` and call it once after stringifying imported stylesheet * Don't create the intermediary array of the spread operator * Formalize that `stringifyRule` should expect a sheet href * Ensure a + " `; @@ -500,7 +500,8 @@ exports[`integration tests > [html file]: with-style-sheet-with-import.html 1`] with style sheet with import - + + " `; diff --git a/packages/rrweb-snapshot/test/alt-css/alt-style.css b/packages/rrweb-snapshot/test/alt-css/alt-style.css new file mode 100644 index 0000000000..bda88053d8 --- /dev/null +++ b/packages/rrweb-snapshot/test/alt-css/alt-style.css @@ -0,0 +1,12 @@ +body { + margin: 0; + background: url('../should-be-in-root-folder.jpg'); + border-image: url('data:image/svg+xml;utf8,'); + } + p { + color: red; + background: url('./should-be-in-alt-css-folder.jpg'); + } + body > p { + color: yellow; + } diff --git a/packages/rrweb-snapshot/test/css/style-with-import.css b/packages/rrweb-snapshot/test/css/style-with-import.css index 5fa59d8039..a24d901947 100644 --- a/packages/rrweb-snapshot/test/css/style-with-import.css +++ b/packages/rrweb-snapshot/test/css/style-with-import.css @@ -1,2 +1,3 @@ @import '//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&family=Roboto:wght@100;300;400;500;700&display=swap"'; @import './style.css'; +@import '../alt-css/alt-style.css'; diff --git a/packages/rrweb-snapshot/test/css/style.css b/packages/rrweb-snapshot/test/css/style.css index 2b3faf2a77..29b1da8ec8 100644 --- a/packages/rrweb-snapshot/test/css/style.css +++ b/packages/rrweb-snapshot/test/css/style.css @@ -1,11 +1,11 @@ body { margin: 0; - background: url('../a.jpg'); + background: url('../should-be-in-root-folder.jpg'); border-image: url('data:image/svg+xml;utf8,'); } p { color: red; - background: url('./b.jpg'); + background: url('./should-be-in-css-folder.jpg'); } body > p { color: yellow; diff --git a/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html index 6b45f65bc5..d98ff7b9fa 100644 --- a/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html +++ b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html @@ -7,6 +7,10 @@ with style sheet with import + diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index de1d79eb6d..7bf6141e44 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -3,63 +3,59 @@ */ import { JSDOM } from 'jsdom'; import { describe, it, expect } from 'vitest'; -import { - absoluteToStylesheet, - serializeNodeWithId, - _isBlockedElement, -} from '../src/snapshot'; +import { serializeNodeWithId, _isBlockedElement } from '../src/snapshot'; import snapshot from '../src/snapshot'; import { serializedNodeWithId, elementNode } from '../src/types'; -import { Mirror } from '../src/utils'; +import { Mirror, absolutifyURLs } from '../src/utils'; describe('absolute url to stylesheet', () => { const href = 'http://localhost/css/style.css'; it('can handle relative path', () => { - expect(absoluteToStylesheet('url(a.jpg)', href)).toEqual( + expect(absolutifyURLs('url(a.jpg)', href)).toEqual( `url(http://localhost/css/a.jpg)`, ); }); it('can handle same level path', () => { - expect(absoluteToStylesheet('url("./a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("./a.jpg")', href)).toEqual( `url("http://localhost/css/a.jpg")`, ); }); it('can handle parent level path', () => { - expect(absoluteToStylesheet('url("../a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("../a.jpg")', href)).toEqual( `url("http://localhost/a.jpg")`, ); }); it('can handle absolute path', () => { - expect(absoluteToStylesheet('url("/a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("/a.jpg")', href)).toEqual( `url("http://localhost/a.jpg")`, ); }); it('can handle external path', () => { - expect(absoluteToStylesheet('url("http://localhost/a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("http://localhost/a.jpg")', href)).toEqual( `url("http://localhost/a.jpg")`, ); }); it('can handle single quote path', () => { - expect(absoluteToStylesheet(`url('./a.jpg')`, href)).toEqual( + expect(absolutifyURLs(`url('./a.jpg')`, href)).toEqual( `url('http://localhost/css/a.jpg')`, ); }); it('can handle no quote path', () => { - expect(absoluteToStylesheet('url(./a.jpg)', href)).toEqual( + expect(absolutifyURLs('url(./a.jpg)', href)).toEqual( `url(http://localhost/css/a.jpg)`, ); }); it('can handle multiple no quote paths', () => { expect( - absoluteToStylesheet( + absolutifyURLs( 'background-image: url(images/b.jpg);background: #aabbcc url(images/a.jpg) 50% 50% repeat;', href, ), @@ -70,11 +66,11 @@ describe('absolute url to stylesheet', () => { }); it('can handle data url image', () => { + expect(absolutifyURLs('url(data:image/gif;base64,ABC)', href)).toEqual( + 'url(data:image/gif;base64,ABC)', + ); expect( - absoluteToStylesheet('url(data:image/gif;base64,ABC)', href), - ).toEqual('url(data:image/gif;base64,ABC)'); - expect( - absoluteToStylesheet( + absolutifyURLs( 'url(data:application/font-woff;base64,d09GMgABAAAAAAm)', href, ), @@ -83,7 +79,7 @@ describe('absolute url to stylesheet', () => { it('preserves quotes around inline svgs with spaces', () => { expect( - absoluteToStylesheet( + absolutifyURLs( "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M3'/%3E%3C/svg%3E\")", href, ), @@ -91,7 +87,7 @@ describe('absolute url to stylesheet', () => { "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M3'/%3E%3C/svg%3E\")", ); expect( - absoluteToStylesheet( + absolutifyURLs( 'url(\'data:image/svg+xml;utf8,\')', href, ), @@ -99,7 +95,7 @@ describe('absolute url to stylesheet', () => { 'url(\'data:image/svg+xml;utf8,\')', ); expect( - absoluteToStylesheet( + absolutifyURLs( 'url("data:image/svg+xml;utf8,")', href, ), @@ -108,7 +104,7 @@ describe('absolute url to stylesheet', () => { ); }); it('can handle empty path', () => { - expect(absoluteToStylesheet(`url('')`, href)).toEqual(`url('')`); + expect(absolutifyURLs(`url('')`, href)).toEqual(`url('')`); }); }); diff --git a/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts b/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts new file mode 100644 index 0000000000..1e42bab1a6 --- /dev/null +++ b/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts @@ -0,0 +1,37 @@ +/** + * @vitest-environment jsdom + */ +import { bench } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { stringifyStylesheet } from '../src/utils'; +import * as CSSOM from 'cssom'; + +describe('stringifyStylesheet', () => { + let benchmarkStylesheet: CSSStyleSheet; + + const cssText = fs.readFileSync( + path.resolve(__dirname, './css/benchmark.css'), + 'utf8', + ); + benchmarkStylesheet = CSSOM.parse(cssText); + benchmarkStylesheet.href = 'https://example.com/style.css'; + + it.skip('stringify', () => { + // written just to ensure it's working + const cssText = '.x { background: url(./relative.jpg) }'; + const styleSheet = CSSOM.parse(cssText); + styleSheet.href = 'https://example.com/style.css'; + expect(stringifyStylesheet(styleSheet)).toEqual( + 'x {background: url(https://example.com/relative.jpg);}', + ); + }); + + bench( + 'stringify', + () => { + stringifyStylesheet(benchmarkStylesheet); + }, + { time: 1000 }, + ); +}); diff --git a/packages/rrweb-snapshot/vitest.config.ts b/packages/rrweb-snapshot/vitest.config.ts index 39888437cf..1b5a8b7e3e 100644 --- a/packages/rrweb-snapshot/vitest.config.ts +++ b/packages/rrweb-snapshot/vitest.config.ts @@ -6,7 +6,7 @@ export default mergeConfig( configShared, defineProject({ test: { - // ... custom test config here + globals: true, }, }), ); diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index 5395cde15d..c2bbacc6ff 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -67,7 +67,7 @@ export class StylesheetManager { styles.push({ styleId, rules: Array.from(sheet.rules || CSSRule, (r, index) => ({ - rule: stringifyRule(r), + rule: stringifyRule(r, sheet.href), index, })), });