@sitecore-feaas/clientside 0.1.27 → 0.1.29

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 (156) hide show
  1. package/build/cdn.js +115 -0
  2. package/build/cdn.js.map +1 -0
  3. package/build/components/FEAASBase.js +212 -79
  4. package/build/components/FEAASBase.js.map +1 -1
  5. package/build/components/FEAASComponent.js +51 -17
  6. package/build/components/FEAASComponent.js.map +1 -1
  7. package/build/components/FEAASLoader.js +6 -0
  8. package/build/components/FEAASLoader.js.map +1 -1
  9. package/build/components/FEAASStylesheet.js +18 -13
  10. package/build/components/FEAASStylesheet.js.map +1 -1
  11. package/build/dom/component.js +34 -0
  12. package/build/dom/component.js.map +1 -0
  13. package/build/dom/data.js +105 -0
  14. package/build/dom/data.js.map +1 -0
  15. package/build/{dom.js → dom/rendering.js} +3 -194
  16. package/build/dom/rendering.js.map +1 -0
  17. package/build/dom/scripts.js +48 -0
  18. package/build/dom/scripts.js.map +1 -0
  19. package/build/dom/stylesheet.js +39 -0
  20. package/build/dom/stylesheet.js.map +1 -0
  21. package/build/fetch.js +72 -22
  22. package/build/fetch.js.map +1 -1
  23. package/build/{basic.js → headless.js} +11 -3
  24. package/build/headless.js.map +1 -0
  25. package/build/ui/FEAASContext.js +10 -3
  26. package/build/ui/FEAASContext.js.map +1 -1
  27. package/build/ui/FEAASEditor.js +5 -0
  28. package/build/ui/FEAASEditor.js.map +1 -1
  29. package/build/ui/FEAASPicker.js +7 -1
  30. package/build/ui/FEAASPicker.js.map +1 -1
  31. package/build/ui/index.js +1 -1
  32. package/build/ui/index.js.map +1 -1
  33. package/build/ui/react.js +6 -0
  34. package/build/ui/react.js.map +1 -0
  35. package/build/utils/promise.js +26 -0
  36. package/build/utils/promise.js.map +1 -0
  37. package/dist/browser/headless.esm.js +69 -0
  38. package/dist/browser/headless.esm.js.map +1 -0
  39. package/dist/browser/headless.js +69 -0
  40. package/dist/browser/headless.js.map +1 -0
  41. package/dist/browser/index.esm.js +3 -5
  42. package/dist/browser/index.esm.js.map +1 -1
  43. package/dist/browser/index.js +5 -5
  44. package/dist/browser/index.js.map +1 -1
  45. package/dist/browser/react.js +67 -0
  46. package/dist/browser/react.js.map +1 -0
  47. package/dist/node/headless.esm.js +67 -0
  48. package/dist/node/headless.esm.js.map +7 -0
  49. package/dist/node/headless.js +67 -0
  50. package/dist/node/headless.js.map +7 -0
  51. package/dist/node/index.esm.js +3 -3
  52. package/dist/node/index.esm.js.map +4 -4
  53. package/dist/node/index.js +3 -3
  54. package/dist/node/index.js.map +4 -4
  55. package/dist/node/react.js +67 -0
  56. package/dist/node/react.js.map +7 -0
  57. package/examples/index.html +0 -5
  58. package/package.json +23 -12
  59. package/scripts/build.js +17 -6
  60. package/scripts/test-setup.ts +1 -0
  61. package/src/cdn.ts +188 -0
  62. package/src/components/FEAASBase.tsx +234 -82
  63. package/src/components/FEAASComponent.tsx +59 -21
  64. package/src/components/FEAASLoader.tsx +4 -0
  65. package/src/components/FEAASStylesheet.tsx +14 -13
  66. package/src/dom/component.ts +35 -0
  67. package/src/dom/data.ts +124 -0
  68. package/src/{dom.ts → dom/rendering.ts} +4 -218
  69. package/src/dom/scripts.ts +47 -0
  70. package/src/dom/stylesheet.ts +48 -0
  71. package/src/fetch.ts +79 -25
  72. package/src/{basic.ts → headless.ts} +11 -2
  73. package/src/ui/FEAASContext.tsx +12 -4
  74. package/src/ui/FEAASEditor.tsx +5 -0
  75. package/src/ui/FEAASPicker.tsx +6 -2
  76. package/src/ui/index.ts +1 -1
  77. package/src/ui/react.tsx +8 -0
  78. package/src/utils/promise.ts +34 -0
  79. package/test/FEAASBase.test.tsx +195 -0
  80. package/test/FEAASComponent.test.tsx +284 -0
  81. package/test/FEAASContext.test.tsx +76 -0
  82. package/test/FEAASPicker.test.tsx +40 -0
  83. package/test/FEAASStylesheet.test.tsx +159 -0
  84. package/test/cdn.ts +39 -0
  85. package/test/{render.ts → dom.test.ts} +12 -6
  86. package/test/extras.ts +1 -1
  87. package/test/fetch.ts +254 -0
  88. package/test/tsconfig.json +4 -1
  89. package/types/cdn.d.ts +57 -0
  90. package/types/cdn.d.ts.map +1 -0
  91. package/types/components/FEAASBase.d.ts +220 -16
  92. package/types/components/FEAASBase.d.ts.map +1 -1
  93. package/types/components/FEAASComponent.d.ts +34 -7
  94. package/types/components/FEAASComponent.d.ts.map +1 -1
  95. package/types/components/FEAASLoader.d.ts +4 -0
  96. package/types/components/FEAASLoader.d.ts.map +1 -1
  97. package/types/components/FEAASStylesheet.d.ts +5 -13
  98. package/types/components/FEAASStylesheet.d.ts.map +1 -1
  99. package/types/dom/component.d.ts +6 -0
  100. package/types/dom/component.d.ts.map +1 -0
  101. package/types/dom/data.d.ts +15 -0
  102. package/types/dom/data.d.ts.map +1 -0
  103. package/types/dom/rendering.d.ts +20 -0
  104. package/types/dom/rendering.d.ts.map +1 -0
  105. package/types/dom/scripts.d.ts +4 -0
  106. package/types/dom/scripts.d.ts.map +1 -0
  107. package/types/dom/stylesheet.d.ts +8 -0
  108. package/types/dom/stylesheet.d.ts.map +1 -0
  109. package/types/fetch.d.ts +25 -11
  110. package/types/fetch.d.ts.map +1 -1
  111. package/types/headless.d.ts +24 -0
  112. package/types/headless.d.ts.map +1 -0
  113. package/types/tsbuildinfo +1 -1
  114. package/types/tsbuildinfo_ui +1 -1
  115. package/types/ui/FEAASContext.d.ts +15 -5
  116. package/types/ui/FEAASContext.d.ts.map +1 -1
  117. package/types/ui/FEAASEditor.d.ts +18 -0
  118. package/types/ui/FEAASEditor.d.ts.map +1 -1
  119. package/types/ui/FEAASPicker.d.ts +15 -3
  120. package/types/ui/FEAASPicker.d.ts.map +1 -1
  121. package/types/ui/index.d.ts +1 -1
  122. package/types/ui/index.d.ts.map +1 -1
  123. package/types/ui/react.d.ts +7 -0
  124. package/types/ui/react.d.ts.map +1 -0
  125. package/types/utils/promise.d.ts +8 -0
  126. package/types/utils/promise.d.ts.map +1 -0
  127. package/build/basic.js.map +0 -1
  128. package/build/cloud.js +0 -71
  129. package/build/cloud.js.map +0 -1
  130. package/build/dom.js.map +0 -1
  131. package/build/node.js +0 -10
  132. package/build/node.js.map +0 -1
  133. package/build/ui/node.js +0 -3
  134. package/build/ui/node.js.map +0 -1
  135. package/dist/browser/basic.esm.js +0 -69
  136. package/dist/browser/basic.esm.js.map +0 -1
  137. package/dist/browser/basic.js +0 -69
  138. package/dist/browser/basic.js.map +0 -1
  139. package/dist/node/basic.esm.js +0 -67
  140. package/dist/node/basic.esm.js.map +0 -7
  141. package/dist/node/basic.js +0 -67
  142. package/dist/node/basic.js.map +0 -7
  143. package/src/cloud.ts +0 -118
  144. package/src/node.ts +0 -10
  145. package/src/ui/node.ts +0 -2
  146. package/test/components.ts +0 -31
  147. package/types/basic.d.ts +0 -16
  148. package/types/basic.d.ts.map +0 -1
  149. package/types/cloud.d.ts +0 -28
  150. package/types/cloud.d.ts.map +0 -1
  151. package/types/dom.d.ts +0 -41
  152. package/types/dom.d.ts.map +0 -1
  153. package/types/node.d.ts +0 -4
  154. package/types/node.d.ts.map +0 -1
  155. package/types/ui/node.d.ts +0 -3
  156. package/types/ui/node.d.ts.map +0 -1
