acrylblends 0.6.3 → 0.6.4

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 (269) hide show
  1. package/README.md +3 -2
  2. package/docs/framework/README.md +2 -2
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/guides/jsx.md +24 -0
  5. package/docs/framework/helpers/ssg.md +3 -6
  6. package/docs/framework/middleware/builtin/jsx-renderer.md +0 -3
  7. package/docs/framework/upstream.json +2 -2
  8. package/lib/accepts.d.ts +1 -1
  9. package/lib/acryl-base.d.ts +1 -1
  10. package/lib/adapter.d.ts +1 -1
  11. package/lib/{app-wFwpg-bt.d.ts → app-D8riatvv.d.ts} +1 -1
  12. package/lib/basic-auth.d.ts +1 -1
  13. package/lib/bearer-auth.d.ts +1 -1
  14. package/lib/body-limit.d.ts +1 -1
  15. package/lib/cache.d.ts +1 -1
  16. package/lib/cache.js +4 -4
  17. package/lib/{client-BHTr318g.js → client-DDLXwhBo.js} +2 -125
  18. package/lib/client.d.ts +2 -2
  19. package/lib/client.js +2 -1
  20. package/lib/combine.d.ts +1 -1
  21. package/lib/{components-Ce5FRyHU.js → components-0o0CBYNv.js} +2 -2
  22. package/lib/{components-DYZdxJIJ.js → components-DdI7-ns7.js} +1 -1
  23. package/lib/compress.d.ts +1 -1
  24. package/lib/conninfo.d.ts +2 -2
  25. package/lib/{context-DVQBjuns.d.ts → context-DS3fYyG-.d.ts} +1 -1
  26. package/lib/context-storage.d.ts +1 -1
  27. package/lib/cookie.d.ts +1 -1
  28. package/lib/cors.d.ts +1 -1
  29. package/lib/csrf.d.ts +1 -1
  30. package/lib/dev.d.ts +2 -2
  31. package/lib/etag.d.ts +1 -1
  32. package/lib/factory.d.ts +2 -2
  33. package/lib/{hooks-Bq5q9IMl.js → hooks-BMw6BAyP.js} +5 -5
  34. package/lib/{index-CUTfU69R.d.ts → index-Bb7YJKCq.d.ts} +2 -2
  35. package/lib/{index-CsvB-F9j.d.ts → index-BvFh1Gfx.d.ts} +1 -1
  36. package/lib/{index-Cqcdluyn.d.ts → index-CTy4N7ng.d.ts} +2 -2
  37. package/lib/index.d.ts +4 -4
  38. package/lib/ip-restriction.d.ts +2 -2
  39. package/lib/jsx/dom/client.js +1 -1
  40. package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
  41. package/lib/jsx/dom/jsx-runtime.js +1 -1
  42. package/lib/jsx/dom/server.js +1 -1
  43. package/lib/jsx/dom.js +3 -3
  44. package/lib/jsx/streaming.js +1 -1
  45. package/lib/{jsx-dev-runtime-kAhR_4C7.js → jsx-dev-runtime-D18Y35TI.js} +1 -1
  46. package/lib/jsx-renderer.d.ts +75 -0
  47. package/lib/jsx-renderer.js +99 -0
  48. package/lib/jsx.js +3 -3
  49. package/lib/jwk.d.ts +1 -1
  50. package/lib/jwt.d.ts +1 -1
  51. package/lib/language.d.ts +1 -1
  52. package/lib/logger.d.ts +1 -1
  53. package/lib/loopback-only.d.ts +1 -1
  54. package/lib/method-not-allowed.d.ts +2 -2
  55. package/lib/method-override.d.ts +2 -2
  56. package/lib/mount.d.ts +1 -1
  57. package/lib/node-server/conninfo.d.ts +1 -1
  58. package/lib/node-server/serve-static.d.ts +1 -1
  59. package/lib/node-server/serve-static.js +1 -1
  60. package/lib/node-server.d.ts +1 -1
  61. package/lib/node-server.js +1 -1
  62. package/lib/powered-by.d.ts +1 -1
  63. package/lib/pretty-json.d.ts +1 -1
  64. package/lib/quick.d.ts +1 -1
  65. package/lib/request-id.d.ts +1 -1
  66. package/lib/request.d.ts +1 -1
  67. package/lib/route.d.ts +1 -1
  68. package/lib/schema-validator.d.ts +1 -1
  69. package/lib/secure-headers.d.ts +1 -1
  70. package/lib/{serve-static-Cw23shXv.js → serve-static-B14MF1Va.js} +1 -0
  71. package/lib/serve-static.d.ts +1 -1
  72. package/lib/ssg-BwjcA-Ge.js +389 -0
  73. package/lib/ssg.d.ts +156 -0
  74. package/lib/ssg.js +2 -0
  75. package/lib/streaming.d.ts +1 -1
  76. package/lib/testing.d.ts +3 -3
  77. package/lib/testing.js +1 -1
  78. package/lib/timeout.d.ts +1 -1
  79. package/lib/timing.d.ts +1 -1
  80. package/lib/tiny.d.ts +1 -1
  81. package/lib/trailing-slash.d.ts +1 -1
  82. package/lib/{types-nEL8VPDC.d.ts → types-Dv_nV2cv.d.ts} +1 -1
  83. package/lib/{types-CQiGmsWW.d.ts → types-zNxyEpG2.d.ts} +2 -2
  84. package/lib/types.d.ts +1 -1
  85. package/lib/utils/body.d.ts +1 -1
  86. package/lib/utils/ipaddr.d.ts +1 -1
  87. package/lib/utils-Cx61GFMC.js +125 -0
  88. package/lib/validator.d.ts +1 -1
  89. package/lib/ws.d.ts +1 -1
  90. package/package.json +9 -1
  91. package/plugins/acryl-inspect/upstream.json +1 -1
  92. package/plugins/acryl-node-server/upstream.json +1 -1
  93. package/plugins/acryl-router/src/core/helper/ssg/index.ts +14 -0
  94. package/plugins/acryl-router/src/core/helper/ssg/middleware.ts +83 -0
  95. package/plugins/acryl-router/src/core/helper/ssg/plugins.ts +72 -0
  96. package/plugins/acryl-router/src/core/helper/ssg/ssg.ts +471 -0
  97. package/plugins/acryl-router/src/core/helper/ssg/utils.ts +118 -0
  98. package/plugins/acryl-router/src/core/jsx/dom/render.ts +6 -6
  99. package/plugins/acryl-router/src/core/middleware/cache/index.ts +4 -4
  100. package/plugins/acryl-router/src/core/middleware/jsx-renderer/index.ts +162 -0
  101. package/plugins/acryl-router/src/helpers/index.ts +1 -0
  102. package/plugins/acryl-router/src/middleware/index.ts +1 -0
  103. package/plugins/acryl-router/upstream.json +2 -2
  104. package/plugins/acryl-router-http/upstream.json +1 -1
  105. package/plugins/acryl-state/upstream.json +1 -1
  106. package/plugins/desktop/upstream.json +1 -1
  107. package/plugins/desktop-web/upstream.json +1 -1
  108. package/plugins/extensions/resolve.ts +4 -0
  109. package/shared/AGENTS.core.md +4 -0
  110. package/templates/agent/tsconfig.json +7 -1
  111. package/templates/todo/upstream.json +1 -1
  112. package/plugins/acryl-inspect/tests/report.spec.ts +0 -69
  113. package/plugins/acryl-inspect/tests/service.spec.ts +0 -121
  114. package/plugins/acryl-node-server/tests/adapter/app.ts +0 -76
  115. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +0 -1
  116. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  117. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +0 -1
  118. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +0 -1
  119. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +0 -1
  120. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +0 -5
  121. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +0 -1
  122. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  123. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +0 -1
  124. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +0 -1
  125. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +0 -1
  126. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +0 -1
  127. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +0 -1
  128. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +0 -1
  129. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +0 -1
  130. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +0 -1
  131. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +0 -34
  132. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +0 -248
  133. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +0 -457
  134. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +0 -23
  135. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +0 -27
  136. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +0 -252
  137. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +0 -190
  138. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +0 -727
  139. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +0 -189
  140. package/plugins/acryl-node-server/tests/adapter/request.test.ts +0 -1230
  141. package/plugins/acryl-node-server/tests/adapter/response.test.ts +0 -285
  142. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +0 -849
  143. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +0 -790
  144. package/plugins/acryl-node-server/tests/adapter/server.test.ts +0 -1312
  145. package/plugins/acryl-node-server/tests/adapter/setup.ts +0 -16
  146. package/plugins/acryl-node-server/tests/adapter/url.test.ts +0 -76
  147. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +0 -23
  148. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +0 -75
  149. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +0 -235
  150. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +0 -260
  151. package/plugins/acryl-node-server/tests/server.spec.ts +0 -141
  152. package/plugins/acryl-router/src/core/app.test.ts +0 -3833
  153. package/plugins/acryl-router/src/core/client/client.test.ts +0 -2313
  154. package/plugins/acryl-router/src/core/client/types.test.ts +0 -149
  155. package/plugins/acryl-router/src/core/client/utils.test.ts +0 -405
  156. package/plugins/acryl-router/src/core/compose.test.ts +0 -751
  157. package/plugins/acryl-router/src/core/context.test.ts +0 -629
  158. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +0 -344
  159. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +0 -55
  160. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +0 -510
  161. package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +0 -506
  162. package/plugins/acryl-router/src/core/helper/css/index.test.tsx +0 -678
  163. package/plugins/acryl-router/src/core/helper/dev/index.test.ts +0 -181
  164. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +0 -434
  165. package/plugins/acryl-router/src/core/helper/html/index.test.ts +0 -78
  166. package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +0 -356
  167. package/plugins/acryl-router/src/core/helper/route/index.test.ts +0 -259
  168. package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +0 -557
  169. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +0 -128
  170. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +0 -34
  171. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +0 -42
  172. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +0 -124
  173. package/plugins/acryl-router/src/core/http-exception.test.ts +0 -45
  174. package/plugins/acryl-router/src/core/jsx/base.test.tsx +0 -97
  175. package/plugins/acryl-router/src/core/jsx/children.test.ts +0 -52
  176. package/plugins/acryl-router/src/core/jsx/components.test.tsx +0 -936
  177. package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +0 -655
  178. package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +0 -128
  179. package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +0 -330
  180. package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +0 -184
  181. package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +0 -149
  182. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +0 -172
  183. package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +0 -3681
  184. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +0 -1344
  185. package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +0 -199
  186. package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +0 -859
  187. package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +0 -40
  188. package/plugins/acryl-router/src/core/jsx/index.test.tsx +0 -1744
  189. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +0 -666
  190. package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +0 -85
  191. package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +0 -1281
  192. package/plugins/acryl-router/src/core/jsx/utils.test.ts +0 -366
  193. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +0 -434
  194. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +0 -1018
  195. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +0 -273
  196. package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +0 -1211
  197. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +0 -355
  198. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +0 -535
  199. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +0 -43
  200. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +0 -532
  201. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +0 -534
  202. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +0 -21
  203. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +0 -503
  204. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +0 -217
  205. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +0 -1135
  206. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +0 -48
  207. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +0 -802
  208. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +0 -552
  209. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +0 -204
  210. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +0 -292
  211. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +0 -221
  212. package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +0 -334
  213. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +0 -37
  214. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +0 -218
  215. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +0 -168
  216. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +0 -671
  217. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +0 -450
  218. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +0 -50
  219. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +0 -70
  220. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +0 -310
  221. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +0 -381
  222. package/plugins/acryl-router/src/core/preset/quick.test.ts +0 -42
  223. package/plugins/acryl-router/src/core/preset/tiny.test.ts +0 -42
  224. package/plugins/acryl-router/src/core/request.test.ts +0 -751
  225. package/plugins/acryl-router/src/core/router/common.case.test.ts +0 -1132
  226. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +0 -80
  227. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +0 -46
  228. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +0 -154
  229. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +0 -265
  230. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +0 -13
  231. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +0 -896
  232. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +0 -8
  233. package/plugins/acryl-router/src/core/types.test.ts +0 -4134
  234. package/plugins/acryl-router/src/core/utils/accept.test.ts +0 -299
  235. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +0 -56
  236. package/plugins/acryl-router/src/core/utils/body.test.ts +0 -496
  237. package/plugins/acryl-router/src/core/utils/buffer.test.ts +0 -147
  238. package/plugins/acryl-router/src/core/utils/color.test.ts +0 -23
  239. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +0 -69
  240. package/plugins/acryl-router/src/core/utils/cookie.test.ts +0 -460
  241. package/plugins/acryl-router/src/core/utils/crypto.test.ts +0 -43
  242. package/plugins/acryl-router/src/core/utils/encode.test.ts +0 -61
  243. package/plugins/acryl-router/src/core/utils/filepath.test.ts +0 -78
  244. package/plugins/acryl-router/src/core/utils/html.test.ts +0 -16
  245. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +0 -149
  246. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +0 -86
  247. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +0 -1788
  248. package/plugins/acryl-router/src/core/utils/mime.test.ts +0 -64
  249. package/plugins/acryl-router/src/core/utils/stream.test.ts +0 -212
  250. package/plugins/acryl-router/src/core/utils/types.test.ts +0 -295
  251. package/plugins/acryl-router/src/core/utils/url.test.ts +0 -384
  252. package/plugins/acryl-router/src/core/validator/utils.test.ts +0 -82
  253. package/plugins/acryl-router/src/core/validator/validator.test.ts +0 -1488
  254. package/plugins/acryl-router/tests/docs-examples.spec.ts +0 -152
  255. package/plugins/acryl-router/tests/jsx.spec.tsx +0 -63
  256. package/plugins/acryl-router/tests/live-install.spec.ts +0 -64
  257. package/plugins/acryl-router/tests/loader.spec.ts +0 -72
  258. package/plugins/acryl-router/tests/loopback-only.spec.ts +0 -80
  259. package/plugins/acryl-router/tests/no-dom/consumer.ts +0 -11
  260. package/plugins/acryl-router/tests/no-dom/tsconfig.json +0 -13
  261. package/plugins/acryl-router/tests/service.spec.ts +0 -426
  262. package/plugins/acryl-router/tests/shadow-state.spec.ts +0 -79
  263. package/plugins/acryl-router-http/tests/adapter.spec.ts +0 -174
  264. package/plugins/acryl-router-http/tests/stream.spec.ts +0 -133
  265. package/plugins/acryl-router-http/tests/surfaces.spec.ts +0 -101
  266. package/plugins/acryl-state/src/react.ts +0 -11
  267. package/plugins/acryl-state/tests/cell.spec.ts +0 -71
  268. package/plugins/acryl-state/tests/react.spec.ts +0 -39
  269. package/plugins/acryl-state/tests/service.spec.ts +0 -132
