Skip to content

Commit edf6302

Browse files
tests: add cloneStyleSheetList and cloneStyleSheets unit tests (#71)
1 parent 42171c8 commit edf6302

File tree

3 files changed

+325
-9
lines changed

3 files changed

+325
-9
lines changed

__tests__/dom.test.ts

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { blockScroll, restoreScroll } from '@/dom'
6+
import { cloneStyleSheetList, cloneStyleSheets } from '@/dom'
67

78
describe( 'dom utilities', () => {
89

@@ -103,5 +104,316 @@ describe( 'dom utilities', () => {
103104
document.body.removeChild( div )
104105

105106
} )
107+
106108
} )
109+
110+
111+
describe( 'cloneStyleSheetList', () => {
112+
113+
beforeEach( () => {
114+
// clean styles
115+
document.querySelectorAll( 'style' ).forEach( style => {
116+
style.remove()
117+
} )
118+
} )
119+
120+
121+
it( 'clones StyleSheetList into HTMLStyleElements', () => {
122+
123+
const styles = Array.from( Array( 2 ) ).map( ( _, index ) => {
124+
const style = document.createElement( 'style' )
125+
style.innerHTML = `h${ index + 1 } {color: green;}`
126+
127+
document.head.appendChild( style )
128+
return style
129+
} )
130+
131+
const cloned = cloneStyleSheetList( document.styleSheets )
132+
133+
expect( cloned.length ).toBe( styles.length )
134+
135+
cloned.forEach( ( style, index ) => {
136+
expect( style ).toBeInstanceOf( HTMLStyleElement )
137+
expect( style.innerText )
138+
.toBe( styles[ index ]?.innerText )
139+
} )
140+
141+
} )
142+
143+
144+
it( 'clones an array of CSSStyleSheet into HTMLStyleElements', () => {
145+
146+
const styles = Array.from( Array( 2 ) ).map( ( _, index ) => {
147+
const style = new CSSStyleSheet()
148+
style.insertRule( `h${ index + 1 }: {color: green;}` )
149+
return style
150+
} )
151+
152+
const cloned = cloneStyleSheetList( styles )
153+
154+
expect( cloned.length ).toBe( styles.length )
155+
156+
cloned.forEach( ( style, index ) => {
157+
const rules = Array.from( styles[ index ]?.cssRules || [] )
158+
159+
expect( style ).toBeInstanceOf( HTMLStyleElement )
160+
expect( style.innerHTML )
161+
.toBe( rules.at( 0 )?.cssText )
162+
} )
163+
164+
} )
165+
166+
167+
it( 'filters out stylesheets with inaccessible cssRules', () => {
168+
169+
const cloned = cloneStyleSheetList( [ new CSSStyleSheet() ] )
170+
171+
expect( cloned.length ).toBe( 0 )
172+
173+
} )
174+
175+
176+
it( 'logs error to console when an error occurs and skips iteration', () => {
177+
178+
const consoleSpy = jest.spyOn( console, 'error' ).mockImplementation( () => {} )
179+
180+
const createElementSpy = jest.spyOn( document, 'createElement' ).mockImplementation( () => {
181+
throw new Error( 'Unexpected error' )
182+
} )
183+
184+
const result = cloneStyleSheetList( [ {} as CSSStyleSheet ] )
185+
186+
expect( result ).toEqual( [] )
187+
188+
expect( consoleSpy ).toHaveBeenCalledWith(
189+
'Error while cloning styles.', expect.any( Error )
190+
)
191+
192+
consoleSpy.mockRestore()
193+
createElementSpy.mockRestore()
194+
195+
} )
196+
197+
} )
198+
199+
200+
describe( 'cloneStyleSheets', () => {
201+
202+
beforeEach( () => {
203+
// clean styles
204+
document.querySelectorAll( 'style' ).forEach( style => {
205+
style.remove()
206+
} )
207+
} )
208+
209+
210+
describe( 'StyleSheetList', () => {
211+
212+
it( 'clones a StyleSheetList or an array of StyleSheetList', async () => {
213+
214+
const styles = Array.from( Array( 2 ) ).map( ( _, index ) => {
215+
const style = document.createElement( 'style' )
216+
style.innerHTML = `h${ index + 1 } {color: green;}`
217+
218+
document.head.appendChild( style )
219+
return style
220+
} )
221+
222+
const cloned = await cloneStyleSheets( document.styleSheets )
223+
224+
expect( cloned.length ).toBe( styles.length )
225+
226+
cloned.forEach( ( style, index ) => {
227+
expect( style ).toBeInstanceOf( HTMLStyleElement )
228+
expect( style.innerText )
229+
.toBe( styles[ index ]?.innerText )
230+
} )
231+
232+
} )
233+
234+
} )
235+
236+
237+
describe( 'CSSStyleSheet', () => {
238+
239+
it( 'clones a CSSStyleSheet', async () => {
240+
241+
const style = new CSSStyleSheet()
242+
style.insertRule( 'h1: {color: green;}' )
243+
244+
const cloned = await cloneStyleSheets( style )
245+
246+
expect( cloned.length ).toBe( 1 )
247+
248+
expect( cloned.at( 0 ) ).toBeInstanceOf( HTMLStyleElement )
249+
expect( cloned.at( 0 )?.innerHTML ).toBe( 'h1: {color: green;}' )
250+
251+
} )
252+
253+
254+
it( 'clones an array of CSSStyleSheet', async () => {
255+
const styles = Array.from( Array( 2 ) ).map( ( _, index ) => {
256+
const style = new CSSStyleSheet()
257+
style.insertRule( `h${ index + 1 }: {color: green;}` )
258+
return style
259+
} )
260+
261+
const cloned = await cloneStyleSheets( styles )
262+
263+
expect( cloned.length ).toBe( styles.length )
264+
265+
cloned.forEach( ( style, index ) => {
266+
const rules = Array.from( styles[ index ]?.cssRules || [] )
267+
268+
expect( style ).toBeInstanceOf( HTMLStyleElement )
269+
expect( style.innerHTML )
270+
.toBe( rules.at( 0 )?.cssText )
271+
} )
272+
} )
273+
274+
} )
275+
276+
277+
describe( 'HTMLStyleElement', () => {
278+
279+
it( 'clones a single HTMLStyleElement', async () => {
280+
281+
const style = document.createElement( 'style' )
282+
style.innerHTML = 'body { color: red; }'
283+
const result = await cloneStyleSheets( style )
284+
285+
expect( result ).toHaveLength( 1 )
286+
expect( result[ 0 ] ).toBeInstanceOf( HTMLStyleElement )
287+
expect( result[ 0 ]?.innerHTML ).toBe( 'body { color: red; }' )
288+
289+
} )
290+
291+
292+
it( 'clones multiple styles from an array of HTMLStyleElement', async () => {
293+
294+
const styles = Array.from( Array( 2 ) ).map( ( _, index ) => {
295+
const style = document.createElement( 'style' )
296+
style.innerHTML = `h${ index + 1 } {color: green;}`
297+
return style
298+
} )
299+
300+
const cloned = await cloneStyleSheets( styles )
301+
302+
expect( cloned.length ).toBe( styles.length )
303+
304+
cloned.forEach( ( style, index ) => {
305+
expect( style ).toBeInstanceOf( HTMLStyleElement )
306+
expect( style.innerHTML )
307+
.toBe( styles[ index ]?.innerHTML )
308+
} )
309+
310+
} )
311+
312+
} )
313+
314+
315+
describe( 'UrlStylesheet', () => {
316+
317+
it( 'creates a link element for URL stylesheet with fetch false', async () => {
318+
319+
const result = await cloneStyleSheets( 'https://example.com/style.css' )
320+
321+
expect( result ).toHaveLength( 1 )
322+
expect( result[ 0 ] ).toBeInstanceOf( HTMLLinkElement )
323+
expect( ( result[ 0 ] as HTMLLinkElement ).href ).toBe( 'https://example.com/style.css' )
324+
expect( ( result[ 0 ] as HTMLLinkElement ).rel ).toBe( 'stylesheet' )
325+
326+
} )
327+
328+
329+
it( 'creates a link element for URL stylesheet object with fetch undefined', async () => {
330+
331+
const result = await cloneStyleSheets( { url: { pathname: '/style.css' }, fetch: undefined } )
332+
333+
expect( result ).toHaveLength( 1 )
334+
expect( result[ 0 ] ).toBeInstanceOf( HTMLLinkElement )
335+
expect( ( result[ 0 ] as HTMLLinkElement ).href ).toBe( 'http://localhost/style.css' )
336+
337+
} )
338+
339+
340+
it( 'creates a link element for URL stylesheet object with fetch false', async () => {
341+
342+
const result = await cloneStyleSheets( { url: 'https://example.com/style.css', fetch: false } )
343+
344+
expect( result ).toHaveLength( 1 )
345+
expect( result[ 0 ] ).toBeInstanceOf( HTMLLinkElement )
346+
expect( ( result[ 0 ] as HTMLLinkElement ).href ).toBe( 'https://example.com/style.css' )
347+
348+
} )
349+
350+
351+
describe( 'fetch', () => {
352+
353+
let deleteFetch = false
354+
let fetchSpy: jest.SpyInstance<ReturnType<typeof fetch>>
355+
const responseText = jest.fn()
356+
357+
beforeAll( () => {
358+
if ( ! global.fetch ) {
359+
Object.defineProperty( global, 'fetch', {
360+
writable: true,
361+
value : jest.fn(),
362+
} )
363+
deleteFetch = true
364+
}
365+
366+
fetchSpy = jest.spyOn( global, 'fetch' ).mockResolvedValue( {
367+
ok : true,
368+
status : 200,
369+
text : responseText,
370+
headers : new Headers( { 'Content-Type': 'text/css' } ),
371+
} as unknown as Response )
372+
} )
373+
374+
afterAll( () => {
375+
fetchSpy.mockRestore()
376+
377+
if ( deleteFetch ) {
378+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
379+
delete ( global as any ).fetch
380+
}
381+
} )
382+
383+
it( 'returns HTMLStyleElement with fetched content if fetch is set to true', async () => {
384+
385+
const css = 'h1 {color: green}'
386+
responseText.mockReturnValueOnce( css )
387+
const result = await cloneStyleSheets( { url: 'https://example.com/style.css', fetch: true } )
388+
389+
expect( fetchSpy ).toHaveBeenCalled()
390+
expect( result ).toHaveLength( 1 )
391+
expect( result[ 0 ] ).toBeInstanceOf( HTMLStyleElement )
392+
expect( result[ 0 ]?.textContent ).toBe( css )
393+
394+
} )
395+
396+
397+
it( 'doesn\'t return a HTMLStyleElement if fetch fails', async () => {
398+
399+
fetchSpy.mockResolvedValueOnce( {
400+
ok : false,
401+
status : 404,
402+
text : responseText,
403+
headers : new Headers(),
404+
} as unknown as Response )
405+
406+
const result = await cloneStyleSheets( { url: 'https://example.com/style.css', fetch: true } )
407+
408+
expect( fetchSpy ).toHaveBeenCalled()
409+
expect( result ).toHaveLength( 0 )
410+
411+
} )
412+
413+
} )
414+
415+
} )
416+
417+
} )
418+
107419
} )

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"//4c": "*********************************************************************",
8888
"test:map": "pnpm test:watch map.test.ts",
8989
"test:blob": "pnpm test:watch blob.test.ts",
90+
"test:dom": "pnpm test:watch __tests__/dom.test.ts",
9091
"test:generators": "pnpm test:watch generators.test.ts",
9192
"test:regex": "pnpm test:watch regex.test.ts",
9293
"test:strings": "pnpm test:watch strings.test.ts",