@@ -0,0 +1,48 @@
1
+ import {
2
+ FEAASCDNStylesheetParams,
3
+ fetchAndRevalidateStylesheet,
4
+ getStylesheetURL,
5
+ ValidatableStylesheetCallback
6
+ } from '../cdn.js'
7
+ export function renderStylesheet(props: FEAASCDNStylesheetParams, style?: HTMLStyleElement) {
8
+ renderStylesheetPromise(props, (style ||= document.createElement('style')))
9
+ return style
10
+ }
11
+
12
+ export function renderStylesheetPromise(props: FEAASCDNStylesheetParams, stylesheet?: HTMLStyleElement) {
13
+ stylesheet ||= document.createElement('style')
14
+ return fetchAndRevalidateStylesheet(props, (cssText, phase) => {
15
+ stylesheet.textContent = cssText
16
+ }).then(() => stylesheet)
17
+ }
18
+
19
+ export function loadStylesheet(props: FEAASCDNStylesheetParams, callback?: ValidatableStylesheetCallback) {
20
+ var foundStylesheet = findStylesheet(props) as HTMLStyleElement
21
+ if (!foundStylesheet) {
22
+ var createdStylesheet = createStylesheet(props)
23
+ }
24
+ return fetchAndRevalidateStylesheet(props, (cssText, phase) => {
25
+ if (createdStylesheet) {
26
+ createdStylesheet.textContent = cssText
27
+ }
28
+ callback?.(cssText, phase)
29
+ }).then(() => foundStylesheet || createdStylesheet)
30
+ }
31
+
32
+ export function loadStylesheetAllowStale(props: FEAASCDNStylesheetParams) {
33
+ return new Promise((resolve, reject) => {
34
+ loadStylesheet(props, resolve).catch(reject)
35
+ })
36
+ }
37
+
38
+ export function findStylesheet(props: FEAASCDNStylesheetParams) {
39
+ return document.head.querySelector(`style[data-href="${getStylesheetURL(props)}"]`)
40
+ }
41
+
42
+ export function createStylesheet(props: FEAASCDNStylesheetParams) {
43
+ const url = getStylesheetURL(props)
44
+ const stylesheet = document.createElement('style')
45
+ stylesheet.setAttribute('data-href', url)
46
+ document.head.appendChild(stylesheet)
47
+ return stylesheet
48
+ }
package/src/fetch.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { ControlledPromise } from './utils/promise.js'
2
+
1
3
  /**
2
4
  * Utility to cache-bust static files. It relies on reading Date header to detect cached response, so it requires CORS
3
5
  * settings to be set.
@@ -10,60 +12,115 @@
10
12
  * It works as promise, but also accepts callback to use cached blobs mid-flight and have access to headers. Any network
11
13
  * errors will not be caught. Function throws when unable to fetch valid response.
12
14
  */
