@sitecore-feaas/clientside 0.2.1 → 0.3.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.
Files changed (113) hide show
  1. package/README.md +7 -7
  2. package/build/components/FEAASComponent.js +72 -10
  3. package/build/components/FEAASComponent.js.map +1 -1
  4. package/build/components/FEAASElement.js +19 -5
  5. package/build/components/FEAASElement.js.map +1 -1
  6. package/build/dom/rendering.js.map +1 -1
  7. package/build/dom/thumbnail.js +29 -24
  8. package/build/dom/thumbnail.js.map +1 -1
  9. package/build/headless.js +5 -0
  10. package/build/headless.js.map +1 -1
  11. package/build/ui/FEAAS.js +5 -0
  12. package/build/ui/FEAAS.js.map +1 -0
  13. package/build/ui/FEAASContext.js +9 -6
  14. package/build/ui/FEAASContext.js.map +1 -1
  15. package/build/ui/FEAASExternal.js +68 -4
  16. package/build/ui/FEAASExternal.js.map +1 -1
  17. package/build/ui/FEAASPicker.js +15 -5
  18. package/build/ui/FEAASPicker.js.map +1 -1
  19. package/build/ui/index.js +3 -4
  20. package/build/ui/index.js.map +1 -1
  21. package/build/ui/react.js +23 -8
  22. package/build/ui/react.js.map +1 -1
  23. package/build/utils/datapath.js +15 -0
  24. package/build/utils/datapath.js.map +1 -1
  25. package/build/utils/fetch.js +40 -0
  26. package/build/utils/fetch.js.map +1 -0
  27. package/build/utils/settings.js +60 -0
  28. package/build/utils/settings.js.map +1 -0
  29. package/dist/browser/headless.cjs +8 -6
  30. package/dist/browser/headless.cjs.map +4 -4
  31. package/dist/browser/headless.esm.js +8 -6
  32. package/dist/browser/headless.esm.js.map +1 -1
  33. package/dist/browser/index.cjs +8 -6
  34. package/dist/browser/index.cjs.map +4 -4
  35. package/dist/browser/index.esm.js +8 -6
  36. package/dist/browser/index.esm.js.map +1 -1
  37. package/dist/browser/react.cjs +8 -6
  38. package/dist/browser/react.cjs.map +4 -4
  39. package/dist/browser/react.esm.js +8 -6
  40. package/dist/browser/react.esm.js.map +1 -1
  41. package/dist/node/headless.cjs +6 -4
  42. package/dist/node/headless.cjs.map +4 -4
  43. package/dist/node/headless.esm.js +6 -4
  44. package/dist/node/headless.esm.js.map +4 -4
  45. package/dist/node/index.cjs +7 -5
  46. package/dist/node/index.cjs.map +4 -4
  47. package/dist/node/index.esm.js +7 -5
  48. package/dist/node/index.esm.js.map +4 -4
  49. package/dist/node/react.cjs +6 -4
  50. package/dist/node/react.cjs.map +4 -4
  51. package/dist/node/react.esm.js +6 -4
  52. package/dist/node/react.esm.js.map +4 -4
  53. package/headless.cjs +6 -4
  54. package/headless.cjs.map +4 -4
  55. package/headless.js +6 -4
  56. package/headless.js.map +4 -4
  57. package/package.json +2 -2
  58. package/react.cjs +8 -6
  59. package/react.cjs.map +4 -4
  60. package/react.js +8 -6
  61. package/react.js.map +4 -4
  62. package/scripts/build.js +1 -1
  63. package/src/components/FEAASComponent.tsx +101 -10
  64. package/src/components/FEAASElement.tsx +30 -5
  65. package/src/dom/rendering.ts +1 -0
  66. package/src/dom/thumbnail.ts +29 -24
  67. package/src/headless.ts +5 -0
  68. package/src/ui/FEAAS.tsx +4 -0
  69. package/src/ui/FEAASContext.tsx +10 -1
  70. package/src/ui/FEAASExternal.tsx +75 -16
  71. package/src/ui/FEAASPicker.tsx +19 -7
  72. package/src/ui/index.ts +4 -4
  73. package/src/ui/react.tsx +48 -14
  74. package/src/utils/datapath.ts +17 -0
  75. package/src/utils/fetch.ts +60 -0
  76. package/src/utils/settings.ts +69 -0
  77. package/test/FEAASComponent.react.test.tsx +1 -1
  78. package/test/FEAASComponent.test.tsx +1 -0
  79. package/test/FEAASComponentData.test.tsx +215 -0
  80. package/test/FEAASContext.test.tsx +3 -3
  81. package/test/FEAASExternal.test.tsx +1 -8
  82. package/test/ServerComponent.test.tsx +33 -0
  83. package/test/fetch-data.ts +55 -0
  84. package/test/settings.test.ts +78 -0
  85. package/types/components/FEAASComponent.d.ts +33 -1
  86. package/types/components/FEAASComponent.d.ts.map +1 -1
  87. package/types/components/FEAASElement.d.ts +10 -1
  88. package/types/components/FEAASElement.d.ts.map +1 -1
  89. package/types/dom/rendering.d.ts.map +1 -1
  90. package/types/dom/thumbnail.d.ts +16 -16
  91. package/types/dom/thumbnail.d.ts.map +1 -1
  92. package/types/headless.d.ts +5 -0
  93. package/types/headless.d.ts.map +1 -1
  94. package/types/tsbuildinfo +1 -1
  95. package/types/ui/FEAAS.d.ts +5 -0
  96. package/types/ui/FEAAS.d.ts.map +1 -0
  97. package/types/ui/FEAASContext.d.ts +8 -0
  98. package/types/ui/FEAASContext.d.ts.map +1 -1
  99. package/types/ui/FEAASExternal.d.ts +56 -3
  100. package/types/ui/FEAASExternal.d.ts.map +1 -1
  101. package/types/ui/FEAASPicker.d.ts +10 -6
  102. package/types/ui/FEAASPicker.d.ts.map +1 -1
  103. package/types/ui/index.d.ts +1 -4
  104. package/types/ui/index.d.ts.map +1 -1
  105. package/types/ui/react.d.ts +7 -2
  106. package/types/ui/react.d.ts.map +1 -1
  107. package/types/ui/tsbuildinfo +1 -1
  108. package/types/utils/datapath.d.ts +2 -0
  109. package/types/utils/datapath.d.ts.map +1 -1
  110. package/types/utils/fetch.d.ts +9 -0
  111. package/types/utils/fetch.d.ts.map +1 -0
  112. package/types/utils/settings.d.ts +13 -0
  113. package/types/utils/settings.d.ts.map +1 -0
