acrylblends 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/docs/framework/README.md +3 -2
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/api/presets.md +0 -3
- package/docs/framework/cordis/acryl-in-a-cordis-app.md +9 -0
- package/docs/framework/getting-started/basic.md +5 -4
- package/docs/framework/getting-started/desktop.md +41 -0
- package/docs/framework/guides/jsx-dom.md +0 -3
- package/docs/framework/guides/jsx.md +1 -4
- package/docs/framework/guides/middleware.md +1 -1
- package/docs/framework/helpers/cookie.md +1 -1
- package/docs/framework/helpers/css.md +0 -3
- package/docs/framework/helpers/dev.md +0 -3
- package/docs/framework/helpers/jwt.md +1 -1
- package/docs/framework/helpers/proxy.md +0 -3
- package/docs/framework/middleware/builtin/basic-auth.md +6 -6
- package/docs/framework/middleware/builtin/cache.md +0 -3
- package/docs/framework/middleware/builtin/combine.md +1 -1
- package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
- package/docs/framework/middleware/builtin/logger.md +1 -1
- package/docs/framework/middleware/builtin/mount.md +0 -3
- package/docs/framework/upstream.json +3 -3
- package/lib/accepts.d.ts +1 -1
- package/lib/acryl-base.d.ts +1 -1
- package/lib/adapter.d.ts +1 -1
- package/lib/{app-CmAYW1Al.d.ts → app-wFwpg-bt.d.ts} +1 -1
- package/lib/base-BpQy2Rk7.d.ts +783 -0
- package/lib/base-ddiVmiE7.js +721 -0
- package/lib/basic-auth.d.ts +1 -1
- package/lib/bearer-auth.d.ts +1 -1
- package/lib/body-limit.d.ts +1 -1
- package/lib/cache.d.ts +43 -0
- package/lib/cache.js +169 -0
- package/lib/client.d.ts +2 -2
- package/lib/combine.d.ts +1 -1
- package/lib/common-D8Z4JRCw.d.ts +46 -0
- package/lib/components-Ce5FRyHU.js +268 -0
- package/lib/components-DYZdxJIJ.js +24 -0
- package/lib/compress.d.ts +1 -1
- package/lib/conninfo.d.ts +2 -2
- package/lib/constants-BR9jM9BC.js +9 -0
- package/lib/{context-Bm7rI-oK.d.ts → context-DVQBjuns.d.ts} +1 -1
- package/lib/context-DxFdY_un.d.ts +250 -0
- package/lib/context-storage.d.ts +1 -1
- package/lib/{cookie-ClMaKDQf.d.ts → cookie-CCelEqky.d.ts} +1 -1
- package/lib/cookie.d.ts +3 -3
- package/lib/cors.d.ts +1 -1
- package/lib/csrf.d.ts +1 -1
- package/lib/css-D2jb7X8b.js +320 -0
- package/lib/css.d.ts +82 -0
- package/lib/css.js +134 -0
- package/lib/dev.d.ts +19 -0
- package/lib/dev.js +50 -0
- package/lib/etag.d.ts +1 -1
- package/lib/factory.d.ts +2 -2
- package/lib/hooks-Bq5q9IMl.js +762 -0
- package/lib/html-SllAe0YT.d.ts +46 -0
- package/lib/html.d.ts +1 -1
- package/lib/index-C850MeBA.d.ts +94 -0
- package/lib/{index-oOClm_Xk.d.ts → index-CUTfU69R.d.ts} +3 -3
- package/lib/{index-B-3F4wt2.d.ts → index-Cqcdluyn.d.ts} +2 -2
- package/lib/{index-UTBmM3GX.d.ts → index-CsvB-F9j.d.ts} +2 -2
- package/lib/index.d.ts +4 -4
- package/lib/ip-restriction.d.ts +2 -2
- package/lib/jsx/dom/client.d.ts +30 -0
- package/lib/jsx/dom/client.js +49 -0
- package/lib/jsx/dom/css.d.ts +84 -0
- package/lib/jsx/dom/css.js +2 -0
- package/lib/jsx/dom/jsx-dev-runtime.d.ts +3 -0
- package/lib/jsx/dom/jsx-dev-runtime.js +2 -0
- package/lib/jsx/dom/jsx-runtime.d.ts +3 -0
- package/lib/jsx/dom/jsx-runtime.js +2 -0
- package/lib/jsx/dom/server.d.ts +105 -0
- package/lib/jsx/dom/server.js +37 -0
- package/lib/jsx/dom.d.ts +5 -0
- package/lib/jsx/dom.js +74 -0
- package/lib/jsx/jsx-dev-runtime.d.ts +6 -0
- package/lib/jsx/jsx-dev-runtime.js +14 -0
- package/lib/jsx/jsx-runtime.d.ts +10 -0
- package/lib/jsx/jsx-runtime.js +30 -0
- package/lib/jsx/streaming.d.ts +2 -0
- package/lib/jsx/streaming.js +2 -0
- package/lib/jsx-dev-runtime-B22O-Npk.d.ts +7 -0
- package/lib/jsx-dev-runtime-kAhR_4C7.js +354 -0
- package/lib/jsx-runtime-BVVgDSdq.d.ts +1 -0
- package/lib/jsx.d.ts +3 -0
- package/lib/jsx.js +47 -0
- package/lib/jwk.d.ts +4 -4
- package/lib/{jwt-Dg493Tai.d.ts → jwt-BARhotDz.d.ts} +1 -1
- package/lib/jwt.d.ts +4 -4
- package/lib/language.d.ts +1 -1
- package/lib/linear-router-C3FPib6h.js +96 -0
- package/lib/logger.d.ts +1 -1
- package/lib/loopback-only.d.ts +1 -1
- package/lib/method-not-allowed.d.ts +2 -2
- package/lib/method-override.d.ts +2 -2
- package/lib/mount.d.ts +44 -0
- package/lib/mount.js +67 -0
- package/lib/node-server/conninfo.d.ts +1 -1
- package/lib/node-server/serve-static.d.ts +1 -1
- package/lib/node-server/serve-static.js +1 -1
- package/lib/node-server.d.ts +1 -1
- package/lib/node-server.js +1 -1
- package/lib/pattern-router-HZxjDGEQ.js +41 -0
- package/lib/powered-by.d.ts +1 -1
- package/lib/pretty-json.d.ts +1 -1
- package/lib/proxy.d.ts +72 -0
- package/lib/proxy.js +121 -0
- package/lib/quick.d.ts +8 -0
- package/lib/quick.js +17 -0
- package/lib/request-id.d.ts +1 -1
- package/lib/request.d.ts +1 -1
- package/lib/route.d.ts +1 -1
- package/lib/router/linear-router.js +1 -95
- package/lib/router/pattern-router.js +1 -40
- package/lib/schema-validator.d.ts +1 -1
- package/lib/secure-headers.d.ts +1 -1
- package/lib/{serve-static-Dw_m6KxR.js → serve-static-Cw23shXv.js} +1 -0
- package/lib/serve-static.d.ts +1 -1
- package/lib/streaming.d.ts +1 -1
- package/lib/testing.d.ts +4 -4
- package/lib/timeout.d.ts +1 -1
- package/lib/timing.d.ts +1 -1
- package/lib/tiny.d.ts +8 -0
- package/lib/tiny.js +15 -0
- package/lib/trailing-slash.d.ts +1 -1
- package/lib/{types-CHesD1KG.d.ts → types-CQiGmsWW.d.ts} +3 -3
- package/lib/{types-CwrQjlaM.d.ts → types-Lqx5BxQF.d.ts} +10 -1
- package/lib/{types-CDO6I3B0.d.ts → types-nEL8VPDC.d.ts} +1 -1
- package/lib/types.d.ts +1 -1
- package/lib/utils/body.d.ts +1 -1
- package/lib/utils/cookie.d.ts +1 -1
- package/lib/utils/crypto.d.ts +1 -1
- package/lib/utils/html.d.ts +1 -45
- package/lib/utils/ipaddr.d.ts +1 -1
- package/lib/utils/jwt.d.ts +1 -1
- package/lib/utils/types.d.ts +2 -2
- package/lib/validator.d.ts +2 -2
- package/lib/ws.d.ts +1 -1
- package/package.json +69 -1
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-node-server/upstream.json +1 -1
- package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +506 -0
- package/plugins/acryl-router/src/core/helper/css/common.ts +337 -0
- package/plugins/acryl-router/src/core/helper/css/index.test.tsx +678 -0
- package/plugins/acryl-router/src/core/helper/css/index.ts +262 -0
- package/plugins/acryl-router/src/core/helper/dev/index.test.ts +181 -0
- package/plugins/acryl-router/src/core/helper/dev/index.ts +79 -0
- package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +356 -0
- package/plugins/acryl-router/src/core/helper/proxy/index.ts +203 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +557 -0
- package/plugins/acryl-router/src/core/jsx/base.test.tsx +97 -0
- package/plugins/acryl-router/src/core/jsx/base.ts +490 -0
- package/plugins/acryl-router/src/core/jsx/children.test.ts +52 -0
- package/plugins/acryl-router/src/core/jsx/children.ts +20 -0
- package/plugins/acryl-router/src/core/jsx/components.test.tsx +936 -0
- package/plugins/acryl-router/src/core/jsx/components.ts +257 -0
- package/plugins/acryl-router/src/core/jsx/constants.ts +6 -0
- package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +655 -0
- package/plugins/acryl-router/src/core/jsx/context.ts +266 -0
- package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +128 -0
- package/plugins/acryl-router/src/core/jsx/dom/client.ts +89 -0
- package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +330 -0
- package/plugins/acryl-router/src/core/jsx/dom/components.ts +39 -0
- package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +184 -0
- package/plugins/acryl-router/src/core/jsx/dom/context.ts +52 -0
- package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +149 -0
- package/plugins/acryl-router/src/core/jsx/dom/css.ts +263 -0
- package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +172 -0
- package/plugins/acryl-router/src/core/jsx/dom/hooks/index.ts +91 -0
- package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +3681 -0
- package/plugins/acryl-router/src/core/jsx/dom/index.ts +170 -0
- package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +1344 -0
- package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.ts +438 -0
- package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.ts +23 -0
- package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.ts +8 -0
- package/plugins/acryl-router/src/core/jsx/dom/render.ts +879 -0
- package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +199 -0
- package/plugins/acryl-router/src/core/jsx/dom/server.ts +76 -0
- package/plugins/acryl-router/src/core/jsx/dom/utils.ts +7 -0
- package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +859 -0
- package/plugins/acryl-router/src/core/jsx/hooks/index.ts +437 -0
- package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +40 -0
- package/plugins/acryl-router/src/core/jsx/index.test.tsx +1744 -0
- package/plugins/acryl-router/src/core/jsx/index.ts +115 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.ts +23 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +666 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.ts +210 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-elements.ts +938 -0
- package/plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts +26 -0
- package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +85 -0
- package/plugins/acryl-router/src/core/jsx/jsx-runtime.ts +51 -0
- package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +1281 -0
- package/plugins/acryl-router/src/core/jsx/streaming.ts +231 -0
- package/plugins/acryl-router/src/core/jsx/types.ts +41 -0
- package/plugins/acryl-router/src/core/jsx/utils.test.ts +366 -0
- package/plugins/acryl-router/src/core/jsx/utils.ts +202 -0
- package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +1211 -0
- package/plugins/acryl-router/src/core/middleware/cache/index.ts +323 -0
- package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +334 -0
- package/plugins/acryl-router/src/core/middleware/mount/index.ts +108 -0
- package/plugins/acryl-router/src/core/preset/quick.test.ts +42 -0
- package/plugins/acryl-router/src/core/preset/quick.ts +24 -0
- package/plugins/acryl-router/src/core/preset/tiny.test.ts +42 -0
- package/plugins/acryl-router/src/core/preset/tiny.ts +20 -0
- package/plugins/acryl-router/src/core/utils/types.ts +9 -0
- package/plugins/acryl-router/src/helpers/index.ts +3 -0
- package/plugins/acryl-router/src/middleware/index.ts +2 -0
- package/plugins/acryl-router/src/service.ts +22 -5
- package/plugins/acryl-router/tests/jsx.spec.tsx +63 -0
- package/plugins/acryl-router/upstream.json +3 -3
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/upstream.json +1 -1
- package/plugins/save/plugin.json +1 -1
- package/templates/todo/upstream.json +1 -1
|
@@ -0,0 +1,1344 @@
|
|
|
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
|
+
})
|