15
+ const phases = ['initial', 'check', 'updated'] as const
16
+ export type ValidatableRequestPhase = typeof phases[number]
17
+ export type FetchCache = Record<string, FetchCacheRequest>
13
18
  export interface ValidatableRequest {
14
19
  url: Parameters<typeof fetch>[0]
15
20
  options?: Parameters<typeof fetch>[1]
16
21
  validator?: (resposne: Response) => boolean
17
- callback?: (response: Response, status: 'initial' | 'updated' | 'check') => void
22
+ callback?: (response: Response, status: ValidatableRequestPhase) => any
18
23
  isCached?: boolean
19
24
  response?: Response
25
+ cache?: FetchCache
26
+ }
27
+
28
+ export type FetchCacheRequest = {
29
+ phase: ValidatableRequestPhase
30
+ initial: ControlledPromise<Response>
31
+ check: ControlledPromise<Response>
32
+ updated: ControlledPromise<Response>
33
+ final: ControlledPromise<Response>
34
+ }
35
+
36
+ export var fetchCache: Record<string, FetchCacheRequest> = {}
37
+ export function clearFetchCache() {
38
+ fetchCache = {}
20
39
  }
21
40
 
22
41
  export async function fetchAndRevalidate(
23
42
  url: ValidatableRequest['url'],
24
43
  options?: ValidatableRequest['options'],
25
44
  validator?: ValidatableRequest['validator'],
26
- callback?: ValidatableRequest['callback']
45
+ callback?: ValidatableRequest['callback'],
46
+ cache: FetchCache = fetchCache
27
47
  ) {
28
48
  // make request as usual, see if there's cached version of an asset
29
- return fetchAndValidate({ url, options, validator, callback })
30
- .then(refetchAndValidate)
49
+ return fetchWithCacheAwareness({ url, options, validator, callback, cache })
50
+ .then(fetchToValidateCache)
31
51
  .then((result) => result.response)
32
52
  }
