@sitecore-feaas/clientside 0.5.3 → 0.5.5

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.
Files changed (57) hide show
  1. package/build/components/FEAASComponent.js +17 -6
  2. package/build/components/FEAASComponent.js.map +1 -1
  3. package/build/dom/stylesheet.js +11 -12
  4. package/build/dom/stylesheet.js.map +1 -1
  5. package/build/fetch.js +25 -1
  6. package/build/fetch.js.map +1 -1
  7. package/build/utils/settings.js +2 -2
  8. package/build/utils/settings.js.map +1 -1
  9. package/dist/browser/headless.cjs +4 -4
  10. package/dist/browser/headless.cjs.map +3 -3
  11. package/dist/browser/headless.esm.js +5 -5
  12. package/dist/browser/headless.esm.js.map +1 -1
  13. package/dist/browser/index.cjs +4 -4
  14. package/dist/browser/index.cjs.map +3 -3
  15. package/dist/browser/index.esm.js +5 -5
  16. package/dist/browser/index.esm.js.map +1 -1
  17. package/dist/browser/react.cjs +5 -5
  18. package/dist/browser/react.cjs.map +3 -3
  19. package/dist/browser/react.esm.js +5 -5
  20. package/dist/browser/react.esm.js.map +1 -1
  21. package/dist/node/headless.cjs +4 -4
  22. package/dist/node/headless.cjs.map +3 -3
  23. package/dist/node/headless.esm.js +4 -4
  24. package/dist/node/headless.esm.js.map +3 -3
  25. package/dist/node/index.cjs +4 -4
  26. package/dist/node/index.cjs.map +3 -3
  27. package/dist/node/index.esm.js +4 -4
  28. package/dist/node/index.esm.js.map +3 -3
  29. package/dist/node/react.cjs +4 -4
  30. package/dist/node/react.cjs.map +3 -3
  31. package/dist/node/react.esm.js +3 -3
  32. package/dist/node/react.esm.js.map +3 -3
  33. package/headless.cjs +4 -4
  34. package/headless.cjs.map +3 -3
  35. package/headless.js +4 -4
  36. package/headless.js.map +3 -3
  37. package/package.json +1 -1
  38. package/react.cjs +5 -5
  39. package/react.cjs.map +3 -3
  40. package/react.js +5 -5
  41. package/react.js.map +3 -3
  42. package/src/components/FEAASComponent.tsx +21 -7
  43. package/src/dom/stylesheet.ts +17 -15
  44. package/src/fetch.ts +25 -1
  45. package/src/utils/settings.ts +2 -2
  46. package/test/FEAASComponent.test.tsx +144 -47
  47. package/test/FEAASStylesheet.test.tsx +13 -11
  48. package/test/datasettings.test.ts +92 -0
  49. package/types/components/FEAASComponent.d.ts +1 -0
  50. package/types/components/FEAASComponent.d.ts.map +1 -1
  51. package/types/dom/stylesheet.d.ts +1 -1
  52. package/types/dom/stylesheet.d.ts.map +1 -1
  53. package/types/fetch.d.ts +24 -0
  54. package/types/fetch.d.ts.map +1 -1
  55. package/types/ui/FEAASPicker.d.ts +3 -3
  56. package/types/utils/settings.d.ts +6 -6
  57. package/types/utils/settings.d.ts.map +1 -1
@@ -104,6 +104,7 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
104
104
  } as Partial<FEAASComponentsProps>
105
105
 
106
106
  needsRefresh: boolean
107
+ needsToFetchTemplate: boolean
107
108
 
108
109
  setData(data: DataOptions) {
109
110
  this.set({ data })
@@ -123,14 +124,19 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
123
124
  // force component to be refreshed
124
125
  if (name == 'library' || name == 'component' || name == 'revision' || name == 'version') {
125
126
  this.payload = undefined
127
+ if (this.alreadyRendered) {
128
+ this.needsRefresh = true
129
+ this.needsToFetchTemplate = true
130
+ }
126
131
  }
127
132
 
128
- this.needsRefresh = true
129
-
130
133
  super.attributeChangedCallback(name, oldValue, newValue)
131
134
 
132
- if (name === 'data' && this.props.fetch.includes('data')) {
133
- this.fetchData()
135
+ if (name == 'data') {
136
+ this.needsRefresh = true
137
+ if (this.props.fetch.includes('data') || this.alreadyRendered || !DataSettings.hasDataSettings(this.props.data)) {
138
+ this.fetchData()
139
+ }
134
140
  }
135
141
  }
136
142
 
@@ -157,6 +163,7 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
157
163
  constructor() {
158
164
  super()
159
165
 
166
+ const data = this.getAttribute('data')
160
167
  if (!this.getAttribute('data')) {
161
168
  this.whenDataReady.resolve({})
162
169
  }
@@ -225,7 +232,8 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
225
232
  })