src/dom.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,19 @@ export type CloneStyleSheetsReturn = ( HTMLStyleElement | HTMLLinkElement )[]
9191
* styles.forEach( style => shadowRoot.appendChild( style ) )
9292
* ```
9393
*/
94-
const cloneStyleSheetList = ( styleSheets: StyleSheetList | CSSStyleSheet[] ) => (
94+
export const cloneStyleSheetList = ( styleSheets: StyleSheetList | CSSStyleSheet[] ) => (
9595
[ ...styleSheets ].map( ( { cssRules } ) => {
9696
try {
97+
98+
if ( cssRules.length <= 0 ) return
9799

98100
const style = document.createElement( 'style' )
99101

100-
for ( let i = 0; i < cssRules.length; i++ ) {
101-
const rule = cssRules[ i ]
102-
103-
if ( ! rule ) continue
104-
102+
Array.from( cssRules ).forEach( rule => {
105103
style.appendChild(
106104
document.createTextNode( rule.cssText )
107105
)
108-
}
106+
} )
109107

110108
return style
111109

@@ -181,7 +179,9 @@ export const cloneStyleSheets = async ( styles: Styles ): Promise<CloneStyleShee
181179
styleNodes.push( ...styleElements.map( style => {
182180
const target = document.createElement( 'style' )
183181
target.appendChild(
184-
document.createTextNode( style.innerText )
182+
document.createTextNode(
183+
style.textContent || style.innerText || style.innerHTML
184+
)
185185
)
186186
return target
187187
} ) )
@@ -213,7 +213,10 @@ export const cloneStyleSheets = async ( styles: Styles ): Promise<CloneStyleShee
213213

214214
const { data, error } = await fetch<string>( url )
215215

216-
if ( error ) throw error
216+
if ( error ) {
217+
console.error( 'Error while fetching the given style URL.', error )
218+
throw error
219+
}
217220

218221
const style = document.createElement( 'style' )
219222

0 commit comments

Comments
 (0)