@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.
- package/README.md +7 -7
- package/build/components/FEAASComponent.js +72 -10
- package/build/components/FEAASComponent.js.map +1 -1
- package/build/components/FEAASElement.js +19 -5
- package/build/components/FEAASElement.js.map +1 -1
- package/build/dom/rendering.js.map +1 -1
- package/build/dom/thumbnail.js +29 -24
- package/build/dom/thumbnail.js.map +1 -1
- package/build/headless.js +5 -0
- package/build/headless.js.map +1 -1
- package/build/ui/FEAAS.js +5 -0
- package/build/ui/FEAAS.js.map +1 -0
- package/build/ui/FEAASContext.js +9 -6
- package/build/ui/FEAASContext.js.map +1 -1
- package/build/ui/FEAASExternal.js +68 -4
- package/build/ui/FEAASExternal.js.map +1 -1
- package/build/ui/FEAASPicker.js +15 -5
- package/build/ui/FEAASPicker.js.map +1 -1
- package/build/ui/index.js +3 -4
- package/build/ui/index.js.map +1 -1
- package/build/ui/react.js +23 -8
- package/build/ui/react.js.map +1 -1
- package/build/utils/datapath.js +15 -0
- package/build/utils/datapath.js.map +1 -1
- package/build/utils/fetch.js +40 -0
- package/build/utils/fetch.js.map +1 -0
- package/build/utils/settings.js +60 -0
- package/build/utils/settings.js.map +1 -0
- package/dist/browser/headless.cjs +8 -6
- package/dist/browser/headless.cjs.map +4 -4
- package/dist/browser/headless.esm.js +8 -6
- package/dist/browser/headless.esm.js.map +1 -1
- package/dist/browser/index.cjs +8 -6
- package/dist/browser/index.cjs.map +4 -4
- package/dist/browser/index.esm.js +8 -6
- package/dist/browser/index.esm.js.map +1 -1
- package/dist/browser/react.cjs +8 -6
- package/dist/browser/react.cjs.map +4 -4
- package/dist/browser/react.esm.js +8 -6
- package/dist/browser/react.esm.js.map +1 -1
- package/dist/node/headless.cjs +6 -4
- package/dist/node/headless.cjs.map +4 -4
- package/dist/node/headless.esm.js +6 -4
- package/dist/node/headless.esm.js.map +4 -4
- package/dist/node/index.cjs +7 -5
- package/dist/node/index.cjs.map +4 -4
- package/dist/node/index.esm.js +7 -5
- package/dist/node/index.esm.js.map +4 -4
- package/dist/node/react.cjs +6 -4
- package/dist/node/react.cjs.map +4 -4
- package/dist/node/react.esm.js +6 -4
- package/dist/node/react.esm.js.map +4 -4
- package/headless.cjs +6 -4
- package/headless.cjs.map +4 -4
- package/headless.js +6 -4
- package/headless.js.map +4 -4
- package/package.json +2 -2
- package/react.cjs +8 -6
- package/react.cjs.map +4 -4
- package/react.js +8 -6
- package/react.js.map +4 -4
- package/scripts/build.js +1 -1
- package/src/components/FEAASComponent.tsx +101 -10
- package/src/components/FEAASElement.tsx +30 -5
- package/src/dom/rendering.ts +1 -0
- package/src/dom/thumbnail.ts +29 -24
- package/src/headless.ts +5 -0
- package/src/ui/FEAAS.tsx +4 -0
- package/src/ui/FEAASContext.tsx +10 -1
- package/src/ui/FEAASExternal.tsx +75 -16
- package/src/ui/FEAASPicker.tsx +19 -7
- package/src/ui/index.ts +4 -4
- package/src/ui/react.tsx +48 -14
- package/src/utils/datapath.ts +17 -0
- package/src/utils/fetch.ts +60 -0
- package/src/utils/settings.ts +69 -0
- package/test/FEAASComponent.react.test.tsx +1 -1
- package/test/FEAASComponent.test.tsx +1 -0
- package/test/FEAASComponentData.test.tsx +215 -0
- package/test/FEAASContext.test.tsx +3 -3
- package/test/FEAASExternal.test.tsx +1 -8
- package/test/ServerComponent.test.tsx +33 -0
- package/test/fetch-data.ts +55 -0
- package/test/settings.test.ts +78 -0
- package/types/components/FEAASComponent.d.ts +33 -1
- package/types/components/FEAASComponent.d.ts.map +1 -1
- package/types/components/FEAASElement.d.ts +10 -1
- package/types/components/FEAASElement.d.ts.map +1 -1
- package/types/dom/rendering.d.ts.map +1 -1
- package/types/dom/thumbnail.d.ts +16 -16
- package/types/dom/thumbnail.d.ts.map +1 -1
- package/types/headless.d.ts +5 -0
- package/types/headless.d.ts.map +1 -1
- package/types/tsbuildinfo +1 -1
- package/types/ui/FEAAS.d.ts +5 -0
- package/types/ui/FEAAS.d.ts.map +1 -0
- package/types/ui/FEAASContext.d.ts +8 -0
- package/types/ui/FEAASContext.d.ts.map +1 -1
- package/types/ui/FEAASExternal.d.ts +56 -3
- package/types/ui/FEAASExternal.d.ts.map +1 -1
- package/types/ui/FEAASPicker.d.ts +10 -6
- package/types/ui/FEAASPicker.d.ts.map +1 -1
- package/types/ui/index.d.ts +1 -4
- package/types/ui/index.d.ts.map +1 -1
- package/types/ui/react.d.ts +7 -2
- package/types/ui/react.d.ts.map +1 -1
- package/types/ui/tsbuildinfo +1 -1
- package/types/utils/datapath.d.ts +2 -0
- package/types/utils/datapath.d.ts.map +1 -1
- package/types/utils/fetch.d.ts +9 -0
- package/types/utils/fetch.d.ts.map +1 -0
- package/types/utils/settings.d.ts +13 -0
- package/types/utils/settings.d.ts.map +1 -0
package/src/ui/FEAASPicker.tsx
CHANGED
|
@@ -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 '
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 '
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
package/src/utils/datapath.ts
CHANGED
|
@@ -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="{"first-name":"Chuck","telephone":"45657890875643"}" 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="{"data":"1"}">{"data": "1"}</feaas-component>`
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
})
|