33 */
44
55import { blockScroll , restoreScroll } from '@/dom'
6+ import { cloneStyleSheetList , cloneStyleSheets } from '@/dom'
67
78describe ( '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} )
0 commit comments