@@ -5,7 +5,7 @@ export function FEAASPickerProps(element: FEAASElement<any, any>) {
5
5
  return {
6
6
  part: element?.getAttribute('part') || 'picker',
7
7
  slot: element?.getAttribute('part') || 'picker',
8
- mode: element?.getAttribute('mode') as 'library' | 'element' | 'themes',
8
+ mode: element?.getAttribute('mode') as FEAASPickerImports.PickerProps['mode'],
9
9
  placeholder: element?.getAttribute('placeholder') || 'No theme',
10
10
  frontend: element?.getContextAttribute('frontend') ?? 'https://components.sitecorecloud.io',
11
11
  src: element?.getAttribute('src') ?? '/picker.js',
@@ -13,7 +13,8 @@ export function FEAASPickerProps(element: FEAASElement<any, any>) {
13
13
  variant: element?.getAttribute('variant') as 'internal' | 'pages',
14
14
  context: undefined as FEAASPickerImports.PickerProps['context'],
15
15
  onClose: undefined as (...args: any[]) => void,
16
- onContextAttributeSet: undefined as (context: any, attribute: string, value: string) => void
16
+ setAttribute: undefined as FEAASPickerImports.AttributeSetter,
17
+ getAttribute: undefined as FEAASPickerImports.AttributeGetter
17
18
  }
18
19
  }
19
20
  export type FEAASPickerProps = ReturnType<typeof FEAASPickerProps>
@@ -32,26 +33,37 @@ export class FEAASPicker extends FEAASElement<FEAASPickerProps, typeof FEAASPick
32
33
  load() {
33
34
  return this.import(this.formatURL(this.props.src, this.props.frontend))
34
35
  }
35
-
36
+ connectedCallback() {
37
+ this.setAttribute('feaas-role', 'ui')
38
+ super.connectedCallback()
39
+ }
36
40
  render(
37
41
  { placeholder, context, mode, children, variant }: React.PropsWithChildren<FEAASPickerProps>,
38
42
  { Picker, React, useLibrary }: typeof FEAASPickerImports
39
43
  ) {
44
+ const [renders, forceRender] = React.useReducer((c) => c + 1, 0)
40
45
  const collections = useLibrary('collections')
46
+ const components = useLibrary('components')
41
47
  const rules = useLibrary('stylesheet.styles')
48
+ const component = components.find((c) => c.id == context?.getAttribute('component'))
42
49
  return (
43
50
  <Picker
44
51
  rules={rules}
45
52
  collections={collections}
46
53
  themeContext={context}
47
54
  placeholder={placeholder}
48
- mode={mode || undefined}
49
- onModeChange={(mode) => {
50
- if (!mode) this.props.onClose(mode)
55
+ component={component}
56
+ versionId={context.getAttribute('version')}
57
+ mode={mode || 'component'}
58
+ setAttribute={(context, attribute, value) => {
59
+ this.props.setAttribute(context, attribute, value)
60
+ forceRender()
51
61
  }}
52
- onContextAttributeSet={this.props.onContextAttributeSet}
53
62
  context={context}
54
63
  variant={variant}
64
+ onModeChange={(mode) => {
65
+ this.set({ mode })
66
+ }}
55
67
  >
56
68
  {children}
57
69
  </Picker>
package/src/ui/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from '../headless.js'
2
- export * from './FEAASEditor.js'
3
- export * from './FEAASPicker.js'
4
- export * from './FEAASContext.js'
1
+ export * from './FEAAS.js'
2
+ import * as FEAAS from './FEAAS.js'
3
+
4
+ FEAAS.Context.prototype.clientside = FEAAS
package/src/ui/react.tsx CHANGED
@@ -1,27 +1,61 @@
1
- export * from './index.js'
2
- import { FEAASComponentProps, parseComponentSource } from './index.js'
1
+ import {
2
+ FEAASComponentProps,
3
+ fetchComponent,
4
+ getComponentURL,
5
+ getStylesheetURL,
6
+ parseComponentSource
7
+ } from './index.js'
3
8
  import React from 'react'
4
9
  import { renderHTMLContent } from '../dom/html.js'
10
+ import { fetchData } from '../utils/fetch.js'
11
+
5
12
  export { renderHTMLContent }
6
13
  export { External } from './FEAASExternal.js'
14
+ export * from './index.js'
15
+ export * from '../headless.js'
16
+
17
+ export const ServerComponent = async (props: FEAASComponentProps<any>) => {
18
+ const fetchedData = await fetchData(props?.data || {})
19
+
20
+ const p = { ...props }
21
+
22
+ if (('src' in p || 'component' in p) && p?.template == null) {
23
+ const response = await fetchComponent(p)
24
+
25
+ p.template = response.template
26
+ // p.lastModified = response.lastModified
27
+ }
28
+
29
+ return FEAASEmbed({ ...p, data: fetchedData })
30
+ }
31
+
32
+ export function FEAASEmbed({ data, ...props }: FEAASComponentProps<any> & { src?: string; preload?: boolean }) {
33
+ let html = ''
34
+ let preload = null
7
35
 
8
- export function FEAASEmbed({ data, ...props }: FEAASComponentProps<any>) {
9
36
  if ('template' in props && props.template) {
10
- var html = renderHTMLContent(props.template, data || {})
37
+ html = renderHTMLContent(props.template, data || {})
11
38
  }
39
+
12
40
  if ('src' in props) {
13
- props = {
14
- ...parseComponentSource(props.src),
15
- ...props
16
- }
41
+ props = { ...parseComponentSource(props.src), ...props }
17
42
  }
43
+
18
44
  return (
19
- <feaas-component
20
- {...props}
21
- template={null}
22
- data={typeof data == 'string' ? data : data ? JSON.stringify(data) : null}
23
- dangerouslySetInnerHTML={{ __html: html || '' }}
24
- />
45
+ <>
46
+ {'preload' in props && preload === true && 'src' in props && (
47
+ <>
48
+ <link rel='preload' as='fetch' href={getComponentURL(props.src)} crossOrigin='anonymous' />
49
+ <link rel='preload' as='style' href={getStylesheetURL(props.src)} crossOrigin='anonymous' />
50
+ </>
51
+ )}
52
+ <feaas-component
53
+ {...props}
54
+ template={null}
55
+ data={typeof data == 'string' ? data : data ? JSON.stringify(data) : null}
56
+ dangerouslySetInnerHTML={{ __html: html || '' }}
57
+ />
58
+ </>
25
59
  )
26
60
  }
27
61
 
@@ -29,3 +29,20 @@ export function cleanCollectionBit(path: string) {
29
29
  export function normalizeCollectionScope(path: string) {
30
30
  return cleanCollectionBit(path) + '.*'
31
31
  }
32
+
33
+ export function getHumanizedLabel(path: string, maxBits = 2) {
34
+ var bits = cleanCollectionBit(path)
35
+ .split(/]\.|\.\*\./g)
36
+ .pop()
37
+ .split(/\./g)
38
+ .slice(-3)
39
+ bits = bits.filter((b) => isNaN(parseInt(b)))
40
+ return bits
41
+ .map((bit) => bit.charAt(0).toUpperCase() + bit.slice(1))
42
+ .slice(-maxBits)
43
+ .join(' ')
44
+ }
45
+
46
+ export function getLastPathBit(path: string) {
47
+ return cleanCollectionBit(path).split('.').pop()
48
+ }
@@ -0,0 +1,60 @@
1
+ type Data = object | any[]
2
+
3
+ type DataSettings = { jsonpath: string; url: string }
4
+
5
+ export const isDataSettings = (data: any): data is DataSettings => {
6
+ if (typeof data.url != 'string') return false
7
+
8
+ for (var property in data) {
9
+ if (
10
+ property == 'body' ||
11
+ property == 'params' ||
12
+ property == 'headers' ||
13
+ property == 'url' ||
14
+ property === 'method' ||
15
+ (property == 'jsonpath' && typeof data[property] == 'string')
16
+ ) {
17
+ continue
18
+ }
19
+
20
+ return false
21
+ }
22
+
23
+ return true
24
+ }
25
+
26
+ const performFetch = async (
27
+ settings: Parameters<typeof fetch>[1] & {
28
+ url: Parameters<typeof fetch>[0]
29
+ }
30
+ ): Promise<Data> => {
31
+ try {
32
+ const response = await fetch(settings.url, settings)
33
+
34
+ if (!response) return {}
35
+
36
+ return await response.json()
37
+ } catch (e) {
38
+ return {}
39
+ }
40
+ }
41
+
42
+ export async function fetchData(data: DataSettings | Record<string, DataSettings | Data> | Data): Promise<Data> {
43
+ if (!data) return data
44
+
45
+ if (isDataSettings(data)) return performFetch(data)
46
+
47
+ const keys = Object.keys(data)
48
+
49
+ const allRequests = await Promise.all(
50
+ keys.map((key: keyof typeof data) => (isDataSettings(data[key]) ? performFetch(data[key]) : null)).filter(Boolean)
51
+ )
52
+
53
+ return keys.reduce(
54
+ (obj, key: keyof typeof data) => ({
55
+ ...obj,
56
+ [key]: isDataSettings(data[key]) ? allRequests.shift() : data[key]
57
+ }),
58
+ {}
59
+ )
60
+ }
@@ -0,0 +1,69 @@
1
+ export function clean(settings: any) {
2
+ let cleanSettings = {}
3
+
4
+ const hasNoBody =
5
+ settings?.method === 'GET' || settings?.method === 'HEAD' || Object.keys(settings?.body || {}).length === 0
6
+
7
+ const hasNoHeaders =
8
+ settings.headers &&
9
+ (Object.keys(settings.headers).length === 0 ||
10
+ (Object.keys(settings.headers).length === 1 &&
11
+ (settings.headers['Content-type'] || settings.headers['Content-Type']) &&
12
+ (settings.headers['Content-type'] === 'application/json' ||
13
+ settings.headers['Content-Type'] === 'application/json')))
14
+
15
+ const hasNoParams = settings.params || Object.keys(settings.params || {}).length === 0
16
+
17
+ if (settings.method && settings.method !== 'GET') cleanSettings = { ...cleanSettings, method: settings.method }
18
+ if (settings.body && !hasNoBody) cleanSettings = { ...cleanSettings, body: settings.body }
19
+ cleanSettings = {
20
+ ...cleanSettings,
21
+ headers: hasNoHeaders ? { 'Content-type': 'application/json' } : settings.headers
22
+ }
23
+ if (settings.params && !hasNoParams) cleanSettings = { ...cleanSettings, params: settings.params }
24
+
25
+ return { ...cleanSettings, url: settings.url || '' }
26
+ }
27
+
28
+ export function toFetchArguments(settings?: any): Parameters<typeof fetch> {
29
+ const normalized = normalize(settings)
30
+
31
+ let body = null
32
+
33
+ const contentType = normalized.headers['Content-Type'] || normalized.headers['Content-type']
34
+
35
+ const isJson = contentType === 'application/json'
36
+ const isForm = contentType === 'multipart/form-data'
37
+ const isUrlEncoded = contentType === 'application/x-www-form-urlencoded'
38
+
39
+ if (isJson) body = JSON.stringify(normalized.body)
40
+ if (isForm)
41
+ body = Object.keys(normalized.body).reduce((form: any, key) => {
42
+ form.append(key, normalized.body[key])
43
+ return form
44
+ }, new FormData())
45
+ if (isUrlEncoded) body = new URLSearchParams(normalized.body)
46
+
47
+ const params = Object.keys(normalized.params).length ? `?${new URLSearchParams(normalized.params)}` : ''
48
+
49
+ let url = `${normalized.url}${params}`
50
+
51
+ let options: any = {}
52
+
53
+ if (body) options.body = body
54
+ if (Object.keys(normalized.headers).length) options.headers = normalized.headers
55
+ if (normalized.method !== 'GET') options.method = normalized.method
56
+
57
+ return [url, options]
58
+ }
59
+
60
+ export function normalize(settings: any) {
61
+ return {
62
+ params: settings?.params || {},
63
+ headers: settings?.headers || {},
64
+ jsonpath: settings?.jsonpath || '$',
65
+ method: settings?.method || 'GET',
66
+ body: settings?.body || {},
67
+ url: settings?.url || ''
68
+ }
69
+ }
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
3
3
  import { describe, expect, it } from 'vitest'
4
4
  import { Component } from '../src/ui/react.js'
5
5
 
6
- async function test(react: any, html: string) {
6
+ export async function test(react: any, html: string) {
7
7
  const div = document.createElement('div')
8
8
  document.body.appendChild(div)
9
9
  const root = ReactDOM.render(react, div)
@@ -6,6 +6,7 @@ import { clearFetchCache } from '../src/fetch.js'
6
6
  const earliest = new Date(2000, 1, 1, 0, 0, 0)
7
7
  const earlier = new Date(2000, 1, 2, 0, 0, 0)
8
8
  const now = new Date(2000, 1, 2, 12, 0, 0)
9
+
9
10
  describe('Components', () => {
10
11
  beforeAll(() => {
11
12
  // vi.useFakeTimers()
@@ -0,0 +1,215 @@
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { FEAASComponent } from '../src/components/FEAASComponent.js'
3
+ import { clearFetchCache } from '../src/fetch.js'
4
+
5
+ const earliest = new Date(2000, 1, 1, 0, 0, 0)
6
+ const earlier = new Date(2000, 1, 2, 0, 0, 0)
7
+ const now = new Date(2000, 1, 2, 12, 0, 0)
8
+
9
+ describe('FEAAS component data', () => {
10
+ beforeAll(() => {
11
+ vi.setSystemTime(now)
12
+ FEAASComponent.register('feaas-component')
13
+ })
14
+
15
+ beforeEach(() => {
16
+ document.write()
17
+ clearFetchCache()
18
+ })
19
+
20
+ const checkRaceCondition = async (promises: { promise: Promise<any>; name: string }[]) => {
21
+ const resolved: { promise: Promise<any>; name: string }[] = []
22
+ await Promise.all(
23
+ promises.map(async (p) => {
24
+ await p.promise
25
+ resolved.push(p)
26
+ })
27
+ )
28
+ expect(resolved.map(({ name }) => name)).to.eql(promises.map(({ name }) => name))
29
+ }
30
+
31
+ const staleCacheSpy = (dataDelay = 100, htmlDelay = 100, stylesDelay = 100) => {
32
+ return vi
33
+ .spyOn(global, 'fetch')
34
+ .mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
35
+ if (url.includes('.css')) {
36
+ await new Promise((resolve) => setTimeout(resolve, stylesDelay))
37
+ return new Response('YESTERDAY {}', {
38
+ headers: {
39
+ date: now.toUTCString(),
40
+ 'last-modified': earliest.toUTCString()
41
+ }
42
+ })
43
+ }
44
+
45
+ if (url.includes('CDN')) {
46
+ await new Promise((resolve) => setTimeout(resolve, htmlDelay))
47
+ return new Response('C', {
48
+ headers: {
49
+ date: now.toUTCString(),
50
+ 'last-modified': earlier.toUTCString()
51
+ }
52
+ })
53
+ }
54
+
55
+ if (url === 'http://my-url') {
56
+ await new Promise((resolve) => setTimeout(resolve, dataDelay))
57
+ return new Response(JSON.stringify({ text: 'My new text' }), {})
58
+ }
59
+
60
+ if (url === 'http://my-url2') {
61
+ await new Promise((resolve) => setTimeout(resolve, dataDelay))
62
+ return new Response(JSON.stringify({ text: 'My new text2' }), {})
63
+ }
64
+
65
+ await new Promise((resolve) => setTimeout(resolve, htmlDelay))
66
+ return new Response('C', {
67
+ headers: {
68
+ date: now.toUTCString(),
69
+ 'last-modified': earlier.toUTCString()
70
+ }
71
+ })
72
+ })
73
+ }
74
+
75
+ describe('Should render component using stale cache, no data initially, then data as object', async () => {
76
+ const runWithDelay = async (delays: number[]) => {
77
+ const spy = staleCacheSpy(delays[0], delays[1], delays[2])
78
+ document.body.innerHTML = `<feaas-component data-path="text" library="L" cdn="CDN" component="C" version="V" revision="R"></feaas-component>`
79
+ const component = document.body.firstElementChild as FEAASComponent
80
+ const updateCall = vi.spyOn(component, 'update')
81
+ await expect(component.whenDataReady).resolves.toEqual({})
82
+ await expect(component.whenRendered).resolves.toEqual(component)
83
+ await expect(component.whenLoaded).resolves.toBeTruthy()
84
+ expect(component.readyData).toEqual({})
85
+ expect(component.innerHTML).toEqual('C')
86
+ expect(spy).toHaveBeenCalledTimes(2)
87
+ await checkRaceCondition([
88
+ { promise: component.whenDataReady, name: 'when data ready' },
89
+ { promise: component.whenRendered, name: 'when rendered' },
90
+ { promise: component.whenLoaded, name: 'when loaded' }
91
+ ])
92
+ expect(updateCall).toHaveBeenCalledTimes(1)
93
+ component.setAttribute('data', '{"text": "this is text"}')
94
+ await expect(component.whenDataReady).resolves.toEqual({ text: 'this is text' })
95
+ await expect(component.whenRendered).resolves.toEqual(component)
96
+ expect(updateCall).toHaveBeenCalledTimes(2)
97
+ expect(component.innerHTML).toEqual('this is text')
98
+ expect(component.readyData).toEqual({ text: 'this is text' })
99
+ expect(spy).toHaveBeenCalledTimes(2)
100
+ await expect(component.whenLoaded).resolves.toBeTruthy()
101
+ await checkRaceCondition([
102
+ { promise: component.whenDataReady, name: 'when data ready' },
103
+ { promise: component.whenRendered, name: 'when rendered' },
104
+ { promise: component.whenLoaded, name: 'when loaded' }
105
+ ])
106
+ }
107
+
108
+ it('DATA -> HTML -> STYLESHEET', async () => await runWithDelay([100, 200, 300]))
109
+ it('DATA -> STYLESHEET -> HTML', async () => await runWithDelay([100, 300, 200]))
110
+ it('HTML -> DATA -> STYLESHEET', async () => await runWithDelay([200, 100, 300]))
111
+ it('STYLESHEET -> DATA -> HTML', async () => await runWithDelay([200, 300, 100]))
112
+ it('HTML -> STYLESHEET -> DATA', async () => await runWithDelay([300, 100, 200]))
113
+ it('STYLESHEET -> HTML -> DATA', async () => await runWithDelay([300, 200, 100]))
114
+ })
115
+
116
+ describe('Should render component using stale cache and data as object first, then settings', () => {
117
+ const runWithDelay = async (delays: number[]) => {
118
+ const spy = staleCacheSpy(delays[0], delays[1], delays[2])
119
+ document.body.innerHTML = `<feaas-component data='{"text": "this is text"}' data-path="text" library="L" cdn="CDN" component="C" version="V" revision="R"></feaas-component>`
120
+ const component = document.body.firstElementChild as FEAASComponent
121
+ const updateCall = vi.spyOn(component, 'update')
122
+ await expect(component.whenDataReady).resolves.toEqual({ text: 'this is text' })
123
+ await expect(component.whenRendered).resolves.toEqual(component)
124
+ await expect(component.whenLoaded).resolves.toBeTruthy()
125
+ expect(component.readyData).toEqual({ text: 'this is text' })
126
+ expect(component.innerHTML).toEqual('this is text')
127
+ expect(spy).toHaveBeenCalledTimes(2)
128
+ await checkRaceCondition([
129
+ { promise: component.whenDataReady, name: 'when data ready' },
130
+ { promise: component.whenRendered, name: 'when rendered' },
131
+ { promise: component.whenLoaded, name: 'when loaded' }
132
+ ])
133
+ expect(updateCall).toHaveBeenCalledTimes(1)
134
+ component.setAttribute('data', '{"jsonpath": "$", "url": "http://my-url"}')
135
+ await expect(component.whenDataReady).resolves.toEqual({ text: 'My new text' })
136
+ await expect(component.whenRendered).resolves.toEqual(component)
137
+ expect(updateCall).toHaveBeenCalledTimes(2)
138
+ expect(component.innerHTML).toEqual('My new text')
139
+ expect(component.readyData).toEqual({ text: 'My new text' })
140
+ expect(spy).toHaveBeenCalledTimes(3)
141
+ await expect(component.whenLoaded).resolves.toBeTruthy()
142
+ await checkRaceCondition([
143
+ { promise: component.whenDataReady, name: 'when data ready' },
144
+ { promise: component.whenRendered, name: 'when rendered' },
145
+ { promise: component.whenLoaded, name: 'when loaded' }
146
+ ])
147
+ }
148
+
149
+ it('DATA -> HTML -> STYLESHEET', async () => await runWithDelay([100, 200, 300]))
150
+ it('DATA -> STYLESHEET -> HTML', async () => await runWithDelay([100, 300, 200]))
151
+ it('HTML -> DATA -> STYLESHEET', async () => await runWithDelay([200, 100, 300]))
152
+ it('STYLESHEET -> DATA -> HTML', async () => await runWithDelay([200, 300, 100]))
153
+ it('HTML -> STYLESHEET -> DATA', async () => await runWithDelay([300, 100, 200]))
154
+ it('STYLESHEET -> HTML -> DATA', async () => await runWithDelay([300, 200, 100]))
155
+ })
156
+
157
+ describe('Should render component using stale cache and data as settings', () => {
158
+ const runWithDelay = async (delays: number[]) => {
159
+ const spy = staleCacheSpy(delays[0], delays[1], delays[2])
160
+ document.body.innerHTML = `<feaas-component data='{ "jsonpath": "$", "url": "http://my-url" }' data-path="text" library="L" cdn="CDN" component="C" version="V" revision="R"></feaas-component>`
161
+ const component = document.body.firstElementChild as FEAASComponent
162
+ const updateCall = vi.spyOn(component, 'update')
163
+ await expect(component.whenDataReady).resolves.toEqual({ text: 'My new text' })
164
+ await expect(component.whenRendered).resolves.toEqual(component)
165
+ await expect(component.whenLoaded).resolves.toBeTruthy()
166
+ expect(component.readyData).toEqual({ text: 'My new text' })
167
+ expect(component.innerHTML).toEqual('My new text')
168
+ expect(spy).toHaveBeenCalledTimes(3)
169
+ await checkRaceCondition([
170
+ { promise: component.whenDataReady, name: 'when data ready' },
171
+ { promise: component.whenRendered, name: 'when rendered' },
172
+ { promise: component.whenLoaded, name: 'when loaded' }
173
+ ])
174
+ expect(updateCall).toHaveBeenCalledTimes(1)
175
+ }
176
+
177
+ it('DATA -> HTML -> STYLESHEET', async () => await runWithDelay([100, 200, 300]))
178
+ it('DATA -> STYLESHEET -> HTML', async () => await runWithDelay([100, 300, 200]))
179
+ it('HTML -> DATA -> STYLESHEET', async () => await runWithDelay([200, 100, 300]))
180
+ it('STYLESHEET -> DATA -> HTML', async () => await runWithDelay([200, 300, 100]))
181
+ it('HTML -> STYLESHEET -> DATA', async () => await runWithDelay([300, 100, 200]))
182
+ it('STYLESHEET -> HTML -> DATA', async () => await runWithDelay([300, 200, 100]))
183
+ })
184
+
185
+ describe('Should render component using stale cache and data as mapping', () => {
186
+ const runWithDelay = async (delays: number[]) => {
187
+ const spy = staleCacheSpy(delays[0], delays[1], delays[2])
188
+ document.body.innerHTML = `<feaas-component data='{ "datasource-1": {"jsonpath": "$", "url": "http://my-url"}, "datasource-2": {"jsonpath": "$", "url": "http://my-url2"} }' data-path="datasource-2.text" library="L" cdn="CDN" component="C" version="V" revision="R"></feaas-component>`
189
+ const component = document.body.firstElementChild as FEAASComponent
190
+ const updateCall = vi.spyOn(component, 'update')
191
+ await expect(component.whenDataReady).resolves.toEqual({ 'datasource-1': { text: 'My new text' }, 'datasource-2': { text: 'My new text2' } })
192
+ await expect(component.whenRendered).resolves.toEqual(component)
193
+ await expect(component.whenLoaded).resolves.toBeTruthy()
194
+ expect(component.readyData).toEqual({ 'datasource-1': { text: 'My new text' }, 'datasource-2': { text: 'My new text2' } })
195
+ expect(component.innerHTML).toEqual('My new text2')
196
+ expect(spy).toHaveBeenCalledTimes(4)
197
+ await checkRaceCondition([
198
+ { promise: component.whenDataReady, name: 'when data ready' },
199
+ { promise: component.whenRendered, name: 'when rendered' },
200
+ { promise: component.whenLoaded, name: 'when loaded' }
201
+ ])
202
+ expect(updateCall).toHaveBeenCalledTimes(1)
203
+ }
204
+
205
+ it('DATA -> HTML -> STYLESHEET', async () => await runWithDelay([100, 200, 300]))
206
+ it('DATA -> STYLESHEET -> HTML', async () => await runWithDelay([100, 300, 200]))
207
+ it('HTML -> DATA -> STYLESHEET', async () => await runWithDelay([200, 100, 300]))
208
+ it('STYLESHEET -> DATA -> HTML', async () => await runWithDelay([200, 300, 100]))
209
+ it('HTML -> STYLESHEET -> DATA', async () => await runWithDelay([300, 100, 200]))
210
+ it('STYLESHEET -> HTML -> DATA', async () => await runWithDelay([300, 200, 100]))
211
+ })
212
+
213
+ // TODO
214
+ it.skip('Should render component using non-stale cache and data as settings', async () => {})
215
+ })
@@ -40,7 +40,7 @@ describe('React', function () {
40
40
  expect(spyLoad).toHaveBeenCalledTimes(1)
41
41
 
42
42
  expect(component.outerHTML).toBe(
43
- '<feaas-context src="test.js" library="abc" isolate="false" frontend="https://components.sitecorecloud.io" backend="https://components.sitecorecloud.io/api" cdn="https://feaas.blob.core.windows.net"><div><p>Context abc</p></div></feaas-context>'
43
+ '<feaas-context src="test.js" library="abc" isolate="false" frontend="https://components.sitecorecloud.io" backend="https://components.sitecorecloud.io/api" cdn="https://feaas.blob.core.windows.net" feaas-role="ui"><div><p>Context abc</p></div></feaas-context>'
44
44
  )
45
45
  })
46
46
  it('should initiate loading without isolation', async () => {
@@ -71,7 +71,7 @@ describe('React', function () {
71
71
  expect(spyLoad).toHaveBeenCalledTimes(1)
72
72
 
73
73
  expect(component.outerHTML).toBe(
74
- '<feaas-context src="test.js" library="abc" isolate="false" frontend="https://components.sitecorecloud.io" backend="https://components.sitecorecloud.io/api" cdn="https://feaas.blob.core.windows.net"><div><p>Context abc</p></div></feaas-context>'
74
+ '<feaas-context src="test.js" library="abc" isolate="false" frontend="https://components.sitecorecloud.io" backend="https://components.sitecorecloud.io/api" cdn="https://feaas.blob.core.windows.net" feaas-role="ui"><div><p>Context abc</p></div></feaas-context>'
75
75
  )
76
76
  })
77
77
  it('should initiate loading with isolation', async () => {
@@ -102,7 +102,7 @@ describe('React', function () {
102
102
  expect(spyLoad).toHaveBeenCalledTimes(1)
103
103
 
104
104
  expect(component.outerHTML).toBe(
105
- '<feaas-context src="test.js" library="abc" frontend="https://components.sitecorecloud.io" backend="https://components.sitecorecloud.io/api" cdn="https://feaas.blob.core.windows.net"></feaas-context>'
105
+ '<feaas-context src="test.js" library="abc" frontend="https://components.sitecorecloud.io" backend="https://components.sitecorecloud.io/api" cdn="https://feaas.blob.core.windows.net" feaas-role="ui"></feaas-context>'
106
106
  )
107
107
  expect(component.shadowRoot.innerHTML).toBe('<div><p>Context abc</p></div>')
108
108
  })
@@ -37,6 +37,7 @@ describe('External', () => {
37
37
  )
38
38
  }
39
39
  const typesComponentSchema = {
40
+ name: 'TypesComponent',
40
41
  properties: {
41
42
  stringProp: {
42
43
  type: 'string'
@@ -66,14 +67,6 @@ describe('External', () => {
66
67
  await expect(component.whenRendered).resolves.toEqual(component)
67
68
  expect(component.innerHTML).toEqual('<div>Hello my name is Chuck and my tel is 45657890875643</div>')
68
69
  })
69
- it('can take functional component name as id', async () => {
70
- Clientside.External.registerComponent(Hello)
71
- document.body.innerHTML =
72
- '<feaas-external id="external-component" class="-component" data-attributes-keys="first-name,telephone" first-name="Chuck" telephone="45657890875643" data-attributes="{&quot;first-name&quot;:&quot;Chuck&quot;,&quot;telephone&quot;:&quot;45657890875643&quot;}" data-external-id="Hello" contenteditable="false"></feaas-external>'
73
- const component = document.getElementById('external-component') as FEAASExternal
74
- await expect(component.whenRendered).resolves.toEqual(component)
75
- expect(component.innerHTML).toEqual('<div>Hello my name is Chuck and my tel is 45657890875643</div>')
76
- })
77
70
  it('should return empty if component is not registered', async () => {
78
71
  document.body.innerHTML =
79
72
  '<feaas-external id="external-component" class="-component" data-external-id="NotRegisteredComponent" contenteditable="false"></feaas-external>'
@@ -0,0 +1,33 @@
1
+ import React from 'react'
2
+ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import { ServerComponent } from '../src/ui/react.js'
4
+ import { FEAASComponent } from '../src/components/FEAASComponent.js'
5
+ import {test} from './FEAASComponent.react.test.js'
6
+
7
+ const earliest = new Date(2000, 1, 1, 0, 0, 0)
8
+ const earlier = new Date(2000, 1, 2, 0, 0, 0)
9
+ const now = new Date(2000, 1, 2, 12, 0, 0)
10
+
11
+ describe('React Server', () => {
12
+ beforeAll(() => {
13
+ vi.setSystemTime(now)
14
+ FEAASComponent.register('feaas-component')
15
+ })
16
+
17
+ beforeEach(() => {
18
+ document.write()
19
+ })
20
+
21
+ it('render server component', async () => {
22
+ vi.spyOn(global, 'fetch').mockImplementation(
23
+ async (url: string, options: Parameters<typeof fetch>[1] = {}) => new Response('{"data": "1"}')
24
+ )
25
+
26
+ const Component = await ServerComponent({ src: 'library/test', data: { url: 'http://localhost:4000/api/data' } })
27
+
28
+ await test(
29
+ <>{Component}</>,
30
+ `<feaas-component library="library" component="test" src="library/test" data="{&quot;data&quot;:&quot;1&quot;}">{"data": "1"}</feaas-component>`
31
+ )
32
+ })
33
+ })