@wordpress/interactivity-router 2.25.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +22 -0
- package/build/assets/dynamic-importmap/fetch.js +60 -0
- package/build/assets/dynamic-importmap/fetch.js.map +1 -0
- package/build/assets/dynamic-importmap/index.js +91 -0
- package/build/assets/dynamic-importmap/index.js.map +1 -0
- package/build/assets/dynamic-importmap/loader.js +286 -0
- package/build/assets/dynamic-importmap/loader.js.map +1 -0
- package/build/assets/dynamic-importmap/resolver.js +220 -0
- package/build/assets/dynamic-importmap/resolver.js.map +1 -0
- package/build/assets/script-modules.js +64 -0
- package/build/assets/script-modules.js.map +1 -0
- package/build/assets/scs.js +62 -0
- package/build/assets/scs.js.map +1 -0
- package/build/assets/styles.js +246 -0
- package/build/assets/styles.js.map +1 -0
- package/build/full-page.js +43 -0
- package/build/full-page.js.map +1 -0
- package/build/index.js +140 -38
- package/build/index.js.map +1 -1
- package/build-module/assets/dynamic-importmap/fetch.js +54 -0
- package/build-module/assets/dynamic-importmap/fetch.js.map +1 -0
- package/build-module/assets/dynamic-importmap/index.js +72 -0
- package/build-module/assets/dynamic-importmap/index.js.map +1 -0
- package/build-module/assets/dynamic-importmap/loader.js +279 -0
- package/build-module/assets/dynamic-importmap/loader.js.map +1 -0
- package/build-module/assets/dynamic-importmap/resolver.js +213 -0
- package/build-module/assets/dynamic-importmap/resolver.js.map +1 -0
- package/build-module/assets/script-modules.js +55 -0
- package/build-module/assets/script-modules.js.map +1 -0
- package/build-module/assets/scs.js +56 -0
- package/build-module/assets/scs.js.map +1 -0
- package/build-module/assets/styles.js +235 -0
- package/build-module/assets/styles.js.map +1 -0
- package/build-module/full-page.js +39 -0
- package/build-module/full-page.js.map +1 -0
- package/build-module/index.js +142 -38
- package/build-module/index.js.map +1 -1
- package/build-types/assets/dynamic-importmap/fetch.d.ts +30 -0
- package/build-types/assets/dynamic-importmap/fetch.d.ts.map +1 -0
- package/build-types/assets/dynamic-importmap/index.d.ts +42 -0
- package/build-types/assets/dynamic-importmap/index.d.ts.map +1 -0
- package/build-types/assets/dynamic-importmap/loader.d.ts +68 -0
- package/build-types/assets/dynamic-importmap/loader.d.ts.map +1 -0
- package/build-types/assets/dynamic-importmap/resolver.d.ts +35 -0
- package/build-types/assets/dynamic-importmap/resolver.d.ts.map +1 -0
- package/build-types/assets/script-modules.d.ts +26 -0
- package/build-types/assets/script-modules.d.ts.map +1 -0
- package/build-types/assets/scs.d.ts +24 -0
- package/build-types/assets/scs.d.ts.map +1 -0
- package/build-types/assets/styles.d.ts +69 -0
- package/build-types/assets/styles.d.ts.map +1 -0
- package/build-types/full-page.d.ts +3 -0
- package/build-types/full-page.d.ts.map +1 -0
- package/build-types/index.d.ts +4 -5
- package/build-types/index.d.ts.map +1 -1
- package/package.json +9 -5
- package/src/assets/dynamic-importmap/fetch.ts +58 -0
- package/src/assets/dynamic-importmap/index.ts +87 -0
- package/src/assets/dynamic-importmap/loader.ts +366 -0
- package/src/assets/dynamic-importmap/resolver.ts +292 -0
- package/src/assets/script-modules.ts +69 -0
- package/src/assets/scs.ts +61 -0
- package/src/assets/styles.ts +272 -0
- package/src/assets/test/scs.test.ts +367 -0
- package/src/assets/test/styles.test.ts +758 -0
- package/src/full-page.ts +53 -0
- package/src/index.ts +165 -45
- package/tsconfig.full-page.json +12 -0
- package/tsconfig.full-page.tsbuildinfo +1 -0
- package/tsconfig.json +9 -4
- package/tsconfig.main.json +21 -0
- package/tsconfig.main.tsbuildinfo +1 -0
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
normalizeMedia,
|
|
6
|
+
updateStylesWithSCS,
|
|
7
|
+
preloadStyles,
|
|
8
|
+
applyStyles,
|
|
9
|
+
type StyleElement,
|
|
10
|
+
} from '../styles';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mocks the `sheet` property for the
|
|
14
|
+
* passed HTMLStyleElement or HTMLLinkElement instance.
|
|
15
|
+
*
|
|
16
|
+
* @param element Style or Link element.
|
|
17
|
+
* @param params Values for certain props.
|
|
18
|
+
* @param params.disabled Value for the `sheet.disabled` prop.
|
|
19
|
+
* @param params.mediaText Value for the `sheet.media.mediaText` prop.
|
|
20
|
+
*/
|
|
21
|
+
const mockSheet = (
|
|
22
|
+
element: StyleElement,
|
|
23
|
+
{ disabled, mediaText }: { disabled: boolean; mediaText: string }
|
|
24
|
+
) => {
|
|
25
|
+
if ( element.sheet ) {
|
|
26
|
+
Object.assign( element.sheet, { disabled, media: { mediaText } } );
|
|
27
|
+
} else {
|
|
28
|
+
Object.defineProperty( element, 'sheet', {
|
|
29
|
+
value: {
|
|
30
|
+
disabled,
|
|
31
|
+
media: { mediaText },
|
|
32
|
+
},
|
|
33
|
+
} );
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates an `HTMLStyleElement` instance for testing.
|
|
39
|
+
*
|
|
40
|
+
* @param id Value for the `id` attribute.
|
|
41
|
+
* @return An `HTMLStyleElement` instance.
|
|
42
|
+
*/
|
|
43
|
+
const createStyleElement = ( id: string ): HTMLStyleElement => {
|
|
44
|
+
const element = document.createElement( 'style' );
|
|
45
|
+
element.id = id;
|
|
46
|
+
element.textContent = `/* Style ${ id } */`;
|
|
47
|
+
return element;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates an `HTMLLinkElement` instance for testing.
|
|
52
|
+
*
|
|
53
|
+
* @param id Value for the `id` attribute.
|
|
54
|
+
* @param href Value for the `href` attribute.
|
|
55
|
+
* @return An `HTMLLinkElement` instance.
|
|
56
|
+
*/
|
|
57
|
+
const createLinkElement = (
|
|
58
|
+
id: string,
|
|
59
|
+
href: string = `https://example.com/${ id }.css`
|
|
60
|
+
): HTMLLinkElement => {
|
|
61
|
+
const element = document.createElement( 'link' );
|
|
62
|
+
element.id = id;
|
|
63
|
+
element.rel = 'stylesheet';
|
|
64
|
+
element.href = href;
|
|
65
|
+
return element;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
describe( 'Router styles management', () => {
|
|
69
|
+
const parent = document.head;
|
|
70
|
+
|
|
71
|
+
beforeEach( () => {
|
|
72
|
+
document.head.replaceChildren();
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
afterAll( () => {
|
|
76
|
+
document.head.replaceChildren();
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
describe( 'updateStylesWithSCS', () => {
|
|
80
|
+
it( 'should append all elements when X is empty in the correct order', () => {
|
|
81
|
+
const X = [];
|
|
82
|
+
const Y = [
|
|
83
|
+
createStyleElement( 'style1' ),
|
|
84
|
+
createLinkElement( 'link1' ),
|
|
85
|
+
createStyleElement( 'style2' ),
|
|
86
|
+
];
|
|
87
|
+
const promises = updateStylesWithSCS( X, Y, parent );
|
|
88
|
+
const { childNodes } = parent;
|
|
89
|
+
|
|
90
|
+
expect( promises.length ).toBe( 3 );
|
|
91
|
+
expect( childNodes.length ).toBe( 3 );
|
|
92
|
+
|
|
93
|
+
// Verify elements are in the correct order.
|
|
94
|
+
expect( childNodes[ 0 ] ).toBe( Y[ 0 ] );
|
|
95
|
+
expect( childNodes[ 1 ] ).toBe( Y[ 1 ] );
|
|
96
|
+
expect( childNodes[ 2 ] ).toBe( Y[ 2 ] );
|
|
97
|
+
|
|
98
|
+
// Verify the correct media attribute is set.
|
|
99
|
+
expect( childNodes[ 0 ] ).toHaveAttribute( 'media', 'preload' );
|
|
100
|
+
expect( childNodes[ 1 ] ).toHaveAttribute( 'media', 'preload' );
|
|
101
|
+
expect( childNodes[ 2 ] ).toHaveAttribute( 'media', 'preload' );
|
|
102
|
+
} );
|
|
103
|
+
|
|
104
|
+
it( 'should handle when both X and Y are empty', () => {
|
|
105
|
+
const promises = updateStylesWithSCS( [], [], parent );
|
|
106
|
+
|
|
107
|
+
expect( promises.length ).toBe( 0 );
|
|
108
|
+
expect( parent.childNodes.length ).toBe( 0 );
|
|
109
|
+
} );
|
|
110
|
+
|
|
111
|
+
it( 'should do nothing when X is non-empty but Y is empty', () => {
|
|
112
|
+
const style1 = createStyleElement( 'style1' );
|
|
113
|
+
const style2 = createStyleElement( 'style2' );
|
|
114
|
+
const X = [ style1, style2 ];
|
|
115
|
+
// Pre-append X so they are in the DOM.
|
|
116
|
+
parent.append( ...X );
|
|
117
|
+
|
|
118
|
+
const promises = updateStylesWithSCS( X, [], parent );
|
|
119
|
+
const { childNodes } = parent;
|
|
120
|
+
|
|
121
|
+
// No new promises should be generated.
|
|
122
|
+
expect( promises.length ).toBe( 0 );
|
|
123
|
+
|
|
124
|
+
// The DOM should still only have the original X elements.
|
|
125
|
+
expect( childNodes.length ).toBe( X.length );
|
|
126
|
+
expect( childNodes[ 0 ] ).toBe( style1 );
|
|
127
|
+
expect( childNodes[ 1 ] ).toBe( style2 );
|
|
128
|
+
|
|
129
|
+
// Verify the correct media attribute is set.
|
|
130
|
+
expect( childNodes[ 0 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
131
|
+
expect( childNodes[ 1 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
it( 'should keep existing elements when they match in both X and Y', () => {
|
|
135
|
+
const X = [
|
|
136
|
+
createStyleElement( 'style1' ),
|
|
137
|
+
createLinkElement( 'link1' ),
|
|
138
|
+
];
|
|
139
|
+
const Y = [
|
|
140
|
+
createStyleElement( 'style1' ),
|
|
141
|
+
createLinkElement( 'link1' ),
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
parent.append( ...X );
|
|
145
|
+
|
|
146
|
+
const promises = updateStylesWithSCS( X, Y, parent );
|
|
147
|
+
const { childNodes } = parent;
|
|
148
|
+
|
|
149
|
+
expect( promises.length ).toBe( 2 );
|
|
150
|
+
expect( childNodes.length ).toBe( 2 );
|
|
151
|
+
|
|
152
|
+
// Should maintain the original elements (not replace with clones).
|
|
153
|
+
expect( childNodes[ 0 ] ).toBe( X[ 0 ] );
|
|
154
|
+
expect( childNodes[ 1 ] ).toBe( X[ 1 ] );
|
|
155
|
+
|
|
156
|
+
// Verify the correct media attribute is set.
|
|
157
|
+
expect( childNodes[ 0 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
158
|
+
expect( childNodes[ 1 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
159
|
+
} );
|
|
160
|
+
|
|
161
|
+
it( 'should insert new elements from Y in correct positions relative to X', () => {
|
|
162
|
+
const X = [
|
|
163
|
+
createStyleElement( 'style1' ),
|
|
164
|
+
createStyleElement( 'style3' ),
|
|
165
|
+
];
|
|
166
|
+
const Y = [
|
|
167
|
+
createStyleElement( 'style1' ),
|
|
168
|
+
createStyleElement( 'style2' ),
|
|
169
|
+
createStyleElement( 'style3' ),
|
|
170
|
+
createStyleElement( 'style4' ),
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
parent.append( ...X );
|
|
174
|
+
|
|
175
|
+
const promises = updateStylesWithSCS( X, Y, parent );
|
|
176
|
+
const { childNodes } = parent;
|
|
177
|
+
|
|
178
|
+
expect( promises.length ).toBe( 4 );
|
|
179
|
+
expect( childNodes.length ).toBe( 4 );
|
|
180
|
+
expect( childNodes[ 0 ] ).toBe( X[ 0 ] );
|
|
181
|
+
expect( childNodes[ 1 ] ).toBe( Y[ 1 ] );
|
|
182
|
+
expect( childNodes[ 2 ] ).toBe( X[ 1 ] );
|
|
183
|
+
expect( childNodes[ 3 ] ).toBe( Y[ 3 ] );
|
|
184
|
+
|
|
185
|
+
// Verify the correct media attribute is set.
|
|
186
|
+
expect( childNodes[ 0 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
187
|
+
expect( childNodes[ 1 ] ).toHaveAttribute( 'media', 'preload' );
|
|
188
|
+
expect( childNodes[ 2 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
189
|
+
expect( childNodes[ 3 ] ).toHaveAttribute( 'media', 'preload' );
|
|
190
|
+
} );
|
|
191
|
+
|
|
192
|
+
it( 'should handle Y having completely different elements than X', () => {
|
|
193
|
+
const X = [
|
|
194
|
+
createStyleElement( 'style1' ),
|
|
195
|
+
createStyleElement( 'style2' ),
|
|
196
|
+
];
|
|
197
|
+
const Y = [
|
|
198
|
+
createStyleElement( 'style3' ),
|
|
199
|
+
createStyleElement( 'style4' ),
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
parent.append( ...X );
|
|
203
|
+
|
|
204
|
+
const promises = updateStylesWithSCS( X, Y, parent );
|
|
205
|
+
const { childNodes } = parent;
|
|
206
|
+
|
|
207
|
+
expect( promises.length ).toBe( 2 );
|
|
208
|
+
|
|
209
|
+
// Check the specific order - based on the SCS algorithm.
|
|
210
|
+
// When X and Y are completely different, the SCS places.
|
|
211
|
+
// all elements from Y before X.
|
|
212
|
+
expect( childNodes[ 0 ] ).toBe( Y[ 0 ] );
|
|
213
|
+
expect( childNodes[ 1 ] ).toBe( Y[ 1 ] );
|
|
214
|
+
expect( childNodes[ 2 ] ).toBe( X[ 0 ] );
|
|
215
|
+
expect( childNodes[ 3 ] ).toBe( X[ 1 ] );
|
|
216
|
+
|
|
217
|
+
// Verify the correct media attribute is set.
|
|
218
|
+
expect( childNodes[ 0 ] ).toHaveAttribute( 'media', 'preload' );
|
|
219
|
+
expect( childNodes[ 1 ] ).toHaveAttribute( 'media', 'preload' );
|
|
220
|
+
expect( childNodes[ 2 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
221
|
+
expect( childNodes[ 3 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
222
|
+
} );
|
|
223
|
+
|
|
224
|
+
it( 'should consider normalized media attributes when comparing elements', () => {
|
|
225
|
+
// These should be considered equal after normalizing the media attribute.
|
|
226
|
+
const X = [ createLinkElement( 'same-link' ) ];
|
|
227
|
+
const Y = [ createLinkElement( 'same-link' ) ];
|
|
228
|
+
|
|
229
|
+
X[ 0 ].setAttribute( 'media', 'preload' );
|
|
230
|
+
X[ 0 ].dataset.originalMedia = 'screen';
|
|
231
|
+
|
|
232
|
+
Y[ 0 ].setAttribute( 'media', 'screen' );
|
|
233
|
+
|
|
234
|
+
parent.append( ...X );
|
|
235
|
+
|
|
236
|
+
const promises = updateStylesWithSCS( X, Y, parent );
|
|
237
|
+
const { childNodes } = parent;
|
|
238
|
+
|
|
239
|
+
expect( childNodes.length ).toBe( 1 );
|
|
240
|
+
expect( promises.length ).toBe( 1 );
|
|
241
|
+
expect( childNodes[ 0 ] ).toBe( X[ 0 ] );
|
|
242
|
+
|
|
243
|
+
// Verify the media attribute has not changed.
|
|
244
|
+
expect( childNodes[ 0 ] ).toHaveAttribute( 'media', 'preload' );
|
|
245
|
+
} );
|
|
246
|
+
|
|
247
|
+
it( 'should treat style elements as already loaded', async () => {
|
|
248
|
+
const X = [ createStyleElement( 'style1' ) ];
|
|
249
|
+
const Y = [ createStyleElement( 'style1' ) ];
|
|
250
|
+
|
|
251
|
+
parent.append( ...X );
|
|
252
|
+
|
|
253
|
+
const promises = updateStylesWithSCS( X, Y, parent );
|
|
254
|
+
|
|
255
|
+
expect( promises.length ).toBe( 1 );
|
|
256
|
+
expect( await promises[ 0 ] ).toBe( X[ 0 ] );
|
|
257
|
+
} );
|
|
258
|
+
|
|
259
|
+
it( 'should return the same promise for equivalent link elements', () => {
|
|
260
|
+
const X = [ createLinkElement( 'link1' ) ];
|
|
261
|
+
const Y = [ createLinkElement( 'link1' ) ];
|
|
262
|
+
|
|
263
|
+
// First, add it to the DOM.
|
|
264
|
+
const promises1 = updateStylesWithSCS( [], X, parent );
|
|
265
|
+
|
|
266
|
+
// Then, use it in a second call.
|
|
267
|
+
const promises2 = updateStylesWithSCS( X, Y, parent );
|
|
268
|
+
|
|
269
|
+
// The promises should be the same.
|
|
270
|
+
expect( promises1[ 0 ] ).toBe( promises2[ 0 ] );
|
|
271
|
+
} );
|
|
272
|
+
|
|
273
|
+
it( 'should return the same promise for equivalent style elements', () => {
|
|
274
|
+
const X = [ createStyleElement( 'style1' ) ];
|
|
275
|
+
const Y = [ createStyleElement( 'style1' ) ];
|
|
276
|
+
|
|
277
|
+
// First, add it to the DOM.
|
|
278
|
+
const promises1 = updateStylesWithSCS( [], X, parent );
|
|
279
|
+
|
|
280
|
+
// Then, use it in a second call.
|
|
281
|
+
const promises2 = updateStylesWithSCS( X, Y, parent );
|
|
282
|
+
|
|
283
|
+
// The promises should be the same.
|
|
284
|
+
expect( promises1[ 0 ] ).toBe( promises2[ 0 ] );
|
|
285
|
+
} );
|
|
286
|
+
|
|
287
|
+
it( 'should handle complex reordering of elements maintaining the correct order', async () => {
|
|
288
|
+
// Initial set of elements.
|
|
289
|
+
const style1 = createStyleElement( 'style1' );
|
|
290
|
+
const style2 = createStyleElement( 'style2' );
|
|
291
|
+
const style3 = createStyleElement( 'style3' );
|
|
292
|
+
const style4 = createStyleElement( 'style4' );
|
|
293
|
+
|
|
294
|
+
const X = [ style1, style2, style3, style4 ];
|
|
295
|
+
parent.append( ...X );
|
|
296
|
+
|
|
297
|
+
// New set with reordering and some new elements.
|
|
298
|
+
const newStyle1 = createStyleElement( 'style1' );
|
|
299
|
+
const newStyle3 = createStyleElement( 'style3' );
|
|
300
|
+
const newStyle5 = createStyleElement( 'style5' );
|
|
301
|
+
const newStyle2 = createStyleElement( 'style2' );
|
|
302
|
+
const newStyle6 = createStyleElement( 'style6' );
|
|
303
|
+
|
|
304
|
+
const Y = [ newStyle1, newStyle3, newStyle5, newStyle2, newStyle6 ];
|
|
305
|
+
|
|
306
|
+
const promises = updateStylesWithSCS( X, Y, parent );
|
|
307
|
+
const { childNodes } = parent;
|
|
308
|
+
|
|
309
|
+
expect( promises.length ).toBe( 5 );
|
|
310
|
+
|
|
311
|
+
// Verify the exact order.
|
|
312
|
+
expect( childNodes[ 0 ] ).toBe( style1 );
|
|
313
|
+
expect( childNodes[ 1 ] ).toBe( newStyle3 );
|
|
314
|
+
expect( childNodes[ 2 ] ).toBe( newStyle5 );
|
|
315
|
+
expect( childNodes[ 3 ] ).toBe( style2 );
|
|
316
|
+
expect( childNodes[ 4 ] ).toBe( newStyle6 );
|
|
317
|
+
expect( childNodes[ 5 ] ).toBe( style3 );
|
|
318
|
+
expect( childNodes[ 6 ] ).toBe( style4 );
|
|
319
|
+
|
|
320
|
+
// Verify the promise values.
|
|
321
|
+
const values = await Promise.all( promises );
|
|
322
|
+
expect( values[ 0 ] ).toBe( style1 );
|
|
323
|
+
expect( values[ 1 ] ).toBe( newStyle3 );
|
|
324
|
+
expect( values[ 2 ] ).toBe( newStyle5 );
|
|
325
|
+
expect( values[ 3 ] ).toBe( style2 );
|
|
326
|
+
expect( values[ 4 ] ).toBe( newStyle6 );
|
|
327
|
+
|
|
328
|
+
// Verify the correct media attribute is set.
|
|
329
|
+
expect( childNodes[ 0 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
330
|
+
expect( childNodes[ 1 ] ).toHaveAttribute( 'media', 'preload' );
|
|
331
|
+
expect( childNodes[ 2 ] ).toHaveAttribute( 'media', 'preload' );
|
|
332
|
+
expect( childNodes[ 3 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
333
|
+
expect( childNodes[ 4 ] ).toHaveAttribute( 'media', 'preload' );
|
|
334
|
+
expect( childNodes[ 5 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
335
|
+
expect( childNodes[ 6 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
336
|
+
} );
|
|
337
|
+
|
|
338
|
+
it( 'should handle link elements with load events', async () => {
|
|
339
|
+
const link1 = createLinkElement( 'link1' );
|
|
340
|
+
const promises = updateStylesWithSCS( [], [ link1 ], parent );
|
|
341
|
+
|
|
342
|
+
// Manually trigger a load event.
|
|
343
|
+
link1.dispatchEvent( new Event( 'load' ) );
|
|
344
|
+
|
|
345
|
+
// The promise should resolve with the link element.
|
|
346
|
+
const result = await promises[ 0 ];
|
|
347
|
+
expect( result ).toBe( link1 );
|
|
348
|
+
} );
|
|
349
|
+
|
|
350
|
+
it( 'should reject the promise when a link element fails to load', async () => {
|
|
351
|
+
const link1 = createLinkElement( 'link-fail' );
|
|
352
|
+
// Remove load/error listeners by not adding it to the DOM initially.
|
|
353
|
+
const promises = updateStylesWithSCS( [], [ link1 ], parent );
|
|
354
|
+
|
|
355
|
+
// Simulate an error event.
|
|
356
|
+
const errorEvent = new Event( 'error' );
|
|
357
|
+
link1.dispatchEvent( errorEvent );
|
|
358
|
+
|
|
359
|
+
await expect( promises[ 0 ] ).rejects.toThrow(
|
|
360
|
+
`The style sheet with the following URL failed to load: ${ link1.href }`
|
|
361
|
+
);
|
|
362
|
+
} );
|
|
363
|
+
|
|
364
|
+
it( 'should handle mixed style and link elements', async () => {
|
|
365
|
+
const X = [
|
|
366
|
+
createLinkElement( 'link1' ),
|
|
367
|
+
createStyleElement( 'style1' ),
|
|
368
|
+
createLinkElement( 'link2' ),
|
|
369
|
+
createStyleElement( 'style2' ),
|
|
370
|
+
createStyleElement( 'style3' ),
|
|
371
|
+
];
|
|
372
|
+
parent.append( ...X );
|
|
373
|
+
|
|
374
|
+
const Y = [
|
|
375
|
+
createLinkElement( 'link1' ),
|
|
376
|
+
createLinkElement( 'link3' ),
|
|
377
|
+
createStyleElement( 'style1' ),
|
|
378
|
+
createLinkElement( 'link2' ),
|
|
379
|
+
createStyleElement( 'style2' ),
|
|
380
|
+
createLinkElement( 'link1' ),
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
// Run the update using X and Y.
|
|
384
|
+
const promises = updateStylesWithSCS( X, Y, parent );
|
|
385
|
+
const { childNodes } = parent;
|
|
386
|
+
|
|
387
|
+
// Expected DOM outcome (based on the SCS algorithm):.
|
|
388
|
+
// We expect that the existing link1 and link2 are reused, and the new link3 is inserted.
|
|
389
|
+
// The duplicate occurrences of link1 (via link1Clone and link1 itself in Y).
|
|
390
|
+
// should resolve to the original link1.
|
|
391
|
+
expect( childNodes.length ).toBe( 7 );
|
|
392
|
+
expect( [ ...childNodes ] ).toEqual( [
|
|
393
|
+
X[ 0 ],
|
|
394
|
+
Y[ 1 ],
|
|
395
|
+
X[ 1 ],
|
|
396
|
+
X[ 2 ],
|
|
397
|
+
X[ 3 ],
|
|
398
|
+
Y[ 5 ],
|
|
399
|
+
X[ 4 ],
|
|
400
|
+
] );
|
|
401
|
+
|
|
402
|
+
( childNodes as NodeListOf< StyleElement > ).forEach( ( element ) =>
|
|
403
|
+
element.dispatchEvent( new Event( 'load' ) )
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Verify that the returned promises resolve to the appropriate elements.
|
|
407
|
+
const resolved = await Promise.all( promises );
|
|
408
|
+
|
|
409
|
+
expect( promises.length ).toBe( 6 );
|
|
410
|
+
expect( resolved ).toEqual( [
|
|
411
|
+
X[ 0 ],
|
|
412
|
+
Y[ 1 ],
|
|
413
|
+
X[ 1 ],
|
|
414
|
+
X[ 2 ],
|
|
415
|
+
X[ 3 ],
|
|
416
|
+
Y[ 5 ],
|
|
417
|
+
] );
|
|
418
|
+
|
|
419
|
+
// Verify the correct media attribute is set.
|
|
420
|
+
expect( childNodes[ 0 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
421
|
+
expect( childNodes[ 1 ] ).toHaveAttribute( 'media', 'preload' );
|
|
422
|
+
expect( childNodes[ 2 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
423
|
+
expect( childNodes[ 3 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
424
|
+
expect( childNodes[ 4 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
425
|
+
expect( childNodes[ 5 ] ).toHaveAttribute( 'media', 'preload' );
|
|
426
|
+
expect( childNodes[ 6 ] ).not.toHaveAttribute( 'media', 'preload' );
|
|
427
|
+
} );
|
|
428
|
+
|
|
429
|
+
it( 'should set media and data-original-media correctly on new elements', async () => {
|
|
430
|
+
const X = [
|
|
431
|
+
createStyleElement( 'style1' ),
|
|
432
|
+
createStyleElement( 'style1Media' ),
|
|
433
|
+
createLinkElement( 'link1' ),
|
|
434
|
+
createLinkElement( 'link1Media' ),
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
X[ 1 ].setAttribute( 'media', 'screen' ); // style1Media
|
|
438
|
+
X[ 3 ].setAttribute( 'media', 'screen' ); // link1Media
|
|
439
|
+
|
|
440
|
+
mockSheet( X[ 2 ], { disabled: false, mediaText: 'all' } ); // link1
|
|
441
|
+
mockSheet( X[ 3 ], { disabled: false, mediaText: 'screen' } ); // link1Media
|
|
442
|
+
|
|
443
|
+
parent.append( ...X );
|
|
444
|
+
|
|
445
|
+
const Y = [
|
|
446
|
+
...X.map( ( e ) => e.cloneNode( true ) as StyleElement ),
|
|
447
|
+
createStyleElement( 'style2' ),
|
|
448
|
+
createStyleElement( 'style2Media' ),
|
|
449
|
+
createLinkElement( 'link2' ),
|
|
450
|
+
createLinkElement( 'link2Media' ),
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
Y[ 5 ].setAttribute( 'media', 'screen' ); // style2Media
|
|
454
|
+
Y[ 7 ].setAttribute( 'media', 'screen' ); // link2Media
|
|
455
|
+
|
|
456
|
+
const promises = updateStylesWithSCS( X, Y, parent );
|
|
457
|
+
|
|
458
|
+
( parent.childNodes as NodeListOf< StyleElement > ).forEach(
|
|
459
|
+
( element ) => element.dispatchEvent( new Event( 'load' ) )
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const elements = await Promise.all( promises );
|
|
463
|
+
|
|
464
|
+
const attributes = elements.map( ( e ) => ( {
|
|
465
|
+
id: e.id,
|
|
466
|
+
media: e.getAttribute( 'media' ),
|
|
467
|
+
originalMedia: e.getAttribute( 'data-original-media' ),
|
|
468
|
+
} ) );
|
|
469
|
+
|
|
470
|
+
expect( attributes ).toEqual( [
|
|
471
|
+
{
|
|
472
|
+
id: 'style1',
|
|
473
|
+
media: null,
|
|
474
|
+
originalMedia: null,
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
id: 'style1Media',
|
|
478
|
+
media: 'screen',
|
|
479
|
+
originalMedia: null,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: 'link1',
|
|
483
|
+
media: null,
|
|
484
|
+
originalMedia: null,
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
id: 'link1Media',
|
|
488
|
+
media: 'screen',
|
|
489
|
+
originalMedia: null,
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: 'style2',
|
|
493
|
+
media: 'preload',
|
|
494
|
+
originalMedia: null,
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
id: 'style2Media',
|
|
498
|
+
media: 'preload',
|
|
499
|
+
originalMedia: 'screen',
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
id: 'link2',
|
|
503
|
+
media: 'preload',
|
|
504
|
+
originalMedia: null,
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: 'link2Media',
|
|
508
|
+
media: 'preload',
|
|
509
|
+
originalMedia: 'screen',
|
|
510
|
+
},
|
|
511
|
+
] );
|
|
512
|
+
} );
|
|
513
|
+
} );
|
|
514
|
+
|
|
515
|
+
// Tests for preloadStyles function.
|
|
516
|
+
describe( 'preloadStyles', () => {
|
|
517
|
+
it( 'should use cached promises for the same URL', () => {
|
|
518
|
+
// Create a test document.
|
|
519
|
+
const doc = document.implementation.createHTMLDocument();
|
|
520
|
+
const style = doc.createElement( 'style' );
|
|
521
|
+
doc.head.appendChild( style );
|
|
522
|
+
|
|
523
|
+
// First call should update the DOM.
|
|
524
|
+
const firstResult = preloadStyles(
|
|
525
|
+
doc,
|
|
526
|
+
'https://example.com/test'
|
|
527
|
+
);
|
|
528
|
+
expect( firstResult ).toBeTruthy();
|
|
529
|
+
|
|
530
|
+
// Second call should return the same promises.
|
|
531
|
+
const secondResult = preloadStyles(
|
|
532
|
+
doc,
|
|
533
|
+
'https://example.com/test'
|
|
534
|
+
);
|
|
535
|
+
expect( secondResult ).toBe( firstResult );
|
|
536
|
+
} );
|
|
537
|
+
|
|
538
|
+
it( 'should extract style elements from the provided document', () => {
|
|
539
|
+
// Create a test document with style elements.
|
|
540
|
+
const doc = document.implementation.createHTMLDocument();
|
|
541
|
+
const style1 = doc.createElement( 'style' );
|
|
542
|
+
style1.id = 'test-style-1';
|
|
543
|
+
const style2 = doc.createElement( 'link' );
|
|
544
|
+
style2.id = 'test-link-1';
|
|
545
|
+
style2.rel = 'stylesheet';
|
|
546
|
+
doc.head.appendChild( style1 );
|
|
547
|
+
doc.head.appendChild( style2 );
|
|
548
|
+
|
|
549
|
+
preloadStyles( doc, 'https://example.com/another-test-page' );
|
|
550
|
+
|
|
551
|
+
// Check that styles were extracted and added to the document.
|
|
552
|
+
const addedStyle1 = document.querySelector( '#test-style-1' );
|
|
553
|
+
const addedLink1 = document.querySelector( '#test-link-1' );
|
|
554
|
+
expect( addedStyle1 ).toBeTruthy();
|
|
555
|
+
expect( addedLink1 ).toBeTruthy();
|
|
556
|
+
expect( addedStyle1 ).toHaveAttribute( 'media', 'preload' );
|
|
557
|
+
expect( addedLink1 ).toHaveAttribute( 'media', 'preload' );
|
|
558
|
+
} );
|
|
559
|
+
} );
|
|
560
|
+
|
|
561
|
+
// Tests for applyStyles function.
|
|
562
|
+
describe( 'applyStyles', () => {
|
|
563
|
+
it( 'should enable included styles and disable others', () => {
|
|
564
|
+
// Create some style elements.
|
|
565
|
+
const style1 = createStyleElement( 'style1' );
|
|
566
|
+
const style2 = createStyleElement( 'style2' );
|
|
567
|
+
const style3 = createStyleElement( 'style3' );
|
|
568
|
+
|
|
569
|
+
// Add all to document.
|
|
570
|
+
document.head.appendChild( style1 );
|
|
571
|
+
document.head.appendChild( style2 );
|
|
572
|
+
document.head.appendChild( style3 );
|
|
573
|
+
|
|
574
|
+
// Init `sheet` property.
|
|
575
|
+
mockSheet( style1, { disabled: true, mediaText: 'all' } );
|
|
576
|
+
mockSheet( style2, { disabled: false, mediaText: 'all' } );
|
|
577
|
+
mockSheet( style3, { disabled: false, mediaText: 'preload' } );
|
|
578
|
+
|
|
579
|
+
// Apply styles to only style1 and style3.
|
|
580
|
+
applyStyles( [ style1, style3 ] );
|
|
581
|
+
|
|
582
|
+
// style1 and style3 should be enabled, style2 should be disabled.
|
|
583
|
+
expect( style1.sheet!.disabled ).toBe( false );
|
|
584
|
+
expect( style2.sheet!.disabled ).toBe( true );
|
|
585
|
+
expect( style3.sheet!.disabled ).toBe( false );
|
|
586
|
+
} );
|
|
587
|
+
|
|
588
|
+
it( 'should set media appropriately based on originalMedia', () => {
|
|
589
|
+
// Create link elements with originalMedia.
|
|
590
|
+
const link1 = createLinkElement( 'link1' );
|
|
591
|
+
link1.setAttribute( 'media', 'preload' );
|
|
592
|
+
link1.dataset.originalMedia = 'print';
|
|
593
|
+
const link2 = createLinkElement( 'link2' );
|
|
594
|
+
link2.setAttribute( 'media', 'preload' );
|
|
595
|
+
link2.dataset.originalMedia = 'screen';
|
|
596
|
+
|
|
597
|
+
// Add to document.
|
|
598
|
+
document.head.appendChild( link1 );
|
|
599
|
+
document.head.appendChild( link2 );
|
|
600
|
+
|
|
601
|
+
// Init `sheet` property.
|
|
602
|
+
mockSheet( link1, { disabled: false, mediaText: 'preload' } );
|
|
603
|
+
mockSheet( link2, { disabled: false, mediaText: 'preload' } );
|
|
604
|
+
|
|
605
|
+
// Apply styles.
|
|
606
|
+
applyStyles( [ link1, link2 ] );
|
|
607
|
+
|
|
608
|
+
// Check that media was set correctly.
|
|
609
|
+
expect( link1.sheet!.media.mediaText ).toBe( 'print' );
|
|
610
|
+
expect( link2.sheet!.media.mediaText ).toBe( 'screen' );
|
|
611
|
+
} );
|
|
612
|
+
|
|
613
|
+
it( 'should use "all" as default media if no originalMedia specified', () => {
|
|
614
|
+
// Create elements without originalMedia.
|
|
615
|
+
const link = createLinkElement( 'link' );
|
|
616
|
+
const style = createStyleElement( 'style' );
|
|
617
|
+
link.setAttribute( 'media', 'preload' );
|
|
618
|
+
style.setAttribute( 'media', 'preload' );
|
|
619
|
+
|
|
620
|
+
// Add to document.
|
|
621
|
+
document.head.append( link, style );
|
|
622
|
+
|
|
623
|
+
// Init `sheet` property.
|
|
624
|
+
mockSheet( link, { disabled: false, mediaText: 'preload' } );
|
|
625
|
+
mockSheet( style, { disabled: false, mediaText: 'preload' } );
|
|
626
|
+
|
|
627
|
+
// Apply styles.
|
|
628
|
+
applyStyles( [ link, style ] );
|
|
629
|
+
|
|
630
|
+
// Default should be "all".
|
|
631
|
+
expect( link.sheet!.media.mediaText ).toBe( 'all' );
|
|
632
|
+
expect( style.sheet!.media.mediaText ).toBe( 'all' );
|
|
633
|
+
} );
|
|
634
|
+
} );
|
|
635
|
+
|
|
636
|
+
describe( 'normalizeMedia', () => {
|
|
637
|
+
let linkElement: HTMLLinkElement;
|
|
638
|
+
let styleElement: HTMLStyleElement;
|
|
639
|
+
|
|
640
|
+
beforeEach( () => {
|
|
641
|
+
// Create fresh elements before each test
|
|
642
|
+
linkElement = document.createElement( 'link' );
|
|
643
|
+
linkElement.rel = 'stylesheet';
|
|
644
|
+
linkElement.href = './styles.css';
|
|
645
|
+
|
|
646
|
+
styleElement = document.createElement( 'style' );
|
|
647
|
+
styleElement.textContent = 'body { color: red; }';
|
|
648
|
+
} );
|
|
649
|
+
|
|
650
|
+
it( 'should always return a clone of the element', () => {
|
|
651
|
+
// Test with no media attribute
|
|
652
|
+
const result1 = normalizeMedia( linkElement );
|
|
653
|
+
expect( result1 ).not.toBe( linkElement );
|
|
654
|
+
expect( result1 ).toHaveAttribute( 'media', 'all' );
|
|
655
|
+
|
|
656
|
+
// Test with media attribute other than "preload"
|
|
657
|
+
linkElement.setAttribute( 'media', 'all' );
|
|
658
|
+
const result2 = normalizeMedia( linkElement );
|
|
659
|
+
expect( result2 ).not.toBe( linkElement );
|
|
660
|
+
expect( result2 ).toHaveAttribute( 'media', 'all' );
|
|
661
|
+
|
|
662
|
+
linkElement.setAttribute( 'media', 'preload' );
|
|
663
|
+
const result3 = normalizeMedia( linkElement );
|
|
664
|
+
expect( result3 ).not.toBe( linkElement );
|
|
665
|
+
expect( result3 ).toHaveAttribute(
|
|
666
|
+
'href',
|
|
667
|
+
linkElement.getAttribute( 'href' )
|
|
668
|
+
);
|
|
669
|
+
} );
|
|
670
|
+
|
|
671
|
+
it( 'should remove media attribute when media="preload" and no data-original-media exists', () => {
|
|
672
|
+
linkElement.setAttribute( 'media', 'preload' );
|
|
673
|
+
const result = normalizeMedia( linkElement );
|
|
674
|
+
expect( result ).toHaveAttribute( 'media', 'all' );
|
|
675
|
+
} );
|
|
676
|
+
|
|
677
|
+
it( 'should restore original media when media="preload" and data-original-media exists', () => {
|
|
678
|
+
linkElement.setAttribute( 'media', 'preload' );
|
|
679
|
+
linkElement.dataset.originalMedia = 'all';
|
|
680
|
+
const result = normalizeMedia( linkElement );
|
|
681
|
+
expect( result ).toHaveAttribute( 'media', 'all' );
|
|
682
|
+
expect( result.dataset.originalMedia ).toBeUndefined();
|
|
683
|
+
} );
|
|
684
|
+
|
|
685
|
+
it( 'should work with style elements too', () => {
|
|
686
|
+
styleElement.setAttribute( 'media', 'preload' );
|
|
687
|
+
styleElement.dataset.originalMedia = 'print';
|
|
688
|
+
const result = normalizeMedia( styleElement );
|
|
689
|
+
expect( result ).not.toBe( styleElement );
|
|
690
|
+
expect( result ).toHaveAttribute( 'media', 'print' );
|
|
691
|
+
expect( result.dataset.originalMedia ).toBeUndefined();
|
|
692
|
+
} );
|
|
693
|
+
|
|
694
|
+
it( 'should handle empty original media correctly', () => {
|
|
695
|
+
linkElement.setAttribute( 'media', 'preload' );
|
|
696
|
+
linkElement.dataset.originalMedia = '';
|
|
697
|
+
const result = normalizeMedia( linkElement );
|
|
698
|
+
expect( result ).toHaveAttribute( 'media', 'all' );
|
|
699
|
+
expect( result.dataset.originalMedia ).toBeUndefined();
|
|
700
|
+
} );
|
|
701
|
+
|
|
702
|
+
it( 'should preserve other attributes when normalizing', () => {
|
|
703
|
+
linkElement.setAttribute( 'media', 'preload' );
|
|
704
|
+
linkElement.dataset.originalMedia = 'all';
|
|
705
|
+
linkElement.setAttribute( 'data-test', 'value' );
|
|
706
|
+
linkElement.setAttribute( 'integrity', 'hash123' );
|
|
707
|
+
|
|
708
|
+
const result = normalizeMedia( linkElement );
|
|
709
|
+
expect( result ).toHaveAttribute( 'media', 'all' );
|
|
710
|
+
expect( result ).toHaveAttribute( 'data-test', 'value' );
|
|
711
|
+
expect( result ).toHaveAttribute( 'integrity', 'hash123' );
|
|
712
|
+
} );
|
|
713
|
+
|
|
714
|
+
it( 'should output the same for equivalent link elements', () => {
|
|
715
|
+
const head = document.createElement( 'head' );
|
|
716
|
+
head.innerHTML = `
|
|
717
|
+
<link rel="stylesheet" src="./assets/styles.css">
|
|
718
|
+
<link rel="stylesheet" src="./assets/styles.css" media="all">
|
|
719
|
+
<link rel="stylesheet" src="./assets/styles.css" media="preload">
|
|
720
|
+
<link rel="stylesheet" src="./assets/styles.css" media="preload" data-original-media="all">
|
|
721
|
+
`;
|
|
722
|
+
|
|
723
|
+
const links = [ ...head.childNodes ]
|
|
724
|
+
.filter( ( e ) => e instanceof HTMLLinkElement )
|
|
725
|
+
.map( normalizeMedia );
|
|
726
|
+
|
|
727
|
+
expect( links[ 0 ].isEqualNode( links[ 1 ] ) ).toBe( true );
|
|
728
|
+
expect( links[ 0 ].isEqualNode( links[ 2 ] ) ).toBe( true );
|
|
729
|
+
expect( links[ 0 ].isEqualNode( links[ 3 ] ) ).toBe( true );
|
|
730
|
+
} );
|
|
731
|
+
|
|
732
|
+
it( 'should output the same for equivalent style elements', () => {
|
|
733
|
+
const head = document.createElement( 'head' );
|
|
734
|
+
head.innerHTML = `
|
|
735
|
+
<style>
|
|
736
|
+
body { background: pink; }
|
|
737
|
+
</style>
|
|
738
|
+
<style media="all">
|
|
739
|
+
body { background: pink; }
|
|
740
|
+
</style>
|
|
741
|
+
<style media="preload">
|
|
742
|
+
body { background: pink; }
|
|
743
|
+
</style>
|
|
744
|
+
<style media="preload" data-original-media="all">
|
|
745
|
+
body { background: pink; }
|
|
746
|
+
</style>
|
|
747
|
+
`;
|
|
748
|
+
|
|
749
|
+
const styles = [ ...head.childNodes ]
|
|
750
|
+
.filter( ( e ) => e instanceof HTMLStyleElement )
|
|
751
|
+
.map( normalizeMedia );
|
|
752
|
+
|
|
753
|
+
expect( styles[ 0 ].isEqualNode( styles[ 1 ] ) ).toBe( true );
|
|
754
|
+
expect( styles[ 0 ].isEqualNode( styles[ 2 ] ) ).toBe( true );
|
|
755
|
+
expect( styles[ 0 ].isEqualNode( styles[ 3 ] ) ).toBe( true );
|
|
756
|
+
} );
|
|
757
|
+
} );
|
|
758
|
+
} );
|