/** * Internal dependencies */ import { normalizeMedia, updateStylesWithSCS, preloadStyles, applyStyles, type StyleElement, } from '../styles'; /** * Mocks the `sheet` property for the * passed HTMLStyleElement or HTMLLinkElement instance. * * @param element Style or Link element. * @param params Values for certain props. * @param params.disabled Value for the `sheet.disabled` prop. * @param params.mediaText Value for the `sheet.media.mediaText` prop. */ const mockSheet = ( element: StyleElement, { disabled, mediaText }: { disabled: boolean; mediaText: string } ) => { if ( element.sheet ) { Object.assign( element.sheet, { disabled, media: { mediaText } } ); } else { Object.defineProperty( element, 'sheet', { value: { disabled, media: { mediaText }, }, } ); } }; /** * Creates an `HTMLStyleElement` instance for testing. * * @param id Value for the `id` attribute. * @return An `HTMLStyleElement` instance. */ const createStyleElement = ( id: string ): HTMLStyleElement => { const element = document.createElement( 'style' ); element.id = id; element.textContent = `/* Style ${ id } */`; return element; }; /** * Creates an `HTMLLinkElement` instance for testing. * * @param id Value for the `id` attribute. * @param href Value for the `href` attribute. * @return An `HTMLLinkElement` instance. */ const createLinkElement = ( id: string, href: string = `https://example.com/${ id }.css` ): HTMLLinkElement => { const element = document.createElement( 'link' ); element.id = id; element.rel = 'stylesheet'; element.href = href; return element; }; describe( 'Router styles management', () => { const parent = document.head; beforeEach( () => { document.head.replaceChildren(); } ); afterAll( () => { document.head.replaceChildren(); } ); describe( 'updateStylesWithSCS', () => { it( 'should append all elements when X is empty in the correct order', () => { const X: HTMLStyleElement[] = []; const Y = [ createStyleElement( 'style1' ), createLinkElement( 'link1' ), createStyleElement( 'style2' ), ]; const promises = updateStylesWithSCS( X, Y, parent ); const { childNodes } = parent; expect( promises.length ).toBe( 3 ); expect( childNodes.length ).toBe( 3 ); // Verify elements are in the correct order. expect( childNodes[ 0 ] ).toBe( Y[ 0 ] ); expect( childNodes[ 1 ] ).toBe( Y[ 1 ] ); expect( childNodes[ 2 ] ).toBe( Y[ 2 ] ); // Verify the correct media attribute is set. expect( childNodes[ 0 ] ).toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 1 ] ).toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 2 ] ).toHaveAttribute( 'media', 'preload' ); } ); it( 'should handle when both X and Y are empty', () => { const promises = updateStylesWithSCS( [], [], parent ); expect( promises.length ).toBe( 0 ); expect( parent.childNodes.length ).toBe( 0 ); } ); it( 'should do nothing when X is non-empty but Y is empty', () => { const style1 = createStyleElement( 'style1' ); const style2 = createStyleElement( 'style2' ); const X = [ style1, style2 ]; // Pre-append X so they are in the DOM. parent.append( ...X ); const promises = updateStylesWithSCS( X, [], parent ); const { childNodes } = parent; // No new promises should be generated. expect( promises.length ).toBe( 0 ); // The DOM should still only have the original X elements. expect( childNodes.length ).toBe( X.length ); expect( childNodes[ 0 ] ).toBe( style1 ); expect( childNodes[ 1 ] ).toBe( style2 ); // Verify the correct media attribute is set. expect( childNodes[ 0 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 1 ] ).not.toHaveAttribute( 'media', 'preload' ); } ); it( 'should keep existing elements when they match in both X and Y', () => { const X = [ createStyleElement( 'style1' ), createLinkElement( 'link1' ), ]; const Y = [ createStyleElement( 'style1' ), createLinkElement( 'link1' ), ]; parent.append( ...X ); const promises = updateStylesWithSCS( X, Y, parent ); const { childNodes } = parent; expect( promises.length ).toBe( 2 ); expect( childNodes.length ).toBe( 2 ); // Should maintain the original elements (not replace with clones). expect( childNodes[ 0 ] ).toBe( X[ 0 ] ); expect( childNodes[ 1 ] ).toBe( X[ 1 ] ); // Verify the correct media attribute is set. expect( childNodes[ 0 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 1 ] ).not.toHaveAttribute( 'media', 'preload' ); } ); it( 'should insert new elements from Y in correct positions relative to X', () => { const X = [ createStyleElement( 'style1' ), createStyleElement( 'style3' ), ]; const Y = [ createStyleElement( 'style1' ), createStyleElement( 'style2' ), createStyleElement( 'style3' ), createStyleElement( 'style4' ), ]; parent.append( ...X ); const promises = updateStylesWithSCS( X, Y, parent ); const { childNodes } = parent; expect( promises.length ).toBe( 4 ); expect( childNodes.length ).toBe( 4 ); expect( childNodes[ 0 ] ).toBe( X[ 0 ] ); expect( childNodes[ 1 ] ).toBe( Y[ 1 ] ); expect( childNodes[ 2 ] ).toBe( X[ 1 ] ); expect( childNodes[ 3 ] ).toBe( Y[ 3 ] ); // Verify the correct media attribute is set. expect( childNodes[ 0 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 1 ] ).toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 2 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 3 ] ).toHaveAttribute( 'media', 'preload' ); } ); it( 'should handle Y having completely different elements than X', () => { const X = [ createStyleElement( 'style1' ), createStyleElement( 'style2' ), ]; const Y = [ createStyleElement( 'style3' ), createStyleElement( 'style4' ), ]; parent.append( ...X ); const promises = updateStylesWithSCS( X, Y, parent ); const { childNodes } = parent; expect( promises.length ).toBe( 2 ); // Check the specific order - based on the SCS algorithm. // When X and Y are completely different, the SCS places. // all elements from Y before X. expect( childNodes[ 0 ] ).toBe( Y[ 0 ] ); expect( childNodes[ 1 ] ).toBe( Y[ 1 ] ); expect( childNodes[ 2 ] ).toBe( X[ 0 ] ); expect( childNodes[ 3 ] ).toBe( X[ 1 ] ); // Verify the correct media attribute is set. expect( childNodes[ 0 ] ).toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 1 ] ).toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 2 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 3 ] ).not.toHaveAttribute( 'media', 'preload' ); } ); it( 'should consider normalized media attributes when comparing elements', () => { // These should be considered equal after normalizing the media attribute. const X = [ createLinkElement( 'same-link' ) ]; const Y = [ createLinkElement( 'same-link' ) ]; X[ 0 ].setAttribute( 'media', 'preload' ); X[ 0 ].dataset.originalMedia = 'screen'; Y[ 0 ].setAttribute( 'media', 'screen' ); parent.append( ...X ); const promises = updateStylesWithSCS( X, Y, parent ); const { childNodes } = parent; expect( childNodes.length ).toBe( 1 ); expect( promises.length ).toBe( 1 ); expect( childNodes[ 0 ] ).toBe( X[ 0 ] ); // Verify the media attribute has not changed. expect( childNodes[ 0 ] ).toHaveAttribute( 'media', 'preload' ); } ); it( 'should treat style elements as already loaded', async () => { const X = [ createStyleElement( 'style1' ) ]; const Y = [ createStyleElement( 'style1' ) ]; parent.append( ...X ); const promises = updateStylesWithSCS( X, Y, parent ); expect( promises.length ).toBe( 1 ); expect( await promises[ 0 ] ).toBe( X[ 0 ] ); } ); it( 'should return the same promise for equivalent link elements', () => { const X = [ createLinkElement( 'link1' ) ]; const Y = [ createLinkElement( 'link1' ) ]; // First, add it to the DOM. const promises1 = updateStylesWithSCS( [], X, parent ); // Then, use it in a second call. const promises2 = updateStylesWithSCS( X, Y, parent ); // The promises should be the same. expect( promises1[ 0 ] ).toBe( promises2[ 0 ] ); } ); it( 'should return the same promise for equivalent style elements', () => { const X = [ createStyleElement( 'style1' ) ]; const Y = [ createStyleElement( 'style1' ) ]; // First, add it to the DOM. const promises1 = updateStylesWithSCS( [], X, parent ); // Then, use it in a second call. const promises2 = updateStylesWithSCS( X, Y, parent ); // The promises should be the same. expect( promises1[ 0 ] ).toBe( promises2[ 0 ] ); } ); it( 'should handle complex reordering of elements maintaining the correct order', async () => { // Initial set of elements. const style1 = createStyleElement( 'style1' ); const style2 = createStyleElement( 'style2' ); const style3 = createStyleElement( 'style3' ); const style4 = createStyleElement( 'style4' ); const X = [ style1, style2, style3, style4 ]; parent.append( ...X ); // New set with reordering and some new elements. const newStyle1 = createStyleElement( 'style1' ); const newStyle3 = createStyleElement( 'style3' ); const newStyle5 = createStyleElement( 'style5' ); const newStyle2 = createStyleElement( 'style2' ); const newStyle6 = createStyleElement( 'style6' ); const Y = [ newStyle1, newStyle3, newStyle5, newStyle2, newStyle6 ]; const promises = updateStylesWithSCS( X, Y, parent ); const { childNodes } = parent; expect( promises.length ).toBe( 5 ); // Verify the exact order. expect( childNodes[ 0 ] ).toBe( style1 ); expect( childNodes[ 1 ] ).toBe( newStyle3 ); expect( childNodes[ 2 ] ).toBe( newStyle5 ); expect( childNodes[ 3 ] ).toBe( style2 ); expect( childNodes[ 4 ] ).toBe( newStyle6 ); expect( childNodes[ 5 ] ).toBe( style3 ); expect( childNodes[ 6 ] ).toBe( style4 ); // Verify the promise values. const values = await Promise.all( promises ); expect( values[ 0 ] ).toBe( style1 ); expect( values[ 1 ] ).toBe( newStyle3 ); expect( values[ 2 ] ).toBe( newStyle5 ); expect( values[ 3 ] ).toBe( style2 ); expect( values[ 4 ] ).toBe( newStyle6 ); // Verify the correct media attribute is set. expect( childNodes[ 0 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 1 ] ).toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 2 ] ).toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 3 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 4 ] ).toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 5 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 6 ] ).not.toHaveAttribute( 'media', 'preload' ); } ); it( 'should handle link elements with load events', async () => { const link1 = createLinkElement( 'link1' ); const promises = updateStylesWithSCS( [], [ link1 ], parent ); // Manually trigger a load event. link1.dispatchEvent( new Event( 'load' ) ); // The promise should resolve with the link element. const result = await promises[ 0 ]; expect( result ).toBe( link1 ); } ); it( 'should reject the promise when a link element fails to load', async () => { const link1 = createLinkElement( 'link-fail' ); // Remove load/error listeners by not adding it to the DOM initially. const promises = updateStylesWithSCS( [], [ link1 ], parent ); // Simulate an error event. const errorEvent = new Event( 'error' ); link1.dispatchEvent( errorEvent ); await expect( promises[ 0 ] ).rejects.toThrow( `The style sheet with the following URL failed to load: ${ link1.href }` ); } ); it( 'should handle mixed style and link elements', async () => { const X = [ createLinkElement( 'link1' ), createStyleElement( 'style1' ), createLinkElement( 'link2' ), createStyleElement( 'style2' ), createStyleElement( 'style3' ), ]; parent.append( ...X ); const Y = [ createLinkElement( 'link1' ), createLinkElement( 'link3' ), createStyleElement( 'style1' ), createLinkElement( 'link2' ), createStyleElement( 'style2' ), createLinkElement( 'link1' ), ]; // Run the update using X and Y. const promises = updateStylesWithSCS( X, Y, parent ); const { childNodes } = parent; // Expected DOM outcome (based on the SCS algorithm):. // We expect that the existing link1 and link2 are reused, and the new link3 is inserted. // The duplicate occurrences of link1 (via link1Clone and link1 itself in Y). // should resolve to the original link1. expect( childNodes.length ).toBe( 7 ); expect( [ ...childNodes ] ).toEqual( [ X[ 0 ], Y[ 1 ], X[ 1 ], X[ 2 ], X[ 3 ], Y[ 5 ], X[ 4 ], ] ); ( childNodes as NodeListOf< StyleElement > ).forEach( ( element ) => element.dispatchEvent( new Event( 'load' ) ) ); // Verify that the returned promises resolve to the appropriate elements. const resolved = await Promise.all( promises ); expect( promises.length ).toBe( 6 ); expect( resolved ).toEqual( [ X[ 0 ], Y[ 1 ], X[ 1 ], X[ 2 ], X[ 3 ], Y[ 5 ], ] ); // Verify the correct media attribute is set. expect( childNodes[ 0 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 1 ] ).toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 2 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 3 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 4 ] ).not.toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 5 ] ).toHaveAttribute( 'media', 'preload' ); expect( childNodes[ 6 ] ).not.toHaveAttribute( 'media', 'preload' ); } ); it( 'should set media and data-original-media correctly on new elements', async () => { const X = [ createStyleElement( 'style1' ), createStyleElement( 'style1Media' ), createLinkElement( 'link1' ), createLinkElement( 'link1Media' ), ]; X[ 1 ].setAttribute( 'media', 'screen' ); // style1Media X[ 3 ].setAttribute( 'media', 'screen' ); // link1Media mockSheet( X[ 2 ], { disabled: false, mediaText: 'all' } ); // link1 mockSheet( X[ 3 ], { disabled: false, mediaText: 'screen' } ); // link1Media parent.append( ...X ); const Y = [ ...X.map( ( e ) => e.cloneNode( true ) as StyleElement ), createStyleElement( 'style2' ), createStyleElement( 'style2Media' ), createLinkElement( 'link2' ), createLinkElement( 'link2Media' ), ]; Y[ 5 ].setAttribute( 'media', 'screen' ); // style2Media Y[ 7 ].setAttribute( 'media', 'screen' ); // link2Media const promises = updateStylesWithSCS( X, Y, parent ); ( parent.childNodes as NodeListOf< StyleElement > ).forEach( ( element ) => element.dispatchEvent( new Event( 'load' ) ) ); const elements = await Promise.all( promises ); const attributes = elements.map( ( e ) => ( { id: e.id, media: e.getAttribute( 'media' ), originalMedia: e.getAttribute( 'data-original-media' ), } ) ); expect( attributes ).toEqual( [ { id: 'style1', media: null, originalMedia: null, }, { id: 'style1Media', media: 'screen', originalMedia: null, }, { id: 'link1', media: null, originalMedia: null, }, { id: 'link1Media', media: 'screen', originalMedia: null, }, { id: 'style2', media: 'preload', originalMedia: null, }, { id: 'style2Media', media: 'preload', originalMedia: 'screen', }, { id: 'link2', media: 'preload', originalMedia: null, }, { id: 'link2Media', media: 'preload', originalMedia: 'screen', }, ] ); } ); } ); // Tests for preloadStyles function. describe( 'preloadStyles', () => { it( 'should return cached promises for the same HTML', () => { // Create a test document. const doc = document.implementation.createHTMLDocument(); const style = doc.createElement( 'style' ); doc.head.appendChild( style ); // NOTE: preloadStyles() modifies the passed document. That's why // the document is cloned beforehand. // First call should update the DOM. const firstResult = preloadStyles( doc.cloneNode() as Document ); expect( firstResult ).toBeTruthy(); // Second call should return the same promises. const secondResult = preloadStyles( doc.cloneNode() as Document ); expect( secondResult ).toEqual( firstResult ); } ); it( 'should extract style elements from the provided document', () => { // Create a test document with style elements. const doc = document.implementation.createHTMLDocument(); const style1 = doc.createElement( 'style' ); style1.id = 'test-style-1'; const style2 = doc.createElement( 'link' ); style2.id = 'test-link-1'; style2.rel = 'stylesheet'; doc.head.appendChild( style1 ); doc.head.appendChild( style2 ); preloadStyles( doc ); // Check that styles were extracted and added to the document. const addedStyle1 = document.querySelector( '#test-style-1' ); const addedLink1 = document.querySelector( '#test-link-1' ); expect( addedStyle1 ).toBeTruthy(); expect( addedLink1 ).toBeTruthy(); expect( addedStyle1 ).toHaveAttribute( 'media', 'preload' ); expect( addedLink1 ).toHaveAttribute( 'media', 'preload' ); } ); } ); // Tests for applyStyles function. describe( 'applyStyles', () => { it( 'should enable included styles and disable others', () => { // Create some style elements. const style1 = createStyleElement( 'style1' ); const style2 = createStyleElement( 'style2' ); const style3 = createStyleElement( 'style3' ); // Add all to document. document.head.appendChild( style1 ); document.head.appendChild( style2 ); document.head.appendChild( style3 ); // Init `sheet` property. mockSheet( style1, { disabled: true, mediaText: 'all' } ); mockSheet( style2, { disabled: false, mediaText: 'all' } ); mockSheet( style3, { disabled: false, mediaText: 'preload' } ); // Apply styles to only style1 and style3. applyStyles( [ style1, style3 ] ); // style1 and style3 should be enabled, style2 should be disabled. expect( style1.sheet!.disabled ).toBe( false ); expect( style2.sheet!.disabled ).toBe( true ); expect( style3.sheet!.disabled ).toBe( false ); } ); it( 'should set media appropriately based on originalMedia', () => { // Create link elements with originalMedia. const link1 = createLinkElement( 'link1' ); link1.setAttribute( 'media', 'preload' ); link1.dataset.originalMedia = 'print'; const link2 = createLinkElement( 'link2' ); link2.setAttribute( 'media', 'preload' ); link2.dataset.originalMedia = 'screen'; // Add to document. document.head.appendChild( link1 ); document.head.appendChild( link2 ); // Init `sheet` property. mockSheet( link1, { disabled: false, mediaText: 'preload' } ); mockSheet( link2, { disabled: false, mediaText: 'preload' } ); // Apply styles. applyStyles( [ link1, link2 ] ); // Check that media was set correctly. expect( link1.sheet!.media.mediaText ).toBe( 'print' ); expect( link2.sheet!.media.mediaText ).toBe( 'screen' ); } ); it( 'should use "all" as default media if no originalMedia specified', () => { // Create elements without originalMedia. const link = createLinkElement( 'link' ); const style = createStyleElement( 'style' ); link.setAttribute( 'media', 'preload' ); style.setAttribute( 'media', 'preload' ); // Add to document. document.head.append( link, style ); // Init `sheet` property. mockSheet( link, { disabled: false, mediaText: 'preload' } ); mockSheet( style, { disabled: false, mediaText: 'preload' } ); // Apply styles. applyStyles( [ link, style ] ); // Default should be "all". expect( link.sheet!.media.mediaText ).toBe( 'all' ); expect( style.sheet!.media.mediaText ).toBe( 'all' ); } ); it( 'should preserve media if it was initially set', () => { // Create link elements with originalMedia. const link = createLinkElement( 'link' ); link.setAttribute( 'media', 'print' ); // Add to document. document.head.appendChild( link ); // Init `sheet` property. mockSheet( link, { disabled: false, mediaText: 'print' } ); // Apply styles. applyStyles( [ link ] ); // Check that media was preserved correctly. expect( link.sheet!.media.mediaText ).toBe( 'print' ); } ); } ); describe( 'normalizeMedia', () => { let linkElement: HTMLLinkElement; let styleElement: HTMLStyleElement; beforeEach( () => { // Create fresh elements before each test linkElement = document.createElement( 'link' ); linkElement.rel = 'stylesheet'; linkElement.href = './styles.css'; styleElement = document.createElement( 'style' ); styleElement.textContent = 'body { color: red; }'; } ); it( 'should always return a clone of the element', () => { // Test with no media attribute const result1 = normalizeMedia( linkElement ); expect( result1 ).not.toBe( linkElement ); expect( result1 ).toHaveAttribute( 'media', 'all' ); // Test with media attribute other than "preload" linkElement.setAttribute( 'media', 'all' ); const result2 = normalizeMedia( linkElement ); expect( result2 ).not.toBe( linkElement ); expect( result2 ).toHaveAttribute( 'media', 'all' ); linkElement.setAttribute( 'media', 'preload' ); const result3 = normalizeMedia( linkElement ); expect( result3 ).not.toBe( linkElement ); expect( result3 ).toHaveAttribute( 'href', linkElement.getAttribute( 'href' ) ); } ); it( 'should remove media attribute when media="preload" and no data-original-media exists', () => { linkElement.setAttribute( 'media', 'preload' ); const result = normalizeMedia( linkElement ); expect( result ).toHaveAttribute( 'media', 'all' ); } ); it( 'should restore original media when media="preload" and data-original-media exists', () => { linkElement.setAttribute( 'media', 'preload' ); linkElement.dataset.originalMedia = 'all'; const result = normalizeMedia( linkElement ); expect( result ).toHaveAttribute( 'media', 'all' ); expect( result.dataset.originalMedia ).toBeUndefined(); } ); it( 'should work with style elements too', () => { styleElement.setAttribute( 'media', 'preload' ); styleElement.dataset.originalMedia = 'print'; const result = normalizeMedia( styleElement ); expect( result ).not.toBe( styleElement ); expect( result ).toHaveAttribute( 'media', 'print' ); expect( result.dataset.originalMedia ).toBeUndefined(); } ); it( 'should handle empty original media correctly', () => { linkElement.setAttribute( 'media', 'preload' ); linkElement.dataset.originalMedia = ''; const result = normalizeMedia( linkElement ); expect( result ).toHaveAttribute( 'media', 'all' ); expect( result.dataset.originalMedia ).toBeUndefined(); } ); it( 'should preserve other attributes when normalizing', () => { linkElement.setAttribute( 'media', 'preload' ); linkElement.dataset.originalMedia = 'all'; linkElement.setAttribute( 'data-test', 'value' ); linkElement.setAttribute( 'integrity', 'hash123' ); const result = normalizeMedia( linkElement ); expect( result ).toHaveAttribute( 'media', 'all' ); expect( result ).toHaveAttribute( 'data-test', 'value' ); expect( result ).toHaveAttribute( 'integrity', 'hash123' ); } ); it( 'should output the same for equivalent link elements', () => { const head = document.createElement( 'head' ); head.innerHTML = ` `; const links = [ ...head.childNodes ] .filter( ( e ) => e instanceof HTMLLinkElement ) .map( normalizeMedia ); expect( links[ 0 ].isEqualNode( links[ 1 ] ) ).toBe( true ); expect( links[ 0 ].isEqualNode( links[ 2 ] ) ).toBe( true ); expect( links[ 0 ].isEqualNode( links[ 3 ] ) ).toBe( true ); } ); it( 'should output the same for equivalent style elements', () => { const head = document.createElement( 'head' ); head.innerHTML = ` `; const styles = [ ...head.childNodes ] .filter( ( e ) => e instanceof HTMLStyleElement ) .map( normalizeMedia ); expect( styles[ 0 ].isEqualNode( styles[ 1 ] ) ).toBe( true ); expect( styles[ 0 ].isEqualNode( styles[ 2 ] ) ).toBe( true ); expect( styles[ 0 ].isEqualNode( styles[ 3 ] ) ).toBe( true ); } ); } ); } );