226
233
  }
227
234
 
228
- if (this.props.fetch.includes('template')) {
235
+ if (this.props.fetch.includes('template') || this.needsToFetchTemplate) {
236
+ this.needsToFetchTemplate = false
229
237
  var templatePromise = fetchAndRevalidateComponent(this.props, (html) => {
230
238
  this.payload = html
231
239
  advanceIfEverythingIsReady()
@@ -252,11 +260,17 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
252
260
  update() {
253
261
  this.alreadyRendered = true
254
262
  // avoid unnecessary rendering if hydration was disabled
255
- if (!this.props.fetch.includes('data') && !this.props.fetch.includes('template') && this.firstElementChild) return
263
+ if (
264
+ !this.props.fetch.includes('data') &&
265
+ !this.props.fetch.includes('template') &&
266
+ this.firstElementChild &&
267
+ !this.needsRefresh
268
+ )
269
+ return
256
270
  if (Array.from(this.children).filter((c) => c.tagName != 'BR').length > 0 && !this.needsRefresh) {
257
271
  renderDOMElement(this, this.readyData)
258
272
  } else {
259
- this.needsRefresh = true
273
+ this.needsRefresh = false
260
274
  // defer rendering to original element
261
275
  if (this.getOriginal()) {
262
276
  return
@@ -17,23 +17,25 @@ export function renderStylesheetPromise(props: FEAASCDNStylesheetProps, styleshe
17
17
  }).then(() => stylesheet)
18
18
  }
19
19
 
20
+ const validatedStylesheets = new WeakSet<HTMLStyleElement | HTMLLinkElement>()
20
21
  export function loadStylesheet(props: FEAASCDNStylesheetProps, callback?: ValidatableStylesheetCallback) {
21
- var foundStylesheet = findStylesheet(props) as HTMLStyleElement
22
- if (!foundStylesheet) {
23
- var createdStylesheet = createStylesheet(props)
24
- } else {
25
- if (foundStylesheet.tagName == 'LINK') {
26
- callback(null, 'initial')
27
- return Promise.resolve(foundStylesheet)
28
- }
22
+ var foundStylesheet: HTMLStyleElement | HTMLLinkElement
23
+ var writableStylesheet: HTMLStyleElement
24
+
25
+ if (foundStylesheet && validatedStylesheets.has(foundStylesheet)) {
26
+ return Promise.resolve(foundStylesheet)
29
27
  }
30
28
 
31
29
  return fetchAndRevalidateStylesheet(props, (cssText, phase) => {
32
- if (createdStylesheet) {
33
- createdStylesheet.textContent = cssText
30
+ foundStylesheet ||= findStylesheet(props)
31
+ // dont need to write stylesheet for cached state if we already found a stylesheet
32
+ if (!(phase == 'initial' && foundStylesheet)) {
33
+ writableStylesheet ||=
34
+ !foundStylesheet || foundStylesheet.tagName == 'LINK' ? createStylesheet(props) : foundStylesheet
35
+ writableStylesheet.textContent = cssText
34
36
  }
35
37
  callback?.(cssText, phase)
36
- }).then(() => foundStylesheet || createdStylesheet)
38
+ }).then(() => foundStylesheet || writableStylesheet)
37
39
  }
38
40
 
39
41
  export function loadStylesheetAllowStale(props: FEAASCDNStylesheetProps) {
@@ -43,10 +45,10 @@ export function loadStylesheetAllowStale(props: FEAASCDNStylesheetProps) {
43
45
  }
44
46
 
45
47
  export function findStylesheet(props: FEAASCDNStylesheetProps) {
46
- return (
47
- document.head.querySelector(`style[data-href="${getStylesheetURL(props)}"]`) ||
48
- document.querySelector(`link[rel="stylesheet"][href="${getStylesheetURL(props)}"]`)
49
- )
48
+ return (document.head.querySelector(`style[data-href="${getStylesheetURL(props)}"]`) ||
49
+ document.querySelector(`link[rel="stylesheet"][href="${getStylesheetURL(props)}"]`)) as
50
+ | HTMLStyleElement
51
+ | HTMLLinkElement
50
52
  }
51
53
 
52
54
  export function createStylesheet(props: FEAASCDNStylesheetProps) {
package/src/fetch.ts CHANGED
@@ -68,6 +68,22 @@ export function getFetchCache(url: Parameters<typeof fetch>[0], dontCreate = fal
68
68
 
69
69
  export class InvalidResponse extends Error {}
70
70
 
71
+ /**
72
+ * Executes a fetch request optionally validating and handling the response. Using callback function it's possible to
73
+ * observe successful response twice: Cached version first, and up-to-date later.
74
+ *
75
+ * @param {Object} request - An object containing request parameters.
76
+ * @param {string} request.url - The URL to fetch.
77
+ * @param {Object} request.options - Fetch request options.
78
+ * @param {Function} [request.validator] - Function to validate the response. Should return `false` if
79
+ * validation fails.
80
+ * @param {Function} [request.callback] - Function to execute after fetching. Receives the response,
81
+ * phase, and cache status.
82
+ * @param {Object} [request.cache] - Cache object to store or retrieve cached data.
83
+ * @param {ValidatableRequestPhase} [phase='initial'] - The phase of the request, defaults to 'initial'.
84
+ * @returns {Promise<Object>} A promise that resolves to an object containing request details and the response.
85
+ * @throws {Error} Throws an error if the fetch fails or if the response is not valid according to the validator.
86
+ */
71
87
  export async function fetchWithCacheAwareness(
72
88
  { url, options, validator, callback, cache }: ValidatableRequest,
73
89
  phase: ValidatableRequestPhase = 'initial'
@@ -107,6 +123,14 @@ export async function fetchWithCacheAwareness(
107
123
  }
108
124
  }
109
125
 
126
+ /**
127
+ * Validates cache of previousl fetch If the resource is cached and there is a newer version available, it will be
128
+ * re-fetched.
129
+ *
130
+ * @param request - The request object containing the URL, cache information, options, validator, and response of
131
+ * initial request.
132
+ * @returns A promise that resolves to the request object.
133
+ */
110
134
  export async function fetchToValidateCache(
111
135
  request: ValidatableRequest & Required<Pick<ValidatableRequest, 'response'>>
112
136
  ) {
@@ -129,7 +153,7 @@ export async function fetchToValidateCache(
129
153
 
130
154
  // there's a newer version of the file that we may fetch
131
155
  if (head.headers.get('last-modified') != response.headers.get('last-modified')) {
132
- // otherwise the new resource has to be re-fetched
156
+ // the new resource has to be re-fetched
133
157
  return fetchBypassingCache(request)
134
158
  }
135
159
  }
@@ -52,7 +52,7 @@ export function clean(settings: any) {
52
52
  * @returns {boolean} - True if the data is of type DataSettings, false otherwise.
53
53
  */
54
54
  export const isDataSettings = (data: any): data is DataSettings => {
55
- if (typeof data.url != 'string') return false
55
+ if (!data || typeof data.url != 'string') return false
56
56
 
57
57
  for (var property in data) {
58
58
  if (
@@ -192,7 +192,7 @@ export async function fetchData(data: DataOptions): Promise<DataScopes> {
192
192
  )
193
193
  }
194
194
 
195
- export async function hasDataSettings(data: DataOptions) {
195
+ export function hasDataSettings(data: DataOptions) {
196
196
  if (!data) return false
197
197
  return isDataSettings(data) || Object.values(data).some(isDataSettings)
198
198
  }
@@ -25,7 +25,7 @@ describe('Components', () => {
25
25
  const spy = vi.spyOn(global, 'fetch')
26
26
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
27
27
  if (url.match('CDN/components/L/C/V/R')) {
28
- return new Response('TODAY', {
28
+ return new Response('FRESH', {
29
29
  headers: {
30
30
  date: now.toUTCString(),
31
31
  'last-modified': earlier.toUTCString()
@@ -43,14 +43,21 @@ describe('Components', () => {
43
43
  await expect(component.whenRendered).resolves.toEqual(component)
44
44
  expect(spy).toHaveBeenCalledTimes(1)
45
45
 
46
- expect(component.innerHTML).toContain('TODAY')
46
+ expect(component.innerHTML).toContain('FRESH')
47
47
  })
48
48
 
49
49
  describe('fetch control', () => {
50
- it('should not make requests when hydration is turned off', async () => {
50
+ it('should not make requests when hydration is turned off, unless attribute changed after initial render', async () => {
51
51
  const spy = vi.spyOn(global, 'fetch')
52
-
53
- // Fixme юhange to production component
52
+ spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
53
+ return new Response(url, {
54
+ headers: {
55
+ date: now.toUTCString(),
56
+ 'last-modified': earlier.toUTCString()
57
+ }
58
+ })
59
+ })
60
+ // Fixme change to production component
54
61
  document.body.innerHTML =
55
62
  '<feaas-component fetch="" library="L" cdn="CDN" component="C" version="V" revision="R"></feaas-component>'
56
63
 
@@ -59,7 +66,11 @@ describe('Components', () => {
59
66
  await expect(component.whenRendered).resolves.toEqual(component)
60
67
  expect(spy).toHaveBeenCalledTimes(0)
61
68
 
62
- expect(component.innerHTML).toBe('')
69
+ component.setAttribute('library', 'Z')
70
+ await expect(component.whenRendered).resolves.toEqual(component)
71
+ expect(spy).toHaveBeenCalledTimes(1)
72
+
73
+ expect(component.innerHTML).toBe('CDN/components/Z/C/V/R.html')
63
74
  })
64
75
 
65
76
  it('should not make requests when hydration is turned off, but should render template', async () => {
@@ -81,7 +92,7 @@ describe('Components', () => {
81
92
  const spy = vi.spyOn(global, 'fetch')
82
93
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
83
94
  if (url.match('.css')) {
84
- return new Response('{}', {
95
+ return new Response('.a{}', {
85
96
  headers: {
86
97
  date: now.toUTCString(),
87
98
  'last-modified': earlier.toUTCString()
@@ -105,7 +116,7 @@ describe('Components', () => {
105
116
  const spy = vi.spyOn(global, 'fetch')
106
117
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
107
118
  if (url.match('CDN/components/L/C/V/R')) {
108
- return new Response('TODAY', {
119
+ return new Response('FRESH', {
109
120
  headers: {
110
121
  date: now.toUTCString(),
111
122
  'last-modified': earlier.toUTCString()
@@ -113,7 +124,7 @@ describe('Components', () => {
113
124
  })
114
125
  }
115
126
  if (url.match('.css')) {
116
- return new Response('{}', {
127
+ return new Response('.a{}', {
117
128
  headers: {
118
129
  date: now.toUTCString(),
119
130
  'last-modified': earlier.toUTCString()
@@ -128,7 +139,7 @@ describe('Components', () => {
128
139
 
129
140
  await expect(component.whenRendered).resolves.toEqual(component)
130
141
  expect(spy).toHaveBeenCalledTimes(2)
131
- expect(component.innerHTML).toBe('TODAY')
142
+ expect(component.innerHTML).toBe('FRESH')
132
143
  expect(document.head.querySelectorAll('style').length).toEqual(1)
133
144
  })
134
145
  it('should make data/css requests when fetch="stylesheet data"', async () => {
@@ -143,7 +154,7 @@ describe('Components', () => {
143
154
  })
144
155
  }
145
156
  if (url.match('.css')) {
146
- return new Response('{}', {
157
+ return new Response('.a{}', {
147
158
  headers: {
148
159
  date: now.toUTCString(),
149
160
  'last-modified': earlier.toUTCString()
@@ -163,17 +174,9 @@ describe('Components', () => {
163
174
  expect(document.head.querySelectorAll('style').length).toEqual(1)
164
175
  })
165
176
 
166
- it('should make data/css/html requests when fetch="template stylesheet data"', async () => {
177
+ it('should fetch data even if fetch control doesnt allow it, once data attribute was updated', async () => {
167
178
  const spy = vi.spyOn(global, 'fetch')
168
179
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
169
- if (url.match('.html')) {
170
- return new Response('<var data-path="source.path"></var>', {
171
- headers: {
172
- date: now.toUTCString(),
173
- 'last-modified': earlier.toUTCString()
174
- }
175
- })
176
- }
177
180
  if (url.match('my.json')) {
178
181
  return new Response('{"path": "value"}', {
179
182
  headers: {
@@ -181,26 +184,23 @@ describe('Components', () => {
181
184
  'last-modified': earlier.toUTCString()
182
185
  }
183
186
  })
184
- }
185
- if (url.match('.css')) {
186
- return new Response('{}', {
187
- headers: {
188
- date: now.toUTCString(),
189
- 'last-modified': earlier.toUTCString()
190
- }
191
- })
187
+ } else {
188
+ throw new Error('Unknown fetch url' + url)
192
189
  }
193
190
  })
194
191
 
195
192
  document.body.innerHTML =
196
- '<feaas-component fetch="template stylesheet data" data=\'{"source": {"url": "my.json"}}\' library="L" cdn="CDN" component="C" version="V" revision="R"></feaas-component>'
193
+ '<feaas-component fetch="" data=\'{"source": {"path": "original"}}\' library="L" cdn="CDN" component="C" version="V" revision="R"><var data-path="source.path">original</var></feaas-component>'
197
194
 
198
195
  const component = document.body.firstElementChild as FEAASComponent
199
196
 
200
197
  await expect(component.whenRendered).resolves.toEqual(component)
201
- expect(spy).toHaveBeenCalledTimes(3)
198
+ expect(spy).toHaveBeenCalledTimes(0)
199
+ expect(component.innerHTML).toBe('<var data-path="source.path">original</var>')
200
+ component.setAttribute('data', '{"source": {"url": "my.json"}}')
201
+ await expect(component.whenRendered).resolves.toEqual(component)
202
+ expect(spy).toHaveBeenCalledTimes(1)
202
203
  expect(component.innerHTML).toBe('<var data-path="source.path">value</var>')
203
- expect(document.head.querySelectorAll('style').length).toEqual(1)
204
204
  })
205
205
  })
206
206
 
@@ -208,7 +208,7 @@ describe('Components', () => {
208
208
  const spy = vi.spyOn(global, 'fetch')
209
209
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
210
210
  if (url.match('http://a/components/b/c/d/1.html')) {
211
- return new Response('TODAY', {
211
+ return new Response('FRESH', {
212
212
  headers: {
213
213
  date: now.toUTCString(),
214
214
  'last-modified': earlier.toUTCString()
@@ -226,7 +226,7 @@ describe('Components', () => {
226
226
  expect(spy).toHaveBeenCalledTimes(1)
227
227
 
228
228
  expect(component.outerHTML).toBe(
229
- `<feaas-component fetch="template data" src="http://a/components/b/c/d/1.html" cdn="http://a" library="b" component="c" version="d" revision="1" class="-feaas">TODAY</feaas-component>`
229
+ `<feaas-component fetch="template data" src="http://a/components/b/c/d/1.html" cdn="http://a" library="b" component="c" version="d" revision="1" class="-feaas">FRESH</feaas-component>`
230
230
  )
231
231
  })
232
232
 
@@ -234,7 +234,7 @@ describe('Components', () => {
234
234
  const spy = vi.spyOn(global, 'fetch')
235
235
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
236
236
  if (url.match('http://a/components/b/c/d/1.html')) {
237
- return new Response('TODAY', {
237
+ return new Response('FRESH', {
238
238
  headers: {
239
239
  date: now.toUTCString(),
240
240
  'last-modified': earlier.toUTCString()
@@ -260,16 +260,108 @@ describe('Components', () => {
260
260
  expect(document.head.querySelectorAll('style')[1].getAttribute('data-href')).toBe('http://a/styles/b/staged.css')
261
261
  })
262
262
 
263
- it('should render component if stylesheet is statically embedded as link', async () => {
263
+ it('should validate stylesheet when included as link before rendering component', async () => {
264
+ const spy = vi.spyOn(global, 'fetch')
265
+ spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
266
+ if (url.match('http://a/components/b/c/d/1.html')) {
267
+ return new Response('FRESH', {
268
+ headers: {
269
+ date: now.toUTCString(),
270
+ 'last-modified': earlier.toUTCString()
271
+ }
272
+ })
273
+ } else if (url.match('.css')) {
274
+ return new Response('FRESH', {
275
+ headers: {
276
+ date: /*String(options?.method).toLowerCase() == 'head' ? */ now.toUTCString(),
277
+ 'last-modified': earlier.toUTCString()
278
+ }
279
+ })
280
+ }
281
+ })
282
+
283
+ document.body.innerHTML =
284
+ '<feaas-component src="http://a/components/b/c/d/1.html"></feaas-component><link rel="stylesheet" href="http://a/styles/b/published.css" />'
285
+
286
+ const component = document.body.firstElementChild as FEAASComponent
287
+ await expect(component.whenRendered).resolves.toEqual(component)
288
+ expect(spy).toHaveBeenCalledTimes(2)
289
+ expect(document.head.querySelectorAll('style').length).toBe(0)
290
+ })
291
+
292
+ it('should validate and re-write stylesheet when included as link before rendering component', async () => {
293
+ const spy = vi.spyOn(global, 'fetch')
294
+ spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
295
+ if (url.match('http://a/components/b/c/d/1.html')) {
296
+ return new Response('FRESH', {
297
+ headers: {
298
+ date: now.toUTCString(),
299
+ 'last-modified': earlier.toUTCString()
300
+ }
301
+ })
302
+ } else if (url.match('.css')) {
303
+ if (!options?.cache) {
304
+ // respond with cached css
305
+ return new Response('CACHED', {
306
+ headers: {
307
+ date: earliest.toUTCString(),
308
+ 'last-modified': earliest.toUTCString()
309
+ }
310
+ })
311
+ } else {
312
+ // respond with up to date
313
+ return new Response('CACHED', {
314
+ headers: {
315
+ date: now.toUTCString(),
316
+ 'last-modified': earliest.toUTCString()
317
+ }
318
+ })
319
+ }
320
+ }
321
+ })
322
+
323
+ document.body.innerHTML =
324
+ '<feaas-component src="http://a/components/b/c/d/1.html"></feaas-component><link rel="stylesheet" href="http://a/styles/b/published.css" />'
325
+
326
+ const component = document.body.firstElementChild as FEAASComponent
327
+ await expect(component.whenRendered).resolves.toEqual(component)
328
+ expect(spy).toHaveBeenCalledTimes(3)
329
+ // render with stale stylesheet
330
+ expect(document.head.querySelectorAll('style').length).toBe(0)
331
+ await new Promise((r) => setTimeout(r, 50))
332
+ expect(spy).toHaveBeenCalledTimes(3)
333
+ expect(document.head.querySelectorAll('style').length).toBe(0)
334
+ })
335
+
336
+ it('should validate and re-write stylesheet when included as link before rendering component', async () => {
264
337
  const spy = vi.spyOn(global, 'fetch')
265
338
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
266
339
  if (url.match('http://a/components/b/c/d/1.html')) {
267
- return new Response('TODAY', {
340
+ return new Response('FRESH', {
268
341
  headers: {
269
342
  date: now.toUTCString(),
270
343
  'last-modified': earlier.toUTCString()
271
344
  }
272
345
  })
346
+ } else if (url.match('.css')) {
347
+ if (!options?.cache) {
348
+ // respond with cached css
349
+ return new Response('CACHED', {
350
+ headers: {
351
+ date: earliest.toUTCString(),
352
+ 'last-modified': earliest.toUTCString()
353
+ }
354
+ })
355
+ } else {
356
+ if (String(options?.method).toLowerCase() == 'head') await new Promise((r) => setTimeout(r, 50))
357
+ // respond with up to date
358
+ return new Response('FRESH', {
359
+ headers: {
360
+ date: now.toUTCString(),
361
+ 'last-modified': now.toUTCString()
362
+ }
363
+ })
364
+ }
273
365
  }
274
366
  })
275
367
 
@@ -278,7 +370,12 @@ describe('Components', () => {
278
370
 
279
371
  const component = document.body.firstElementChild as FEAASComponent
280
372
  await expect(component.whenRendered).resolves.toEqual(component)
373
+ expect(spy).toHaveBeenCalledTimes(3)
374
+ // render with stale stylesheet
281
375
  expect(document.head.querySelectorAll('style').length).toBe(0)
376
+ await new Promise((r) => setTimeout(r, 100))
377
+ expect(spy).toHaveBeenCalledTimes(4)
378
+ expect(document.head.querySelectorAll('style').length).toBe(1)
282
379
  })
283
380
 
284
381
  it('should revalidate cached component response', async () => {
@@ -286,7 +383,7 @@ describe('Components', () => {
286
383
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
287
384
  if (url.match('CDN/components/L/C/V/R') && options.method != 'HEAD') {
288
385
  // 1. Here's cached component
289
- return new Response('TODAY', {
386
+ return new Response('CACHED', {
290
387
  headers: {
291
388
  date: earlier.toUTCString(),
292
389
  'last-modified': earlier.toUTCString()
@@ -311,7 +408,7 @@ describe('Components', () => {
311
408
  await expect(component.whenRendered).resolves.toEqual(component)
312
409
  expect(spy).toHaveBeenCalledTimes(2)
313
410
 
314
- expect(component.innerHTML).toContain('TODAY')
411
+ expect(component.innerHTML).toContain('CACHED')
315
412
  })
316
413
 
317
414
  it('should reload component while using stale cache', async () => {
@@ -335,7 +432,7 @@ describe('Components', () => {
335
432
  })
336
433
  } else {
337
434
  // Up to date version
338
- return new Response('TODAY', {
435
+ return new Response('FRESH', {
339
436
  headers: {
340
437
  date: now.toUTCString(),
341
438
  'last-modified': earlier.toUTCString()
@@ -356,7 +453,7 @@ describe('Components', () => {
356
453
  // whenLoaded only fires when ensures that up-to-date version is loaded
357
454
  await expect(component.whenLoaded).resolves.toBeTruthy()
358
455
  expect(spy).toHaveBeenCalledTimes(3)
359
- expect(component.innerHTML).toContain('TODAY')
456
+ expect(component.innerHTML).toContain('FRESH')
360
457
  })
361
458
 
362
459
  it('should re-fetch component when attributes change', async () => {
@@ -418,7 +515,7 @@ describe('Components', () => {
418
515
  })
419
516
  } else {
420
517
  await new Promise((resolve) => setTimeout(resolve, 200))
421
- return new Response('TODAY {}', {
518
+ return new Response('FRESH {}', {
422
519
  headers: {
423
520
  date: now.toUTCString(),
424
521
  'last-modified': earlier.toUTCString()
@@ -445,7 +542,7 @@ describe('Components', () => {
445
542
  } else {
446
543
  // Latest
447
544
  await new Promise((resolve) => setTimeout(resolve, 100))
448
- return new Response('TODAY', {
545
+ return new Response('FRESH', {
449
546
  headers: {
450
547
  date: now.toUTCString(),
451
548
  'last-modified': earlier.toUTCString()
@@ -459,20 +556,20 @@ describe('Components', () => {
459
556
  const component = document.body.firstElementChild as FEAASComponent
460
557
  expect(document.head.querySelector('style')).toBeNull()
461
558
  await new Promise((resolve) => setTimeout(resolve, 50))
462
- const stylesheet = document.head.querySelector('style') as HTMLStyleElement
463
- expect(stylesheet.innerHTML).toEqual('')
559
+ expect(document.head.querySelector('style')).toBeNull()
464
560
  expect(component.innerHTML).toEqual('')
465
561
  // Component will rendered with stale cached stylesheet
466
562
  await expect(component.whenRendered).resolves.toEqual(component)
563
+ const stylesheet = document.head.querySelector('style') as HTMLStyleElement
467
564
  expect(stylesheet.innerHTML).toEqual('YESTERDAY {}')
468
565
  expect(component.innerHTML).toEqual('YESTERDAY')
469
566
  // Component will update itself with stale cached stylesheet
470
- await expect(component.whenLoaded).resolves.toEqual('TODAY')
567
+ await expect(component.whenLoaded).resolves.toEqual('FRESH')
471
568
  expect(stylesheet.innerHTML).toEqual('YESTERDAY {}')
472
- expect(component.innerHTML).toEqual('TODAY')
569
+ expect(component.innerHTML).toEqual('FRESH')
473
570
  await new Promise((resolve) => setTimeout(resolve, 200))
474
571
  // Stylesheet will update later
475
- expect(stylesheet.innerHTML).toEqual('TODAY {}')
572
+ expect(stylesheet.innerHTML).toEqual('FRESH {}')
476
573
  })
477
574
 
478
575
  it('render without data', () => {
@@ -29,7 +29,7 @@ describe('Stylesheet', () => {
29
29
  it('should load stylesheet (cold cache)', async () => {
30
30
  const spy = vi.spyOn(global, 'fetch')
31
31
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
32
- return new Response('TODAY {}', {
32
+ return new Response('FRESH {}', {
33
33
  headers: {
34
34
  date: now.toUTCString(),
35
35
  'last-modified': earlier.toUTCString()
@@ -44,13 +44,13 @@ describe('Stylesheet', () => {
44
44
  await expect(stylesheet.whenRendered).resolves.toEqual(stylesheet)
45
45
  expect(spy).toHaveBeenCalledTimes(1)
46
46
 
47
- expect(document.head.querySelector('style').textContent).toContain('TODAY')
47
+ expect(document.head.querySelector('style').textContent).toContain('FRESH')
48
48
  })
49
49
  it('should load stylesheet with minimum parameters(cold cache)', async () => {
50
50
  const spy = vi.spyOn(global, 'fetch')
51
51
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
52
52
  if (url.match(DEFAULT_CDN_URL + '/styles/L/published.css'))
53
- return new Response('TODAY {}', {
53
+ return new Response('FRESH {}', {
54
54
  headers: {
55
55
  date: now.toUTCString(),
56
56
  'last-modified': earlier.toUTCString()
@@ -65,14 +65,14 @@ describe('Stylesheet', () => {
65
65
  await expect(stylesheet.whenRendered).resolves.toEqual(stylesheet)
66
66
  expect(spy).toHaveBeenCalledTimes(1)
67
67
 
68
- expect(document.head.querySelector('style').textContent).toContain('TODAY')
68
+ expect(document.head.querySelector('style').textContent).toContain('FRESH')
69
69
  })
70
70
 
71
71
  it('should invalidate stylesheet cache', async () => {
72
72
  const spy = vi.spyOn(global, 'fetch')
73
73
  spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
74
74
  if (url.match('CDN/styles/L/published.css') && options?.method != 'HEAD') {
75
- return new Response('TODAY {}', {
75
+ return new Response('FRESH {}', {
76
76
  headers: {
77
77
  date: earlier.toUTCString(),
78
78
  'last-modified': earlier.toUTCString()
@@ -95,7 +95,7 @@ describe('Stylesheet', () => {
95
95
  await expect(stylesheet.whenRendered).resolves.toEqual(stylesheet)
96
96
  expect(spy).toHaveBeenCalledTimes(2)
97
97
 
98
- expect(document.head.querySelector('style').textContent).toContain('TODAY')
98
+ expect(document.head.querySelector('style').textContent).toContain('FRESH')
99
99
  })
100
100
  it('should update stylesheet while using stale cache', async () => {
101
101
  const spy = vi.spyOn(global, 'fetch')
@@ -117,7 +117,7 @@ describe('Stylesheet', () => {
117
117
  }
118
118
  })
119
119
  } else {
120
- return new Response('TODAY {}', {
120
+ return new Response('FRESH {}', {
121
121
  headers: {
122
122
  date: now.toUTCString(),
123
123
  'last-modified': earlier.toUTCString()
@@ -137,7 +137,7 @@ describe('Stylesheet', () => {
137
137
  // whenLoaded only fires when ensures that up-to-date version is loaded
138
138
  await expect(stylesheet.whenLoaded).resolves.toBeTruthy()
139
139
  expect(spy).toHaveBeenCalledTimes(3)
140
- expect(document.head.querySelector('style').textContent).toContain('TODAY {}')
140
+ expect(document.head.querySelector('style').textContent).toContain('FRESH {}')
141
141
  })
142
142
 
143
143
  it('should load stylesheet statically', async () => {
@@ -160,7 +160,7 @@ describe('Stylesheet', () => {
160
160
  }
161
161
  })
162
162
  } else {
163
- return new Response('TODAY {}', {
163
+ return new Response('FRESH {}', {
164
164
  headers: {
165
165
  date: now.toUTCString(),
166
166
  'last-modified': earlier.toUTCString()
@@ -175,7 +175,9 @@ describe('Stylesheet', () => {
175
175
  library: 'L',
176
176
  revision: 'published'
177
177
  }).then((r) => r.textContent)
178
- ).resolves.toEqual('TODAY {}')
179
- await expect(loadStylesheet(`http://CDN/L/published.css`).then((r) => r.textContent)).resolves.toEqual('TODAY {}')
178
+ ).resolves.toEqual('FRESH {}')
179
+ await expect(loadStylesheet(`http://CDN/L/published.css`).then((r) => r.textContent)).resolves.toEqual('FRESH {}')
180
180
  })
181
+
182
+ it('should ')
181
183
  })