33
53
 
34
- export async function fetchAndValidate(
35
- { url, options, validator, callback }: ValidatableRequest,
36
- phase: 'initial' | 'updated' | 'check' = 'initial'
54
+ export function getFetchCache(url: Parameters<typeof fetch>[0], dontCreate = false) {
55
+ const key = String(url).replace('?head', '')
56
+ var cache = fetchCache[key]
57
+ if (!cache && !dontCreate) {
58
+ cache = fetchCache[key] = {
59
+ phase: null,
60
+ initial: ControlledPromise(),
61
+ check: ControlledPromise(),
62
+ updated: ControlledPromise(),
63
+ final: ControlledPromise()
64
+ }
65
+ }
66
+ return cache
67
+ }
68
+
69
+ export async function fetchWithCacheAwareness(
70
+ { url, options, validator, callback, cache }: ValidatableRequest,
71
+ phase: ValidatableRequestPhase = 'initial'
37
72
  ) {
38
73
  const date = new Date()
39
- const response = await fetch(url, options)
74
+ if (cache != null) {
75
+ var cachedRequest = getFetchCache(url)
76
+ if (phases.indexOf(cachedRequest.phase) >= phases.indexOf(phase)) {
77
+ var promise = cachedRequest[phase]
78
+ } else {
79
+ cachedRequest.phase = phase
80
+ }
81
+ }
82
+ const response = await (promise || fetch(url, options))
83
+ if (!promise && cachedRequest) {
84
+ if (!response.ok) {
85
+ cachedRequest[phase].reject(new Error('Fetch failed'))
86
+ } else {
87
+ cachedRequest[phase].resolve(response)
88
+ }
89
+ }
40
90
  if (!response.ok) throw new Error('Fetch failed')
41
91
  if (validator?.(response) === false) throw new Error('Response is not valid')
42
92
  // see if response was pulled from cache
43
- const isCached = new Date(response.headers.get('date')) < date
44
- callback?.(response, phase)
93
+ const isCached =
94
+ new Date(response.headers.get('date')) < date ||
95
+ (promise && phase == 'initial' && (cachedRequest.phase == 'updated' || cachedRequest.phase == 'check'))
96
+ await callback?.(response, phase)
45
97
  return {
46
98
  url,
47
99
  options,
48
100
  isCached,
49
101
  response,
50
- validator
102
+ validator,
103
+ callback,
104
+ cache
51
105
  }
52
106
  }
53
107
 
54
- export async function refetchAndValidate(request: ValidatableRequest & Required<Pick<ValidatableRequest, 'response'>>) {
55
- const { url, isCached, options, validator, response, callback } = request
108
+ export async function fetchToValidateCache(
109
+ request: ValidatableRequest & Required<Pick<ValidatableRequest, 'response'>>
110
+ ) {
111
+ const { url, isCached, options, validator, response, cache } = request
56
112
  if (isCached) {
57
113
  // check if response was stale by sending quick HEAD request busting cache
58
- const { response: head } = await fetchAndValidate(
114
+ const { response: head } = await fetchWithCacheAwareness(
59
115
  {
60
- url: url + '?' + Math.random(),
116
+ url: url + '?head',
61
117
  options: {
62
118
  ...options,
63
119
  method: 'HEAD',
64
120
  cache: 'no-cache'
65
121
  },
66
- validator
122
+ validator,
123
+ cache
67
124
  },
68
125
  'check'
69
126
  )
@@ -71,23 +128,20 @@ export async function refetchAndValidate(request: ValidatableRequest & Required<
71
128
  // there's a newer version of the file that we may fetch
72
129
  if (head.headers.get('last-modified') != response.headers.get('last-modified')) {
73
130
  // otherwise the new resource has to be re-fetched
74
- return invalidateRequest(request)
131
+ return fetchBypassingCache(request)
75
132
  }
76
133
  }
77
134
  return request
78
135
  }
79
136
 
80
- export async function invalidateRequest(request: ValidatableRequest) {
81
- const { url, options, validator, callback } = request
82
- return fetchAndValidate(
137
+ export async function fetchBypassingCache(request: ValidatableRequest) {
138
+ return fetchWithCacheAwareness(
83
139
  {
84
- url,
140
+ ...request,
85
141
  options: {
86
- ...options,
142
+ ...request.options,
87
143
  cache: 'reload'
88
- },
89
- validator,
90
- callback
144
+ }
91
145
  },
92
146
  'updated'
93
147
  )
@@ -1,8 +1,17 @@
1
+ /**
2
+ * Simplified build of clientside that does not rely on any Frontend UI (like pickers). It refered from Frontend app to
3
+ * avoid circular dependency. Regular clientside build entry point is in ui/index.ts
4
+ */
5
+
1
6
  import FEAASElement from './components/FEAASElement.js'
2
7
 
3
- export * from './dom.js'
8
+ export * from './dom/rendering.js'
9
+ export * from './dom/data.js'
10
+ export * from './dom/scripts.js'
11
+ export * from './dom/stylesheet.js'
12
+ export * from './dom/component.js'
4
13
  export * from './fetch.js'
5
- export * from './cloud.js'
14
+ export * from './cdn.js'
6
15
  export * from './components/FEAASBase.js'
7
16
  export * from './components/FEAASElement.js'
8
17
  export * from './components/FEAASComponent.js'
@@ -1,4 +1,5 @@
1
1
  import type * as FEAASContextImports from '@sitecore-feaas/frontend/types/components/providers/ContextProvider.js'
2
+ import type { AuthModel } from '@sitecore-feaas/sdk'
2
3
  import { FEAASBase, OptionalExcept } from '../components/FEAASBase.js'
3
4
 
4
5
  export function FEAASContextProps(element: HTMLElement = this) {
@@ -8,10 +9,11 @@ export function FEAASContextProps(element: HTMLElement = this) {
8
9
  frontend: element?.getAttribute('frontend') ?? 'https://components.sitecoreclud.io',
9
10
  backend: element?.getAttribute('backend') ?? 'https://components-api.sitecoreclud.io',
10
11
  cdn: element?.getAttribute('cdn') ?? 'https://feaascompontsapiprod.blob.core.windows.net',
11
- getToken: null as (options: any) => Promise<string>
12
+ auth: null as Partial<AuthModel>,
13
+ isolate: element?.getAttribute('isolate') == null || element?.getAttribute('isolate') == 'true'
12
14
  }
13
15
  }
14
- type FEAASContextProps = ReturnType<typeof FEAASContextProps>
16
+ export type FEAASContextProps = ReturnType<typeof FEAASContextProps>
15
17
 
16
18
  /**
17
19
  * Context provides shared parameters and react providers for its nested elements. It loads lazily once any of its
@@ -23,8 +25,12 @@ export class FEAASContext extends FEAASBase<FEAASContextProps, typeof FEAASConte
23
25
  return FEAASContextProps(this)
24
26
  }
25
27
 
28
+ defaultProps = {
29
+ isolate: true
30
+ } as Partial<FEAASContextProps>
31
+
26
32
  load() {
27
- return import(/* @vite-ignore */ /* webpackIgnore: true */ this.formatURL(this.props.src, this.props.frontend))
33
+ return this.import(this.formatURL(this.props.src, this.props.frontend))
28
34
  }
29
35
 
30
36
  isReadyToLoad() {
@@ -38,11 +44,13 @@ export class FEAASContext extends FEAASBase<FEAASContextProps, typeof FEAASConte
38
44
  }
39
45
 
40
46
  getRoot(): HTMLElement | FEAASBase<any, any> | ShadowRoot {
47
+ if (this.getAttribute('isolate') === 'false') return this
41
48
  return this.shadowRoot || this.attachShadow({ mode: 'open' })
42
49
  }
43
50
 
44
51
  reactRootElement: HTMLDivElement
45
52
  getReactRootElement() {
53
+ //if (!this.props.isolate) return this.getRoot()
46
54
  if (!this.reactRootElement) {
47
55
  this.reactRootElement = document.createElement('div')
48
56
  this.getRoot().appendChild(this.reactRootElement)
@@ -81,7 +89,7 @@ export class FEAASContext extends FEAASBase<FEAASContextProps, typeof FEAASConte
81
89
  backend={this.props.backend}
82
90
  libraryId={this.props.library}
83
91
  container={this.getRoot()}
84
- getToken={this.props.getToken}
92
+ auth={this.props.auth}
85
93
  getTheme={this.getTheme}
86
94
  onStatusChange={this.onStatusChange}
87
95
  modalProps={{
@@ -70,6 +70,10 @@ export class FEAASEditor extends FEAASBase<FEAASEditorProps, typeof FEAASEditorI
70
70
 
71
71
  static observedAttributes = ['library', 'component', 'version', 'revision', 'hidden', 'state', 'changed', 'forked']
72
72
 
73
+ defaultProps = {
74
+ hidden: false
75
+ } as Partial<FEAASEditorProps>
76
+
73
77
  getProps() {
74
78
  return FEAASEditorProps(this)
75
79
  }
@@ -230,6 +234,7 @@ export class FEAASEditor extends FEAASBase<FEAASEditorProps, typeof FEAASEditorI
230
234
  }
231
235
 
232
236
  setPlaceholder() {
237
+ if (!this.iframe) return
233
238
  const x = this.iframe.clientWidth
234
239
  const y = this.iframe.clientHeight
235
240
  const rect = this.iframe.getBoundingClientRect()
@@ -16,17 +16,21 @@ export function FEAASPickerProps(element: FEAASBase<any, any> = this) {
16
16
  onContextAttributeSet: undefined as (context: any, attribute: string, value: string) => void
17
17
  }
18
18
  }
19
- type FEAASPickerProps = ReturnType<typeof FEAASPickerProps>
19
+ export type FEAASPickerProps = ReturnType<typeof FEAASPickerProps>
20
20
 
21
21
  export class FEAASPicker extends FEAASBase<FEAASPickerProps, typeof FEAASPickerImports> {
22
22
  static observedAttributes = ['library', 'mode', 'frontend', 'src', 'hidden', 'variant']
23
23
 
24
+ defaultProps = {
25
+ hidden: false
26
+ } as Partial<FEAASPickerProps>
27
+
24
28
  getProps() {
25
29
  return FEAASPickerProps(this)
26
30
  }
27
31
 
28
32
  load() {
29
- return import(/* @vite-ignore */ /* webpackIgnore: true */ this.formatURL(this.props.src, this.props.frontend))
33
+ return this.import(this.formatURL(this.props.src, this.props.frontend))
30
34
  }
31
35
 
32
36
  render(
package/src/ui/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from '../basic.js'
1
+ export * from '../headless.js'
2
2
  export * from './FEAASEditor.js'
3
3
  export * from './FEAASPicker.js'
4
4
  export * from './FEAASContext.js'
@@ -0,0 +1,8 @@
1
+ import { FEAASComponentProps } from '../headless.js'
2
+
3
+ export * from './index.js'
4
+ import React from 'react'
5
+
6
+ export function FEAASEmbed({ data, ...props }: FEAASComponentProps & { src: string }) {
7
+ return <feaas-component {...props} data={data ? JSON.stringify(data) : null} />
8
+ }
@@ -0,0 +1,34 @@
1
+ export type ControlledPromise<T> = Promise<T> & {
2
+ resolve: (...args: any) => void
3
+ reject: (e: Error) => void
4
+ restart: () => ControlledPromise<T>
5
+ first: ControlledPromise<T>
6
+ }
7
+
8
+ export function ControlledPromise<T>(onSuccess?: (result: T) => void, onFailure = (e: Error) => {}) {
9
+ var first: ControlledPromise<T>
10
+ const restart = () => {
11
+ var resolve: (...args: any) => void
12
+ var reject: (e: Error) => void
13
+ const promise = new Promise<T>((success, failure) => {
14
+ resolve = (value: T) => {
15
+ onSuccess?.(value)
16
+ success(value)
17
+ }
18
+ reject = failure
19
+ })
20
+ var controlled: ControlledPromise<T> = Object.assign(promise, {
21
+ resolve,
22
+ reject,
23
+ restart,
24
+ first
25
+ })
26
+ first = controlled
27
+
28
+ // Avoid unhandled rejection
29
+ controlled.catch(onFailure)
30
+
31
+ return controlled
32
+ }
33
+ return restart()
34
+ }
@@ -0,0 +1,195 @@
1
+ import { describe, it, assert, beforeAll, expect } from 'vitest'
2
+ import { FEAASBase, ReactPayload } from '../src/headless.js'
3
+ import { JSDOM } from 'jsdom'
4
+
5
+ describe('React', function () {
6
+ describe('Error boundary', () => {
7
+ class FEAASErrorTest extends FEAASBase<{ throwAt: string }> {
8
+ static observedAttributes = ['test']
9
+
10
+ log() {}
11
+
12
+ getProps() {
13
+ this.fail('get-props')
14
+ return {
15
+ throw: this.getAttribute('throw') ?? null,
16
+ test: this.getAttribute('test')
17
+ }
18
+ }
19
+
20
+ mount() {
21
+ if (this.fail('before-mount', true)) return
22
+ super.mount()
23
+ if (this.fail('after-mount', true)) {
24
+ super.unmount()
25
+ }
26
+ }
27
+
28
+ unmount() {
29
+ this.fail('before-unmount', true)
30
+ super.unmount()
31
+ this.fail('after-unmount', true)
32
+ }
33
+
34
+ connectedCallback(): void {
35
+ super.connectedCallback()
36
+ }
37
+
38
+ load() {
39
+ this.fail('before-load')
40
+ return Promise.all([import('react'), import('react-dom'), import('react-dom/client')]).then(
41
+ ([React, ReactDOM, ReactDOMClient]) => {
42
+ this.fail('after-load')
43
+ return { React, ReactDOM, ReactDOMClient }
44
+ }
45
+ )
46
+ }
47
+
48
+ render({}: typeof this.props, { React }: ReactPayload) {
49
+ const error = console.error
50
+ console.error = () => {}
51
+ requestAnimationFrame(() => (console.error = error))
52
+ this.fail('render')
53
+ return <div>{1}</div>
54
+ }
55
+
56
+ fail(type: string, soft = false) {
57
+ if (
58
+ (this.getAttribute('throw') == 'initial-' + type && !this.initialized) ||
59
+ (this.getAttribute('throw') == 'subsequent-' + type && this.initialized)
60
+ ) {
61
+ const e = new Error(this.getAttribute('throw'))
62
+ if (soft) {
63
+ this.onError(e)
64
+ return true
65
+ } else {
66
+ throw e
67
+ }
68
+ }
69
+ }
70
+ }
71
+ FEAASErrorTest.register('feaas-test-error-boundary')
72
+ const errors = [] as Error[]
73
+ document.addEventListener('feaasError', (e: CustomEvent<Error>) => {
74
+ errors.push(e.detail)
75
+ //e.preventDefault()
76
+ })
77
+
78
+ describe('during initialization', () => {
79
+ it('should render successfuly', async () => {
80
+ document.write('<feaas-test-error-boundary></feaas-test-error-boundary>')
81
+ await (document.body.firstElementChild as FEAASErrorTest).whenRendered
82
+ expect(document.body.innerHTML).toEqual(`<feaas-test-error-boundary><div>1</div></feaas-test-error-boundary>`)
83
+ })
84
+ async function failOn(errorType: string) {
85
+ try {
86
+ document.write(`<feaas-test-error-boundary throw="${errorType}"></feaas-test-error-boundary>`)
87
+ } catch (e) {}
88
+ const component = document.body.firstElementChild as FEAASErrorTest
89
+
90
+ // Initialization promise is rejected
91
+ await Promise.all([
92
+ expect(() => component.whenRendered).rejects.toEqual(new Error(errorType)),
93
+ expect(() => component.whenLoaded).rejects.toEqual(new Error(errorType))
94
+ ])
95
+
96
+ // feaasEvent is dispatched and bubbled to dom
97
+ expect(errors.splice(0)).toEqual([new Error(errorType)])
98
+
99
+ // Component is auto-unmounted (but not ejected from dom)
100
+ expect(component.connected).toBe(false)
101
+ expect(document.body.innerHTML).toEqual(
102
+ `<feaas-test-error-boundary throw="${errorType}"></feaas-test-error-boundary>`
103
+ )
104
+ }
105
+ it('handles error on get-props', () => failOn('initial-get-props'))
106
+ it('handles error on before mount', () => failOn('initial-before-mount'))
107
+ it('handles error on after mount', () => failOn('initial-after-mount'))
108
+ it('handles error on before load', () => failOn('initial-before-load'))
109
+ it('handles error on after load', () => failOn('initial-after-load'))
110
+ })
111
+ describe('during first render', () => {
112
+ it('should render successfuly', async () => {
113
+ document.write('<feaas-test-error-boundary></feaas-test-error-boundary>')
114
+ await (document.body.firstElementChild as FEAASErrorTest).whenRendered
115
+ expect(document.body.innerHTML).toEqual(`<feaas-test-error-boundary><div>1</div></feaas-test-error-boundary>`)
116
+ })
117
+ async function failOn(errorType: string) {
118
+ try {
119
+ document.write(`<feaas-test-error-boundary throw="${errorType}"></feaas-test-error-boundary>`)
120
+ } catch (e) {}
121
+ const component = document.body.firstElementChild as FEAASErrorTest
122
+
123
+ // Initialization promise is rejected
124
+ await Promise.all([
125
+ expect(() => component.whenRendered).rejects.toEqual(new Error(errorType)),
126
+ expect(component.whenLoaded).resolves.toBeTruthy()
127
+ ])
128
+
129
+ // feaasEvent is dispatched and bubbled to dom
130
+ expect(errors.splice(0)).toEqual([new Error(errorType)])
131
+
132
+ // Component is auto-unmounted (but not ejected from dom)
133
+ expect(component.connected).toBe(false)
134
+ expect(document.body.innerHTML).toEqual(
135
+ `<feaas-test-error-boundary throw="${errorType}"></feaas-test-error-boundary>`
136
+ )
137
+ }
138
+ it('handles error on render', () => failOn('initial-render'))
139
+ })
140
+ describe('subsequently', () => {
141
+ it('should render successfuly', async () => {
142
+ document.write('<feaas-test-error-boundary></feaas-test-error-boundary>')
143
+ const component = document.body.firstElementChild as FEAASErrorTest
144
+ await expect(component.whenLoaded).resolves.toBeTruthy()
145
+ component.setAttribute('test', 'lol')
146
+ await expect(component.whenRendered).resolves.toEqual(component)
147
+ expect(document.body.innerHTML).toEqual(
148
+ `<feaas-test-error-boundary test="lol"><div>1</div></feaas-test-error-boundary>`
149
+ )
150
+ })
151
+ async function failOn(errorType: string, callback: (component: FEAASErrorTest) => void) {
152
+ document.write(`<feaas-test-error-boundary throw="${errorType}"></feaas-test-error-boundary>`)
153
+ const component = document.body.firstElementChild as FEAASErrorTest
154
+
155
+ // Initialization promise is resolved
156
+ await expect(component.whenLoaded).resolves.toBeTruthy()
157
+ await expect(component.whenRendered).resolves.toBeTruthy()
158
+
159
+ // No errors were thrown during initialization
160
+ expect(errors.splice(0)).toEqual([])
161
+
162
+ // Component has rendered
163
+ expect(document.body.innerHTML).toEqual(
164
+ `<feaas-test-error-boundary throw="${errorType}"><div>1</div></feaas-test-error-boundary>`
165
+ )
166
+
167
+ // Execute logic after initialization
168
+ callback(component)
169
+ await expect(() => component.whenRendered).rejects.toEqual(new Error(errorType))
170
+
171
+ // feaasEvent is dispatched and bubbled to dom
172
+ expect(errors.splice(0)).toEqual([new Error(errorType)])
173
+
174
+ // Component is auto-unmounted (but not ejected from dom)
175
+ expect(component.connected).toBe(false)
176
+ }
177
+ it('handles error on change', async () => {
178
+ await failOn('subsequent-get-props', (component) => {
179
+ component.setAttribute('test', 'lol')
180
+ })
181
+ expect(document.body.innerHTML).toEqual(
182
+ `<feaas-test-error-boundary throw="subsequent-get-props" test="lol"><div>1</div></feaas-test-error-boundary>`
183
+ )
184
+ })
185
+ it('handles error on render', async () => {
186
+ await failOn('subsequent-render', (component) => {
187
+ component.setAttribute('test', 'lol')
188
+ })
189
+ expect(document.body.innerHTML).toEqual(
190
+ `<feaas-test-error-boundary throw="subsequent-render" test="lol"></feaas-test-error-boundary>`
191
+ )
192
+ })
193
+ })
194
+ })
195
+ })