@@ -1,1344 +0,0 @@
1
- /** @jsxImportSource ../../ */
2
- import { JSDOM, ResourceLoader } from 'jsdom'
3
- import { Suspense, render } from '../index.ts'
4
- import { useState } from '../../hooks/index.ts'
5
- import { clearCache, composeRef } from './components.ts'
6
-
7
- describe('intrinsic element', () => {
8
- let CustomResourceLoader: typeof ResourceLoader
9
- beforeAll(() => {
10
- global.requestAnimationFrame = (cb) => setTimeout(cb)
11
-
12
- CustomResourceLoader = class CustomResourceLoader extends ResourceLoader {
13
- fetch(url: string) {
14
- return url.includes('invalid')
15
- ? Promise.reject('Invalid URL')
16
- : // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- (Promise.resolve(Buffer.from('')) as any)
18
- }
19
- }
20
- })
21
-
22
- let dom: JSDOM
23
- let root: HTMLElement
24
- beforeEach(() => {
25
- clearCache()
26
-
27
- dom = new JSDOM('<html><head></head><body><div id="root"></div></body></html>', {
28
- runScripts: 'dangerously',
29
- resources: new CustomResourceLoader(),
30
- })
31
- global.document = dom.window.document
32
- global.HTMLElement = dom.window.HTMLElement
33
- global.SVGElement = dom.window.SVGElement
34
- global.Text = dom.window.Text
35
- global.FormData = dom.window.FormData
36
- global.CustomEvent = dom.window.CustomEvent
37
- root = document.getElementById('root') as HTMLElement
38
- })
39
-
40
- describe('document metadata', () => {
41
- describe('title element', () => {
42
- it('should be inserted into head', () => {
43
- const App = () => {
44
- return (
45
- <div>
46
- <title>Document Title</title>
47
- Content
48
- </div>
49
- )
50
- }
51
- render(<App />, root)
52
- expect(document.head.innerHTML).toBe('<title>Document Title</title>')
53
- expect(root.innerHTML).toBe('<div>Content</div>')
54
- })
55
-
56
- it('should be updated', async () => {
57
- const App = () => {
58
- const [count, setCount] = useState(0)
59
- return (
60
- <div>
61
- <title>Document Title {count}</title>
62
- <button onClick={() => setCount(count + 1)}>+</button>
63
- </div>
64
- )
65
- }
66
- render(<App />, root)
67
- expect(document.head.innerHTML).toBe('<title>Document Title 0</title>')
68
- expect(root.innerHTML).toBe('<div><button>+</button></div>')
69
- root.querySelector('button')?.click()
70
- await Promise.resolve()
71
- expect(document.head.innerHTML).toBe('<title>Document Title 1</title>')
72
- })
73
-
74
- it('should be removed when unmounted', async () => {
75
- const App = () => {
76
- const [count, setCount] = useState(0)
77
- return (
78
- <div>
79
- {count === 1 && <title>Document Title {count}</title>}
80
- <div>{count}</div>
81
- <button onClick={() => setCount(count + 1)}>+</button>
82
- </div>
83
- )
84
- }
85
- render(<App />, root)
86
- expect(document.head.innerHTML).toBe('')
87
- expect(root.innerHTML).toBe('<div><div>0</div><button>+</button></div>')
88
- root.querySelector('button')?.click()
89
- await Promise.resolve()
90
- expect(document.head.innerHTML).toBe('<title>Document Title 1</title>')
91
- expect(root.innerHTML).toBe('<div><div>1</div><button>+</button></div>')
92
- root.querySelector('button')?.click()
93
- await Promise.resolve()
94
- expect(document.head.innerHTML).toBe('')
95
- expect(root.innerHTML).toBe('<div><div>2</div><button>+</button></div>')
96
- })
97
-
98
- it('should be inserted bottom of head if existing element is removed', async () => {
99
- document.head.innerHTML = '<title>Existing Title</title>'
100
-
101
- const App = () => {
102
- const [count, setCount] = useState(0)
103
- return (
104
- <div>
105
- {count === 1 && <title>Document Title {count}</title>}
106
- <div>{count}</div>
107
- <button onClick={() => setCount(count + 1)}>+</button>
108
- </div>
109
- )
110
- }
111
- render(<App />, root)
112
- expect(document.head.innerHTML).toBe('<title>Existing Title</title>')
113
- expect(root.innerHTML).toBe('<div><div>0</div><button>+</button></div>')
114
- root.querySelector('button')?.click()
115
- document.head.querySelector('title')?.remove()
116
- await Promise.resolve()
117
- expect(document.head.innerHTML).toBe('<title>Document Title 1</title>')
118
- expect(root.innerHTML).toBe('<div><div>1</div><button>+</button></div>')
119
- })
120
-
121
- it('should be inserted before existing title element', async () => {
122
- document.head.innerHTML = '<title>Existing Title</title>'
123
-
124
- const App = () => {
125
- const [count, setCount] = useState(0)
126
- return (
127
- <div>
128
- {count === 1 && <title>Document Title {count}</title>}
129
- <div>{count}</div>
130
- <button onClick={() => setCount(count + 1)}>+</button>
131
- </div>
132
- )
133
- }
134
- render(<App />, root)
135
- expect(document.head.innerHTML).toBe('<title>Existing Title</title>')
136
- expect(root.innerHTML).toBe('<div><div>0</div><button>+</button></div>')
137
- root.querySelector('button')?.click()
138
- await Promise.resolve()
139
- expect(document.head.innerHTML).toBe(
140
- '<title>Document Title 1</title><title>Existing Title</title>'
141
- )
142
- expect(root.innerHTML).toBe('<div><div>1</div><button>+</button></div>')
143
- root.querySelector('button')?.click()
144
- await Promise.resolve()
145
- expect(document.head.innerHTML).toBe('<title>Existing Title</title>')
146
- expect(root.innerHTML).toBe('<div><div>2</div><button>+</button></div>')
147
- })
148
- })
149
-
150
- describe('link element', () => {
151
- it('should be inserted into head', () => {
152
- const App = () => {
153
- return (
154
- <div>
155
- <link rel='stylesheet' href='style.css' precedence='default' />
156
- Content
157
- </div>
158
- )
159
- }
160
- render(<App />, root)
161
- expect(document.head.innerHTML).toBe(
162
- '<link href="style.css" rel="stylesheet" data-precedence="default">'
163
- )
164
- expect(root.innerHTML).toBe('<div>Content</div>')
165
- })
166
-
167
- it('should be updated', async () => {
168
- const App = () => {
169
- const [count, setCount] = useState(0)
170
- return (
171
- <div>
172
- <link rel='stylesheet' href={`style${count}.css`} precedence='default' />
173
- <button onClick={() => setCount(count + 1)}>+</button>
174
- </div>
175
- )
176
- }
177
- render(<App />, root)
178
- expect(document.head.innerHTML).toBe(
179
- '<link href="style0.css" rel="stylesheet" data-precedence="default">'
180
- )
181
- expect(root.innerHTML).toBe('<div><button>+</button></div>')
182
- root.querySelector('button')?.click()
183
- await Promise.resolve()
184
- expect(document.head.innerHTML).toBe(
185
- '<link href="style1.css" rel="stylesheet" data-precedence="default">'
186
- )
187
- })
188
-
189
- it('should not do special behavior if disabled is present', () => {
190
- const App = () => {
191
- return (
192
- <div>
193
- <link rel='stylesheet' href={'style.css'} precedence='default' disabled={true} />
194
- </div>
195
- )
196
- }
197
- render(<App />, root)
198
- expect(document.head.innerHTML).toBe('')
199
- expect(root.innerHTML).toBe(
200
- '<div><link rel="stylesheet" href="style.css" precedence="default" disabled=""></div>'
201
- )
202
- })
203
-
204
- it('should be ordered by precedence attribute', () => {
205
- const App = () => {
206
- return (
207
- <div>
208
- <link rel='stylesheet' href='style-a.css' precedence='default' />
209
- <link rel='stylesheet' href='style-b.css' precedence='high' />
210
- <link rel='stylesheet' href='style-c.css' precedence='default' />
211
- Content
212
- </div>
213
- )
214
- }
215
- render(<App />, root)
216
- expect(document.head.innerHTML).toBe(
217
- '<link href="style-a.css" rel="stylesheet" data-precedence="default"><link href="style-c.css" rel="stylesheet" data-precedence="default"><link href="style-b.css" rel="stylesheet" data-precedence="high">'
218
- )
219
- expect(root.innerHTML).toBe('<div>Content</div>')
220
- })
221
-
222
- it('should be de-duplicated by href attribute', async () => {
223
- const App = () => {
224
- const [count, setCount] = useState(0)
225
- return (
226
- <div>
227
- <link rel='stylesheet' href='style-a.css' precedence='default' />
228
- <link rel='stylesheet' href='style-b.css' precedence='high' />
229
- {count === 1 && (
230
- <>
231
- <link rel='stylesheet' href='style-a.css' precedence='default' />
232
- <link rel='stylesheet' href='style-c.css' precedence='other' />
233
- </>
234
- )}
235
- <button onClick={() => setCount(count + 1)}>+</button>
236
- {count}
237
- </div>
238
- )
239
- }
240
- render(<App />, root)
241
- expect(document.head.innerHTML).toBe(
242
- '<link href="style-a.css" rel="stylesheet" data-precedence="default"><link href="style-b.css" rel="stylesheet" data-precedence="high">'
243
- )
244
- expect(root.innerHTML).toBe('<div><button>+</button>0</div>')
245
- root.querySelector('button')?.click()
246
- await Promise.resolve()
247
- expect(document.head.innerHTML).toBe(
248
- '<link href="style-a.css" rel="stylesheet" data-precedence="default"><link href="style-b.css" rel="stylesheet" data-precedence="high"><link href="style-c.css" rel="stylesheet" data-precedence="other">'
249
- )
250
- expect(root.innerHTML).toBe('<div><button>+</button>1</div>')
251
- })
252
-
253
- it('should be preserved when unmounted', async () => {
254
- const App = () => {
255
- const [count, setCount] = useState(0)
256
- return (
257
- <div>
258
- {count === 1 && <link rel='stylesheet' href='style.css' precedence='default' />}
259
- <div>{count}</div>
260
- <button onClick={() => setCount(count + 1)}>+</button>
261
- </div>
262
- )
263
- }
264
- render(<App />, root)
265
- expect(document.head.innerHTML).toBe('')
266
- expect(root.innerHTML).toBe('<div><div>0</div><button>+</button></div>')
267
- root.querySelector('button')?.click()
268
- await Promise.resolve()
269
- expect(document.head.innerHTML).toBe(
270
- '<link href="style.css" rel="stylesheet" data-precedence="default">'
271
- )
272
- expect(root.innerHTML).toBe('<div><div>1</div><button>+</button></div>')
273
- root.querySelector('button')?.click()
274
- await Promise.resolve()
275
- expect(document.head.innerHTML).toBe(
276
- '<link href="style.css" rel="stylesheet" data-precedence="default">'
277
- )
278
- expect(root.innerHTML).toBe('<div><div>2</div><button>+</button></div>')
279
- })
280
-
281
- it('should be blocked by blocking attribute', async () => {
282
- const Component = () => {
283
- return (
284
- <Suspense fallback={<div>Loading...</div>}>
285
- <div>
286
- <link
287
- rel='stylesheet'
288
- href='http://localhost/style.css'
289
- precedence='default'
290
- blocking='render'
291
- />
292
- Content
293
- </div>
294
- </Suspense>
295
- )
296
- }
297
- const App = () => {
298
- const [show, setShow] = useState(false)
299
- return (
300
- <div>
301
- {show && <Component />}
302
- <button onClick={() => setShow(true)}>Show</button>
303
- </div>
304
- )
305
- }
306
- render(<App />, root)
307
- expect(document.head.innerHTML).toBe('')
308
- expect(root.innerHTML).toBe('<div><button>Show</button></div>')
309
- root.querySelector('button')?.click()
310
- await Promise.resolve()
311
- expect(root.innerHTML).toBe('<div><div>Loading...</div><button>Show</button></div>')
312
- await new Promise((resolve) => setTimeout(resolve))
313
- await Promise.resolve()
314
- expect(root.innerHTML).toBe('<div><div>Content</div><button>Show</button></div>')
315
- })
316
-
317
- describe('React 19 compatibility', () => {
318
- type LinkSignatureInput = {
319
- rel: string
320
- href: string
321
- hrefLang?: string
322
- type?: string
323
- title?: string
324
- media?: string
325
- as?: string
326
- crossOrigin?: string
327
- sizes?: string
328
- dataPrecedence?: string
329
- }
330
-
331
- const sig = (v: LinkSignatureInput) =>
332
- [
333
- v.rel,
334
- v.href,
335
- v.hrefLang ?? '',
336
- v.type ?? '',
337
- v.title ?? '',
338
- v.media ?? '',
339
- v.as ?? '',
340
- v.crossOrigin ?? '',
341
- v.sizes ?? '',
342
- v.dataPrecedence ?? '',
343
- ].join('|')
344
-
345
- const headLinkSignatures = () =>
346
- Array.from(document.head.querySelectorAll('link')).map((el) =>
347
- sig({
348
- rel: el.getAttribute('rel') ?? '',
349
- href: el.getAttribute('href') ?? '',
350
- hrefLang: el.getAttribute('hreflang') ?? '',
351
- type: el.getAttribute('type') ?? '',
352
- title: el.getAttribute('title') ?? '',
353
- media: el.getAttribute('media') ?? '',
354
- as: el.getAttribute('as') ?? '',
355
- crossOrigin: el.getAttribute('crossorigin') ?? '',
356
- sizes: el.getAttribute('sizes') ?? '',
357
- dataPrecedence: el.getAttribute('data-precedence') ?? '',
358
- })
359
- )
360
-
361
- const assertHeadLinks = (
362
- node: unknown,
363
- expected: string[],
364
- expectedRootInnerHTML?: string
365
- ) => {
366
- render(node as never, root)
367
- expect(headLinkSignatures()).toEqual(expected)
368
- if (expectedRootInnerHTML !== undefined) {
369
- expect(root.innerHTML).toBe(expectedRootInnerHTML)
370
- }
371
- }
372
-
373
- it('should keep canonical and alternates in source order', () => {
374
- assertHeadLinks(
375
- <div>
376
- <link rel='canonical' href='https://example.com/en/about' />
377
- <link rel='alternate' hrefLang='en' href='https://example.com/en/about' />
378
- <link rel='alternate' hrefLang='ja' href='https://example.com/ja/about' />
379
- </div>,
380
- [
381
- sig({ rel: 'canonical', href: 'https://example.com/en/about' }),
382
- sig({ rel: 'alternate', href: 'https://example.com/en/about', hrefLang: 'en' }),
383
- sig({ rel: 'alternate', href: 'https://example.com/ja/about', hrefLang: 'ja' }),
384
- ]
385
- )
386
- })
387
-
388
- it('should keep alternate-canonical-alternate order', () => {
389
- assertHeadLinks(
390
- <div>
391
- <link rel='alternate' hrefLang='en' href='https://example.com/en/about' />
392
- <link rel='canonical' href='https://example.com/en/about' />
393
- <link rel='alternate' hrefLang='ja' href='https://example.com/ja/about' />
394
- </div>,
395
- [
396
- sig({ rel: 'alternate', href: 'https://example.com/en/about', hrefLang: 'en' }),
397
- sig({ rel: 'canonical', href: 'https://example.com/en/about' }),
398
- sig({ rel: 'alternate', href: 'https://example.com/ja/about', hrefLang: 'ja' }),
399
- ]
400
- )
401
- })
402
-
403
- it('should not de-duplicate canonical links', () => {
404
- assertHeadLinks(
405
- <div>
406
- <link rel='canonical' href='https://example.com/en/about' />
407
- <link rel='canonical' href='https://example.com/en/about' />
408
- </div>,
409
- [
410
- sig({ rel: 'canonical', href: 'https://example.com/en/about' }),
411
- sig({ rel: 'canonical', href: 'https://example.com/en/about' }),
412
- ]
413
- )
414
- })
415
-
416
- it('should not de-duplicate alternate links', () => {
417
- assertHeadLinks(
418
- <div>
419
- <link rel='alternate' hrefLang='en' href='https://example.com/en/about' />
420
- <link rel='alternate' hrefLang='en' href='https://example.com/en/about' />
421
- </div>,
422
- [
423
- sig({ rel: 'alternate', href: 'https://example.com/en/about', hrefLang: 'en' }),
424
- sig({ rel: 'alternate', href: 'https://example.com/en/about', hrefLang: 'en' }),
425
- ]
426
- )
427
- })
428
-
429
- it('should de-duplicate stylesheet with precedence', () => {
430
- assertHeadLinks(
431
- <div>
432
- <link rel='stylesheet' href='/style.css' precedence='default' />
433
- <link rel='stylesheet' href='/style.css' precedence='default' />
434
- </div>,
435
- [sig({ rel: 'stylesheet', href: '/style.css', dataPrecedence: 'default' })]
436
- )
437
- })
438
-
439
- it('should not de-duplicate stylesheet against preload with same href', () => {
440
- assertHeadLinks(
441
- <div>
442
- <link rel='preload' href='/style.css' as='style' />
443
- <link rel='stylesheet' href='/style.css' precedence='default' />
444
- <link rel='stylesheet' href='/style.css' precedence='default' />
445
- </div>,
446
- [
447
- sig({ rel: 'stylesheet', href: '/style.css', dataPrecedence: 'default' }),
448
- sig({ rel: 'preload', href: '/style.css', as: 'style' }),
449
- ]
450
- )
451
- })
452
-
453
- it('should keep stylesheet links without precedence in root', () => {
454
- assertHeadLinks(
455
- <div>
456
- <link rel='stylesheet' href='/style.css' />
457
- <link rel='stylesheet' href='/style.css' />
458
- </div>,
459
- [],
460
- '<div><link rel="stylesheet" href="/style.css"><link rel="stylesheet" href="/style.css"></div>'
461
- )
462
- })
463
-
464
- it('should keep different stylesheets with same precedence', () => {
465
- assertHeadLinks(
466
- <div>
467
- <link rel='stylesheet' href='/a.css' precedence='default' />
468
- <link rel='stylesheet' href='/b.css' precedence='default' />
469
- </div>,
470
- [
471
- sig({ rel: 'stylesheet', href: '/a.css', dataPrecedence: 'default' }),
472
- sig({ rel: 'stylesheet', href: '/b.css', dataPrecedence: 'default' }),
473
- ]
474
- )
475
- })
476
-
477
- it('should not de-duplicate preload links', () => {
478
- assertHeadLinks(
479
- <div>
480
- <link rel='preload' href='/font.woff2' as='font' crossOrigin='' />
481
- <link rel='preload' href='/font.woff2' as='font' crossOrigin='' />
482
- </div>,
483
- [
484
- sig({ rel: 'preload', href: '/font.woff2', as: 'font', crossOrigin: '' }),
485
- sig({ rel: 'preload', href: '/font.woff2', as: 'font', crossOrigin: '' }),
486
- ]
487
- )
488
- })
489
-
490
- it('should not de-duplicate modulepreload links', () => {
491
- assertHeadLinks(
492
- <div>
493
- <link rel='modulepreload' href='/module.js' />
494
- <link rel='modulepreload' href='/module.js' />
495
- </div>,
496
- [
497
- sig({ rel: 'modulepreload', href: '/module.js' }),
498
- sig({ rel: 'modulepreload', href: '/module.js' }),
499
- ]
500
- )
501
- })
502
-
503
- it('should keep links from two components', () => {
504
- const Head = () => (
505
- <>
506
- <link rel='canonical' href='https://example.com/en/about' />
507
- <link rel='alternate' hrefLang='en' href='https://example.com/en/about' />
508
- <link rel='alternate' hrefLang='ja' href='https://example.com/ja/about' />
509
- </>
510
- )
511
- const Body = () => (
512
- <>
513
- <link rel='canonical' href='https://example.com/en/about' />
514
- <link rel='alternate' hrefLang='en' href='https://example.com/en/about' />
515
- <link rel='alternate' hrefLang='ja' href='https://example.com/ja/about' />
516
- </>
517
- )
518
- assertHeadLinks(
519
- <div>
520
- <Head />
521
- <Body />
522
- </div>,
523
- [
524
- sig({ rel: 'canonical', href: 'https://example.com/en/about' }),
525
- sig({ rel: 'alternate', href: 'https://example.com/en/about', hrefLang: 'en' }),
526
- sig({ rel: 'alternate', href: 'https://example.com/ja/about', hrefLang: 'ja' }),
527
- sig({ rel: 'canonical', href: 'https://example.com/en/about' }),
528
- sig({ rel: 'alternate', href: 'https://example.com/en/about', hrefLang: 'en' }),
529
- sig({ rel: 'alternate', href: 'https://example.com/ja/about', hrefLang: 'ja' }),
530
- ]
531
- )
532
- })
533
-
534
- it('should hoist from deep nested component and keep duplicates', () => {
535
- const Nested = () => (
536
- <div>
537
- <div>
538
- <link rel='canonical' href='https://example.com/en/about' />
539
- </div>
540
- </div>
541
- )
542
- assertHeadLinks(
543
- <div>
544
- <link rel='canonical' href='https://example.com/en/about' />
545
- <Nested />
546
- </div>,
547
- [
548
- sig({ rel: 'canonical', href: 'https://example.com/en/about' }),
549
- sig({ rel: 'canonical', href: 'https://example.com/en/about' }),
550
- ]
551
- )
552
- })
553
-
554
- it('should keep mixed links and de-duplicate only stylesheet', () => {
555
- assertHeadLinks(
556
- <div>
557
- <link rel='canonical' href='https://example.com/en/about' />
558
- <link rel='alternate' hrefLang='en' href='https://example.com/en/about' />
559
- <link rel='stylesheet' href='/style.css' precedence='default' />
560
- <link rel='preload' href='/font.woff2' as='font' crossOrigin='' />
561
- <link rel='canonical' href='https://example.com/en/about' />
562
- <link rel='alternate' hrefLang='en' href='https://example.com/en/about' />
563
- <link rel='stylesheet' href='/style.css' precedence='default' />
564
- <link rel='preload' href='/font.woff2' as='font' crossOrigin='' />
565
- </div>,
566
- [
567
- sig({ rel: 'stylesheet', href: '/style.css', dataPrecedence: 'default' }),
568
- sig({ rel: 'canonical', href: 'https://example.com/en/about' }),
569
- sig({ rel: 'alternate', href: 'https://example.com/en/about', hrefLang: 'en' }),
570
- sig({ rel: 'preload', href: '/font.woff2', as: 'font', crossOrigin: '' }),
571
- sig({ rel: 'canonical', href: 'https://example.com/en/about' }),
572
- sig({ rel: 'alternate', href: 'https://example.com/en/about', hrefLang: 'en' }),
573
- sig({ rel: 'preload', href: '/font.woff2', as: 'font', crossOrigin: '' }),
574
- ]
575
- )
576
- })
577
- })
578
- })
579
-
580
- describe('style element', () => {
581
- it('should be inserted into head', () => {
582
- const App = () => {
583
- return (
584
- <div>
585
- <style href='red' precedence='default'>
586
- {'body { color: red; }'}
587
- </style>
588
- Content
589
- </div>
590
- )
591
- }
592
- render(<App />, root)
593
- expect(document.head.innerHTML).toBe(
594
- '<style data-href="red" data-precedence="default">body { color: red; }</style>'
595
- )
596
- expect(root.innerHTML).toBe('<div>Content</div>')
597
- })
598
-
599
- it('should be updated', async () => {
600
- const App = () => {
601
- const [count, setCount] = useState(0)
602
- return (
603
- <div>
604
- <style href='color' precedence='default'>{`body { color: ${
605
- count % 2 ? 'red' : 'blue'
606
- }; }`}</style>
607
- <button onClick={() => setCount(count + 1)}>+</button>
608
- </div>
609
- )
610
- }
611
- render(<App />, root)
612
- expect(document.head.innerHTML).toBe(
613
- '<style data-href="color" data-precedence="default">body { color: blue; }</style>'
614
- )
615
- expect(root.innerHTML).toBe('<div><button>+</button></div>')
616
- root.querySelector('button')?.click()
617
- await Promise.resolve()
618
- expect(document.head.innerHTML).toBe(
619
- '<style data-href="color" data-precedence="default">body { color: red; }</style>'
620
- )
621
- })
622
-
623
- it('should be preserved when unmounted', async () => {
624
- const App = () => {
625
- const [count, setCount] = useState(0)
626
- return (
627
- <div>
628
- {count === 1 && (
629
- <style href='red' precedence='default'>
630
- {'body { color: red; }'}
631
- </style>
632
- )}
633
- <div>{count}</div>
634
- <button onClick={() => setCount(count + 1)}>+</button>
635
- </div>
636
- )
637
- }
638
- render(<App />, root)
639
- expect(document.head.innerHTML).toBe('')
640
- expect(root.innerHTML).toBe('<div><div>0</div><button>+</button></div>')
641
- root.querySelector('button')?.click()
642
- await Promise.resolve()
643
- expect(document.head.innerHTML).toBe(
644
- '<style data-href="red" data-precedence="default">body { color: red; }</style>'
645
- )
646
- expect(root.innerHTML).toBe('<div><div>1</div><button>+</button></div>')
647
- root.querySelector('button')?.click()
648
- await Promise.resolve()
649
- expect(document.head.innerHTML).toBe(
650
- '<style data-href="red" data-precedence="default">body { color: red; }</style>'
651
- )
652
- expect(root.innerHTML).toBe('<div><div>2</div><button>+</button></div>')
653
- })
654
-
655
- it('should be de-duplicated by href attribute', async () => {
656
- const App = () => {
657
- const [count, setCount] = useState(0)
658
- return (
659
- <div>
660
- <style href='red' precedence='default'>
661
- {'body { color: red; }'}
662
- </style>
663
- <style href='blue' precedence='default'>
664
- {'body { color: blue; }'}
665
- </style>
666
- <style href='green' precedence='default'>
667
- {'body { color: green; }'}
668
- </style>
669
- {count === 1 && (
670
- <>
671
- <style href='blue' precedence='default'>
672
- {'body { color: blue; }'}
673
- </style>
674
- <style href='yellow' precedence='default'>
675
- {'body { color: yellow; }'}
676
- </style>
677
- </>
678
- )}
679
- <button onClick={() => setCount(count + 1)}>+</button>
680
- {count}
681
- </div>
682
- )
683
- }
684
- render(<App />, root)
685
- expect(document.head.innerHTML).toBe(
686
- '<style data-href="red" data-precedence="default">body { color: red; }</style><style data-href="blue" data-precedence="default">body { color: blue; }</style><style data-href="green" data-precedence="default">body { color: green; }</style>'
687
- )
688
- expect(root.innerHTML).toBe('<div><button>+</button>0</div>')
689
- root.querySelector('button')?.click()
690
- await Promise.resolve()
691
- expect(document.head.innerHTML).toBe(
692
- '<style data-href="red" data-precedence="default">body { color: red; }</style><style data-href="blue" data-precedence="default">body { color: blue; }</style><style data-href="green" data-precedence="default">body { color: green; }</style><style data-href="yellow" data-precedence="default">body { color: yellow; }</style>'
693
- )
694
- expect(root.innerHTML).toBe('<div><button>+</button>1</div>')
695
- })
696
-
697
- it('should be ordered by precedence attribute', () => {
698
- const App = () => {
699
- return (
700
- <div>
701
- <style href='red' precedence='default'>
702
- {'body { color: red; }'}
703
- </style>
704
- <style href='green' precedence='high'>
705
- {'body { color: green; }'}
706
- </style>
707
- <style href='blue' precedence='default'>
708
- {'body { color: blue; }'}
709
- </style>
710
- Content
711
- </div>
712
- )
713
- }
714
- render(<App />, root)
715
- expect(document.head.innerHTML).toBe(
716
- '<style data-href="red" data-precedence="default">body { color: red; }</style><style data-href="blue" data-precedence="default">body { color: blue; }</style><style data-href="green" data-precedence="high">body { color: green; }</style>'
717
- )
718
- expect(root.innerHTML).toBe('<div>Content</div>')
719
- })
720
-
721
- it('should not do special behavior if href is present', () => {
722
- const template = (
723
- <html>
724
- <head></head>
725
- <body>
726
- <style>{'body { color: red; }'}</style>
727
- <h1>World</h1>
728
- </body>
729
- </html>
730
- )
731
- render(template, root)
732
- expect(document.head.innerHTML).toBe('')
733
- expect(root.innerHTML).toBe(
734
- '<html><head></head><body><style>body { color: red; }</style><h1>World</h1></body></html>'
735
- )
736
- })
737
- })
738
-
739
- describe('meta element', () => {
740
- it('should be inserted into head', () => {
741
- const App = () => {
742
- return (
743
- <div>
744
- <meta name='description' content='description' />
745
- Content
746
- </div>
747
- )
748
- }
749
- render(<App />, root)
750
- expect(document.head.innerHTML).toBe('<meta name="description" content="description">')
751
- expect(root.innerHTML).toBe('<div>Content</div>')
752
- })
753
-
754
- it('should be updated', async () => {
755
- const App = () => {
756
- const [count, setCount] = useState(0)
757
- return (
758
- <div>
759
- <meta name='description' content={`description ${count}`} />
760
- <button onClick={() => setCount(count + 1)}>+</button>
761
- </div>
762
- )
763
- }
764
- render(<App />, root)
765
- expect(document.head.innerHTML).toBe('<meta name="description" content="description 0">')
766
- expect(root.innerHTML).toBe('<div><button>+</button></div>')
767
- root.querySelector('button')?.click()
768
- await Promise.resolve()
769
- expect(document.head.innerHTML).toBe('<meta name="description" content="description 1">')
770
- })
771
-
772
- it('should not do special behavior if itemProp is present', () => {
773
- const App = () => {
774
- return (
775
- <div>
776
- <meta name='description' content='description' itemProp='test' />
777
- Content
778
- </div>
779
- )
780
- }
781
- render(<App />, root)
782
- expect(document.head.innerHTML).toBe('')
783
- expect(root.innerHTML).toBe(
784
- '<div><meta name="description" content="description" itemprop="test">Content</div>'
785
- )
786
- })
787
-
788
- it('should ignore precedence attribute', () => {
789
- const App = () => {
790
- return (
791
- <div>
792
- <meta name='description-a' content='description-a' precedence='default' />
793
- <meta name='description-b' content='description-b' precedence='high' />
794
- <meta name='description-c' content='description-c' precedence='default' />
795
- Content
796
- </div>
797
- )
798
- }
799
- render(<App />, root)
800
- expect(document.head.innerHTML).toBe(
801
- '<meta name="description-a" content="description-a"><meta name="description-b" content="description-b"><meta name="description-c" content="description-c">'
802
- )
803
- expect(root.innerHTML).toBe('<div>Content</div>')
804
- })
805
-
806
- it('should be de-duplicated by name attribute', async () => {
807
- const App = () => {
808
- const [count, setCount] = useState(0)
809
- return (
810
- <div>
811
- <meta name='description-a' content='description-a' />
812
- <meta name='description-b' content='description-b' />
813
- {count === 1 && (
814
- <>
815
- <meta name='description-a' content='description-a' />
816
- <meta name='description-c' content='description-c' />
817
- </>
818
- )}
819
- <button onClick={() => setCount(count + 1)}>+</button>
820
- {count}
821
- </div>
822
- )
823
- }
824
- render(<App />, root)
825
- expect(document.head.innerHTML).toBe(
826
- '<meta name="description-a" content="description-a"><meta name="description-b" content="description-b">'
827
- )
828
- expect(root.innerHTML).toBe('<div><button>+</button>0</div>')
829
- root.querySelector('button')?.click()
830
- await Promise.resolve()
831
- expect(document.head.innerHTML).toBe(
832
- '<meta name="description-a" content="description-a"><meta name="description-b" content="description-b"><meta name="description-c" content="description-c">'
833
- )
834
- expect(root.innerHTML).toBe('<div><button>+</button>1</div>')
835
- })
836
- })
837
-
838
- describe('script element', () => {
839
- it('should be inserted into head', async () => {
840
- const App = () => {
841
- return (
842
- <div>
843
- <script src='script.js' async={true} />
844
- Content
845
- </div>
846
- )
847
- }
848
- render(<App />, root)
849
- expect(document.head.innerHTML).toBe('<script src="script.js" async=""></script>')
850
- expect(root.innerHTML).toBe('<div>Content</div>')
851
- })
852
-
853
- it('should be updated', async () => {
854
- const App = () => {
855
- const [count, setCount] = useState(0)
856
- return (
857
- <div>
858
- <script src={`script${count}.js`} async={true} />
859
- <button onClick={() => setCount(count + 1)}>+</button>
860
- </div>
861
- )
862
- }
863
- render(<App />, root)
864
- expect(document.head.innerHTML).toBe('<script src="script0.js" async=""></script>')
865
- expect(root.innerHTML).toBe('<div><button>+</button></div>')
866
- root.querySelector('button')?.click()
867
- await Promise.resolve()
868
- expect(document.head.innerHTML).toBe('<script src="script1.js" async=""></script>')
869
- })
870
-
871
- it('should be de-duplicated by src attribute with async=true', async () => {
872
- const App = () => {
873
- const [count, setCount] = useState(0)
874
- return (
875
- <div>
876
- <script src='script-a.js' async={true} />
877
- <script src='script-b.js' async={true} />
878
- {count === 1 && (
879
- <>
880
- <script src='script-a.js' async={true} />
881
- <script src='script-c.js' async={true} />
882
- </>
883
- )}
884
- <button onClick={() => setCount(count + 1)}>+</button>
885
- {count}
886
- </div>
887
- )
888
- }
889
- render(<App />, root)
890
- expect(document.head.innerHTML).toBe(
891
- '<script src="script-a.js" async=""></script><script src="script-b.js" async=""></script>'
892
- )
893
- expect(root.innerHTML).toBe('<div><button>+</button>0</div>')
894
- root.querySelector('button')?.click()
895
- await Promise.resolve()
896
- expect(document.head.innerHTML).toBe(
897
- '<script src="script-a.js" async=""></script><script src="script-b.js" async=""></script><script src="script-c.js" async=""></script>'
898
- )
899
- expect(root.innerHTML).toBe('<div><button>+</button>1</div>')
900
- })
901
-
902
- it('should be preserved when unmounted', async () => {
903
- const App = () => {
904
- const [count, setCount] = useState(0)
905
- return (
906
- <div>
907
- {count === 1 && <script src='script.js' async={true} />}
908
- <div>{count}</div>
909
- <button onClick={() => setCount(count + 1)}>+</button>
910
- </div>
911
- )
912
- }
913
- render(<App />, root)
914
- expect(document.head.innerHTML).toBe('')
915
- expect(root.innerHTML).toBe('<div><div>0</div><button>+</button></div>')
916
- root.querySelector('button')?.click()
917
- await Promise.resolve()
918
- expect(document.head.innerHTML).toBe('<script src="script.js" async=""></script>')
919
- expect(root.innerHTML).toBe('<div><div>1</div><button>+</button></div>')
920
- root.querySelector('button')?.click()
921
- await Promise.resolve()
922
- expect(document.head.innerHTML).toBe('<script src="script.js" async=""></script>')
923
- expect(root.innerHTML).toBe('<div><div>2</div><button>+</button></div>')
924
- })
925
-
926
- it('should be fired onLoad event', async () => {
927
- const onLoad = vi.fn()
928
- const onError = vi.fn()
929
- const App = () => {
930
- return (
931
- <div>
932
- <script
933
- src='http://localhost/script.js'
934
- async={true}
935
- onLoad={onLoad}
936
- onError={onError}
937
- />
938
- Content
939
- </div>
940
- )
941
- }
942
- render(<App />, root)
943
- expect(document.head.innerHTML).toBe(
944
- '<script src="http://localhost/script.js" async=""></script>'
945
- )
946
- await Promise.resolve()
947
- await new Promise((resolve) => setTimeout(resolve))
948
- expect(onLoad).toBeCalledTimes(1)
949
- expect(onError).not.toBeCalled()
950
- })
951
-
952
- it('should be fired onError event', async () => {
953
- const onLoad = vi.fn()
954
- const onError = vi.fn()
955
- const App = () => {
956
- return (
957
- <div>
958
- <script
959
- src='http://localhost/invalid.js'
960
- async={true}
961
- onLoad={onLoad}
962
- onError={onError}
963
- />
964
- Content
965
- </div>
966
- )
967
- }
968
- render(<App />, root)
969
- expect(document.head.innerHTML).toBe(
970
- '<script src="http://localhost/invalid.js" async=""></script>'
971
- )
972
- await Promise.resolve()
973
- await new Promise((resolve) => setTimeout(resolve))
974
- await Promise.resolve()
975
- await new Promise((resolve) => setTimeout(resolve))
976
- await Promise.resolve()
977
- await new Promise((resolve) => setTimeout(resolve))
978
- await Promise.resolve()
979
- await new Promise((resolve) => setTimeout(resolve))
980
- expect(onLoad).not.toBeCalled()
981
- expect(onError).toBeCalledTimes(1)
982
- })
983
-
984
- it('should not register load handlers when the tag has no de-duplication key', () => {
985
- const addEventListener = vi.fn<HTMLElement['addEventListener']>()
986
- const onLoad = vi.fn()
987
- const onError = vi.fn()
988
-
989
- const App = () => {
990
- return (
991
- <div>
992
- <title
993
- ref={(e: HTMLTitleElement) => {
994
- if (!e) {
995
- return
996
- }
997
- const originalAddEventListener = e.addEventListener.bind(e)
998
- addEventListener.mockImplementation((...args) =>
999
- originalAddEventListener(...args)
1000
- )
1001
- e.addEventListener = addEventListener
1002
- }}
1003
- onLoad={onLoad}
1004
- onError={onError}
1005
- >
1006
- Document Title
1007
- </title>
1008
- Content
1009
- </div>
1010
- )
1011
- }
1012
- render(<App />, root)
1013
- expect(document.head.innerHTML).toBe('<title>Document Title</title>')
1014
- expect(root.innerHTML).toBe('<div>Content</div>')
1015
- expect(addEventListener).not.toBeCalled()
1016
- expect(onLoad).not.toBeCalled()
1017
- expect(onError).not.toBeCalled()
1018
- })
1019
-
1020
- it('should be blocked by blocking attribute', async () => {
1021
- const Component = () => {
1022
- return (
1023
- <Suspense fallback={<div>Loading...</div>}>
1024
- <div>
1025
- <script src='http://localhost/script.js' async={true} blocking='render' />
1026
- Content
1027
- </div>
1028
- </Suspense>
1029
- )
1030
- }
1031
- const App = () => {
1032
- const [show, setShow] = useState(false)
1033
- return (
1034
- <div>
1035
- {show && <Component />}
1036
- <button onClick={() => setShow(true)}>Show</button>
1037
- </div>
1038
- )
1039
- }
1040
- render(<App />, root)
1041
- expect(document.head.innerHTML).toBe('')
1042
- expect(root.innerHTML).toBe('<div><button>Show</button></div>')
1043
- root.querySelector('button')?.click()
1044
- await Promise.resolve()
1045
- expect(root.innerHTML).toBe('<div><div>Loading...</div><button>Show</button></div>')
1046
- await new Promise((resolve) => setTimeout(resolve))
1047
- await Promise.resolve()
1048
- expect(root.innerHTML).toBe('<div><div>Content</div><button>Show</button></div>')
1049
- })
1050
-
1051
- it('should be inserted into body if has no props', async () => {
1052
- const App = () => {
1053
- return (
1054
- <div>
1055
- <script>alert('Hello')</script>
1056
- </div>
1057
- )
1058
- }
1059
- render(<App />, root)
1060
- expect(document.head.innerHTML).toBe('')
1061
- // prettier-ignore
1062
- expect(root.innerHTML).toBe('<div><script>alert(\'Hello\')</script></div>')
1063
- })
1064
-
1065
- it('should be inserted into body if has only src prop', async () => {
1066
- const App = () => {
1067
- return (
1068
- <div>
1069
- <script src='script.js'></script>
1070
- </div>
1071
- )
1072
- }
1073
- render(<App />, root)
1074
- expect(document.head.innerHTML).toBe('')
1075
- expect(root.innerHTML).toBe('<div><script src="script.js"></script></div>')
1076
- })
1077
- })
1078
-
1079
- it('accept ref object', async () => {
1080
- const ref = { current: null }
1081
- const App = () => {
1082
- return (
1083
- <div>
1084
- <script src='script-a.js' ref={ref} async={true} />
1085
- </div>
1086
- )
1087
- }
1088
- render(<App />, root)
1089
- expect(ref.current).toBe(document.head.querySelector('script'))
1090
- })
1091
-
1092
- it('accept ref function', async () => {
1093
- const ref = vi.fn()
1094
- const App = () => {
1095
- return (
1096
- <div>
1097
- <script src='script-a.js' ref={ref} async={true} />
1098
- </div>
1099
- )
1100
- }
1101
- render(<App />, root)
1102
- expect(ref).toHaveBeenCalledTimes(1)
1103
- })
1104
- })
1105
-
1106
- describe('form element', () => {
1107
- it('should accept Function as action', () => {
1108
- const action = vi.fn()
1109
- const App = () => {
1110
- return (
1111
- <form action={action} method='post'>
1112
- <input type='text' name='name' value='Hello' />
1113
- <button type='submit'>Submit</button>
1114
- </form>
1115
- )
1116
- }
1117
- render(<App />, root)
1118
- root.querySelector('button')?.click()
1119
- expect(action).toBeCalledTimes(1)
1120
- const formData = action.mock.calls[0][0]
1121
- expect(formData.get('name')).toBe('Hello')
1122
- })
1123
-
1124
- it('should accept string as action', () => {
1125
- const App = () => {
1126
- return (
1127
- <form action={'/entries'} method='post'>
1128
- <button type='submit'>Submit</button>
1129
- </form>
1130
- )
1131
- }
1132
- render(<App />, root)
1133
- expect(root.innerHTML).toBe(
1134
- '<form method="post" action="/entries"><button type="submit">Submit</button></form>'
1135
- )
1136
- })
1137
-
1138
- it('toggle show / hide form', async () => {
1139
- const action = vi.fn()
1140
- const App = () => {
1141
- const [show, setShow] = useState(false)
1142
- return (
1143
- <div>
1144
- {show && (
1145
- <form action={action} method='post'>
1146
- <input type='text' name='name' value='Hello' />
1147
- </form>
1148
- )}
1149
- <button onClick={() => setShow((status) => !status)}>Toggle</button>
1150
- </div>
1151
- )
1152
- }
1153
- render(<App />, root)
1154
- expect(root.innerHTML).toBe('<div><button>Toggle</button></div>')
1155
- root.querySelector('button')?.click()
1156
- await Promise.resolve()
1157
- expect(root.innerHTML).toBe(
1158
- '<div><form method="post"><input type="text" name="name" value="Hello"></form><button>Toggle</button></div>'
1159
- )
1160
- root.querySelector('button')?.click()
1161
- await Promise.resolve()
1162
- await Promise.resolve()
1163
- await Promise.resolve()
1164
- expect(root.innerHTML).toBe('<div><button>Toggle</button></div>')
1165
- })
1166
- })
1167
-
1168
- describe('input element', () => {
1169
- it('should accept Function as formAction', () => {
1170
- const action = vi.fn()
1171
- const App = () => {
1172
- return (
1173
- <form>
1174
- <input type='text' name='name' value='Hello' />
1175
- <input type='submit' value='Submit' formAction={action} />
1176
- </form>
1177
- )
1178
- }
1179
- render(<App />, root)
1180
- root.querySelector<HTMLInputElement>('input[type="submit"]')?.click()
1181
- expect(action).toBeCalledTimes(1)
1182
- const formData = action.mock.calls[0][0]
1183
- expect(formData.get('name')).toBe('Hello')
1184
- })
1185
-
1186
- it('should accept string as formAction', () => {
1187
- const App = () => {
1188
- return (
1189
- <form method='post'>
1190
- <input type='submit' formAction={'/entries'} value='Submit' />
1191
- </form>
1192
- )
1193
- }
1194
- render(<App />, root)
1195
- expect(root.innerHTML).toBe('<form method="post"><input type="submit" value="Submit"></form>')
1196
- })
1197
-
1198
- it('toggle show / hide input', async () => {
1199
- const action = vi.fn()
1200
- const App = () => {
1201
- const [show, setShow] = useState(false)
1202
- return (
1203
- <div>
1204
- {show && (
1205
- <form method='post'>
1206
- <input type='submit' formAction={action} value='Submit' />
1207
- </form>
1208
- )}
1209
- <button onClick={() => setShow((status) => !status)}>Toggle</button>
1210
- </div>
1211
- )
1212
- }
1213
- render(<App />, root)
1214
- expect(root.innerHTML).toBe('<div><button>Toggle</button></div>')
1215
- root.querySelector('button')?.click()
1216
- await Promise.resolve()
1217
- expect(root.innerHTML).toBe(
1218
- '<div><form method="post"><input type="submit" value="Submit"></form><button>Toggle</button></div>'
1219
- )
1220
- root.querySelector('button')?.click()
1221
- await Promise.resolve()
1222
- await Promise.resolve()
1223
- await Promise.resolve()
1224
- expect(root.innerHTML).toBe('<div><button>Toggle</button></div>')
1225
- })
1226
- })
1227
-
1228
- describe('button element', () => {
1229
- it('should accept Function as formAction', () => {
1230
- const action = vi.fn()
1231
- const App = () => {
1232
- return (
1233
- <form>
1234
- <input type='text' name='name' value='Hello' />
1235
- <button type='submit' formAction={action}>
1236
- Submit
1237
- </button>
1238
- </form>
1239
- )
1240
- }
1241
- render(<App />, root)
1242
- root.querySelector('button')?.click()
1243
- expect(action).toBeCalledTimes(1)
1244
- const formData = action.mock.calls[0][0]
1245
- expect(formData.get('name')).toBe('Hello')
1246
- })
1247
-
1248
- it('should accept string as formAction', () => {
1249
- const App = () => {
1250
- return (
1251
- <form method='post'>
1252
- <button type='submit' formAction={'/entries'}>
1253
- Submit
1254
- </button>
1255
- </form>
1256
- )
1257
- }
1258
- render(<App />, root)
1259
- expect(root.innerHTML).toBe(
1260
- '<form method="post"><button type="submit">Submit</button></form>'
1261
- )
1262
- })
1263
-
1264
- it('toggle show / hide', async () => {
1265
- const action = vi.fn()
1266
- const App = () => {
1267
- const [show, setShow] = useState(false)
1268
- return (
1269
- <div>
1270
- {show && (
1271
- <form method='post'>
1272
- <button formAction={action}>Submit</button>
1273
- </form>
1274
- )}
1275
- <button id='toggle' onClick={() => setShow((status) => !status)}>
1276
- Toggle
1277
- </button>
1278
- </div>
1279
- )
1280
- }
1281
- render(<App />, root)
1282
- expect(root.innerHTML).toBe('<div><button id="toggle">Toggle</button></div>')
1283
- root.querySelector<HTMLButtonElement>('#toggle')?.click()
1284
- await Promise.resolve()
1285
- expect(root.innerHTML).toBe(
1286
- '<div><form method="post"><button>Submit</button></form><button id="toggle">Toggle</button></div>'
1287
- )
1288
- root.querySelector<HTMLButtonElement>('#toggle')?.click()
1289
- await Promise.resolve()
1290
- await Promise.resolve()
1291
- await Promise.resolve()
1292
- expect(root.innerHTML).toBe('<div><button id="toggle">Toggle</button></div>')
1293
- })
1294
- })
1295
- })
1296
-
1297
- describe('internal utility method', () => {
1298
- describe('composeRef()', () => {
1299
- it('should compose a ref object', () => {
1300
- const ref = { current: null }
1301
- const cbCleanUp = vi.fn()
1302
- const cb = vi.fn().mockReturnValue(cbCleanUp)
1303
- const composed = composeRef(ref, cb)
1304
- const cleanup = composed('ref')
1305
- expect(ref.current).toBe('ref')
1306
- expect(cb).toBeCalledWith('ref')
1307
- expect(cbCleanUp).not.toBeCalled()
1308
- cleanup()
1309
- expect(ref.current).toBe(null)
1310
- expect(cbCleanUp).toBeCalledTimes(1)
1311
- })
1312
-
1313
- it('should compose a function', () => {
1314
- const ref = vi.fn()
1315
- const cbCleanUp = vi.fn()
1316
- const cb = vi.fn().mockReturnValue(cbCleanUp)
1317
- const composed = composeRef(ref, cb)
1318
- const cleanup = composed('ref')
1319
- expect(ref).toBeCalledWith('ref')
1320
- expect(cb).toBeCalledWith('ref')
1321
- expect(cbCleanUp).not.toBeCalled()
1322
- cleanup()
1323
- expect(ref).toBeCalledWith(null)
1324
- expect(cbCleanUp).toBeCalledTimes(1)
1325
- })
1326
-
1327
- it('should compose a function returns a cleanup function', () => {
1328
- const refCleanUp = vi.fn()
1329
- const ref = vi.fn().mockReturnValue(refCleanUp)
1330
- const cbCleanUp = vi.fn()
1331
- const cb = vi.fn().mockReturnValue(cbCleanUp)
1332
- const composed = composeRef(ref, cb)
1333
- const cleanup = composed('ref')
1334
- expect(ref).toBeCalledWith('ref')
1335
- expect(cb).toBeCalledWith('ref')
1336
- expect(refCleanUp).not.toBeCalled()
1337
- expect(cbCleanUp).not.toBeCalled()
1338
- cleanup()
1339
- expect(ref).toHaveBeenCalledTimes(1)
1340
- expect(refCleanUp).toBeCalledTimes(1)
1341
- expect(cbCleanUp).toBeCalledTimes(1)
1342
- })
1343
- })
1344
- })