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.
- package/README.md +3 -2
- package/docs/framework/README.md +2 -2
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/guides/jsx.md +24 -0
- package/docs/framework/helpers/ssg.md +3 -6
- package/docs/framework/middleware/builtin/jsx-renderer.md +0 -3
- package/docs/framework/upstream.json +2 -2
- 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-wFwpg-bt.d.ts → app-D8riatvv.d.ts} +1 -1
- 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 +1 -1
- package/lib/cache.js +4 -4
- package/lib/{client-BHTr318g.js → client-DDLXwhBo.js} +2 -125
- package/lib/client.d.ts +2 -2
- package/lib/client.js +2 -1
- package/lib/combine.d.ts +1 -1
- package/lib/{components-Ce5FRyHU.js → components-0o0CBYNv.js} +2 -2
- package/lib/{components-DYZdxJIJ.js → components-DdI7-ns7.js} +1 -1
- package/lib/compress.d.ts +1 -1
- package/lib/conninfo.d.ts +2 -2
- package/lib/{context-DVQBjuns.d.ts → context-DS3fYyG-.d.ts} +1 -1
- package/lib/context-storage.d.ts +1 -1
- package/lib/cookie.d.ts +1 -1
- package/lib/cors.d.ts +1 -1
- package/lib/csrf.d.ts +1 -1
- package/lib/dev.d.ts +2 -2
- package/lib/etag.d.ts +1 -1
- package/lib/factory.d.ts +2 -2
- package/lib/{hooks-Bq5q9IMl.js → hooks-BMw6BAyP.js} +5 -5
- package/lib/{index-CUTfU69R.d.ts → index-Bb7YJKCq.d.ts} +2 -2
- package/lib/{index-CsvB-F9j.d.ts → index-BvFh1Gfx.d.ts} +1 -1
- package/lib/{index-Cqcdluyn.d.ts → index-CTy4N7ng.d.ts} +2 -2
- package/lib/index.d.ts +4 -4
- package/lib/ip-restriction.d.ts +2 -2
- package/lib/jsx/dom/client.js +1 -1
- package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
- package/lib/jsx/dom/jsx-runtime.js +1 -1
- package/lib/jsx/dom/server.js +1 -1
- package/lib/jsx/dom.js +3 -3
- package/lib/jsx/streaming.js +1 -1
- package/lib/{jsx-dev-runtime-kAhR_4C7.js → jsx-dev-runtime-D18Y35TI.js} +1 -1
- package/lib/jsx-renderer.d.ts +75 -0
- package/lib/jsx-renderer.js +99 -0
- package/lib/jsx.js +3 -3
- package/lib/jwk.d.ts +1 -1
- package/lib/jwt.d.ts +1 -1
- package/lib/language.d.ts +1 -1
- 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 +1 -1
- 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/powered-by.d.ts +1 -1
- package/lib/pretty-json.d.ts +1 -1
- package/lib/quick.d.ts +1 -1
- package/lib/request-id.d.ts +1 -1
- package/lib/request.d.ts +1 -1
- package/lib/route.d.ts +1 -1
- package/lib/schema-validator.d.ts +1 -1
- package/lib/secure-headers.d.ts +1 -1
- package/lib/{serve-static-Cw23shXv.js → serve-static-B14MF1Va.js} +1 -0
- package/lib/serve-static.d.ts +1 -1
- package/lib/ssg-BwjcA-Ge.js +389 -0
- package/lib/ssg.d.ts +156 -0
- package/lib/ssg.js +2 -0
- package/lib/streaming.d.ts +1 -1
- package/lib/testing.d.ts +3 -3
- package/lib/testing.js +1 -1
- package/lib/timeout.d.ts +1 -1
- package/lib/timing.d.ts +1 -1
- package/lib/tiny.d.ts +1 -1
- package/lib/trailing-slash.d.ts +1 -1
- package/lib/{types-nEL8VPDC.d.ts → types-Dv_nV2cv.d.ts} +1 -1
- package/lib/{types-CQiGmsWW.d.ts → types-zNxyEpG2.d.ts} +2 -2
- package/lib/types.d.ts +1 -1
- package/lib/utils/body.d.ts +1 -1
- package/lib/utils/ipaddr.d.ts +1 -1
- package/lib/utils-Cx61GFMC.js +125 -0
- package/lib/validator.d.ts +1 -1
- package/lib/ws.d.ts +1 -1
- package/package.json +9 -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/ssg/index.ts +14 -0
- package/plugins/acryl-router/src/core/helper/ssg/middleware.ts +83 -0
- package/plugins/acryl-router/src/core/helper/ssg/plugins.ts +72 -0
- package/plugins/acryl-router/src/core/helper/ssg/ssg.ts +471 -0
- package/plugins/acryl-router/src/core/helper/ssg/utils.ts +118 -0
- package/plugins/acryl-router/src/core/jsx/dom/render.ts +6 -6
- package/plugins/acryl-router/src/core/middleware/cache/index.ts +4 -4
- package/plugins/acryl-router/src/core/middleware/jsx-renderer/index.ts +162 -0
- package/plugins/acryl-router/src/helpers/index.ts +1 -0
- package/plugins/acryl-router/src/middleware/index.ts +1 -0
- package/plugins/acryl-router/upstream.json +2 -2
- 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/extensions/resolve.ts +4 -0
- package/shared/AGENTS.core.md +4 -0
- package/templates/agent/tsconfig.json +7 -1
- package/templates/todo/upstream.json +1 -1
- package/plugins/acryl-inspect/tests/report.spec.ts +0 -69
- package/plugins/acryl-inspect/tests/service.spec.ts +0 -121
- package/plugins/acryl-node-server/tests/adapter/app.ts +0 -76
- package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +0 -5
- package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +0 -1
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +0 -1
- package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +0 -34
- package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +0 -248
- package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +0 -457
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +0 -23
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +0 -27
- package/plugins/acryl-node-server/tests/adapter/headers.test.ts +0 -252
- package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +0 -190
- package/plugins/acryl-node-server/tests/adapter/listener.test.ts +0 -727
- package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +0 -189
- package/plugins/acryl-node-server/tests/adapter/request.test.ts +0 -1230
- package/plugins/acryl-node-server/tests/adapter/response.test.ts +0 -285
- package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +0 -849
- package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +0 -790
- package/plugins/acryl-node-server/tests/adapter/server.test.ts +0 -1312
- package/plugins/acryl-node-server/tests/adapter/setup.ts +0 -16
- package/plugins/acryl-node-server/tests/adapter/url.test.ts +0 -76
- package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +0 -23
- package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +0 -75
- package/plugins/acryl-node-server/tests/adapter/utils.test.ts +0 -235
- package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +0 -260
- package/plugins/acryl-node-server/tests/server.spec.ts +0 -141
- package/plugins/acryl-router/src/core/app.test.ts +0 -3833
- package/plugins/acryl-router/src/core/client/client.test.ts +0 -2313
- package/plugins/acryl-router/src/core/client/types.test.ts +0 -149
- package/plugins/acryl-router/src/core/client/utils.test.ts +0 -405
- package/plugins/acryl-router/src/core/compose.test.ts +0 -751
- package/plugins/acryl-router/src/core/context.test.ts +0 -629
- package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +0 -344
- package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +0 -55
- package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +0 -510
- package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +0 -506
- package/plugins/acryl-router/src/core/helper/css/index.test.tsx +0 -678
- package/plugins/acryl-router/src/core/helper/dev/index.test.ts +0 -181
- package/plugins/acryl-router/src/core/helper/factory/index.test.ts +0 -434
- package/plugins/acryl-router/src/core/helper/html/index.test.ts +0 -78
- package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +0 -356
- package/plugins/acryl-router/src/core/helper/route/index.test.ts +0 -259
- package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +0 -557
- package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +0 -128
- package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +0 -34
- package/plugins/acryl-router/src/core/helper/testing/index.test.ts +0 -42
- package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +0 -124
- package/plugins/acryl-router/src/core/http-exception.test.ts +0 -45
- package/plugins/acryl-router/src/core/jsx/base.test.tsx +0 -97
- package/plugins/acryl-router/src/core/jsx/children.test.ts +0 -52
- package/plugins/acryl-router/src/core/jsx/components.test.tsx +0 -936
- package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +0 -655
- package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +0 -128
- package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +0 -330
- package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +0 -184
- package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +0 -149
- package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +0 -172
- package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +0 -3681
- package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +0 -1344
- package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +0 -199
- package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +0 -859
- package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +0 -40
- package/plugins/acryl-router/src/core/jsx/index.test.tsx +0 -1744
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +0 -666
- package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +0 -85
- package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +0 -1281
- package/plugins/acryl-router/src/core/jsx/utils.test.ts +0 -366
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +0 -434
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +0 -1018
- package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +0 -273
- package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +0 -1211
- package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +0 -355
- package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +0 -535
- package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +0 -43
- package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +0 -532
- package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +0 -534
- package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +0 -21
- package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +0 -503
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +0 -217
- package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +0 -1135
- package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +0 -48
- package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +0 -802
- package/plugins/acryl-router/src/core/middleware/language/index.test.ts +0 -552
- package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +0 -204
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +0 -292
- package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +0 -221
- package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +0 -334
- package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +0 -37
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +0 -218
- package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +0 -168
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +0 -671
- package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +0 -450
- package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +0 -50
- package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +0 -70
- package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +0 -310
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +0 -381
- package/plugins/acryl-router/src/core/preset/quick.test.ts +0 -42
- package/plugins/acryl-router/src/core/preset/tiny.test.ts +0 -42
- package/plugins/acryl-router/src/core/request.test.ts +0 -751
- package/plugins/acryl-router/src/core/router/common.case.test.ts +0 -1132
- package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +0 -80
- package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +0 -46
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +0 -154
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +0 -265
- package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +0 -13
- package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +0 -896
- package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +0 -8
- package/plugins/acryl-router/src/core/types.test.ts +0 -4134
- package/plugins/acryl-router/src/core/utils/accept.test.ts +0 -299
- package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +0 -56
- package/plugins/acryl-router/src/core/utils/body.test.ts +0 -496
- package/plugins/acryl-router/src/core/utils/buffer.test.ts +0 -147
- package/plugins/acryl-router/src/core/utils/color.test.ts +0 -23
- package/plugins/acryl-router/src/core/utils/concurrent.test.ts +0 -69
- package/plugins/acryl-router/src/core/utils/cookie.test.ts +0 -460
- package/plugins/acryl-router/src/core/utils/crypto.test.ts +0 -43
- package/plugins/acryl-router/src/core/utils/encode.test.ts +0 -61
- package/plugins/acryl-router/src/core/utils/filepath.test.ts +0 -78
- package/plugins/acryl-router/src/core/utils/html.test.ts +0 -16
- package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +0 -149
- package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +0 -86
- package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +0 -1788
- package/plugins/acryl-router/src/core/utils/mime.test.ts +0 -64
- package/plugins/acryl-router/src/core/utils/stream.test.ts +0 -212
- package/plugins/acryl-router/src/core/utils/types.test.ts +0 -295
- package/plugins/acryl-router/src/core/utils/url.test.ts +0 -384
- package/plugins/acryl-router/src/core/validator/utils.test.ts +0 -82
- package/plugins/acryl-router/src/core/validator/validator.test.ts +0 -1488
- package/plugins/acryl-router/tests/docs-examples.spec.ts +0 -152
- package/plugins/acryl-router/tests/jsx.spec.tsx +0 -63
- package/plugins/acryl-router/tests/live-install.spec.ts +0 -64
- package/plugins/acryl-router/tests/loader.spec.ts +0 -72
- package/plugins/acryl-router/tests/loopback-only.spec.ts +0 -80
- package/plugins/acryl-router/tests/no-dom/consumer.ts +0 -11
- package/plugins/acryl-router/tests/no-dom/tsconfig.json +0 -13
- package/plugins/acryl-router/tests/service.spec.ts +0 -426
- package/plugins/acryl-router/tests/shadow-state.spec.ts +0 -79
- package/plugins/acryl-router-http/tests/adapter.spec.ts +0 -174
- package/plugins/acryl-router-http/tests/stream.spec.ts +0 -133
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +0 -101
- package/plugins/acryl-state/src/react.ts +0 -11
- package/plugins/acryl-state/tests/cell.spec.ts +0 -71
- package/plugins/acryl-state/tests/react.spec.ts +0 -39
- package/plugins/acryl-state/tests/service.spec.ts +0 -132
|
@@ -1,3681 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource ../ */
|
|
2
|
-
import { JSDOM } from 'jsdom'
|
|
3
|
-
import DefaultExport, {
|
|
4
|
-
cloneElement,
|
|
5
|
-
cloneElement as cloneElementForDom,
|
|
6
|
-
createElement as createElementForDom,
|
|
7
|
-
createContext,
|
|
8
|
-
useContext,
|
|
9
|
-
createPortal,
|
|
10
|
-
flushSync,
|
|
11
|
-
isValidElement,
|
|
12
|
-
memo,
|
|
13
|
-
render,
|
|
14
|
-
version,
|
|
15
|
-
} from './index.ts'
|
|
16
|
-
import type { Child, FC } from '../index.ts'
|
|
17
|
-
// run tests by old style jsx default
|
|
18
|
-
// acryl-router/jsx/jsx-runtime and acryl-router/jsx/dom/jsx-runtime are tested in their respective settings
|
|
19
|
-
import { createElement, jsx } from '../index.ts'
|
|
20
|
-
import type { RefObject } from '../hooks/index.ts'
|
|
21
|
-
import {
|
|
22
|
-
createRef,
|
|
23
|
-
useCallback,
|
|
24
|
-
useEffect,
|
|
25
|
-
useInsertionEffect,
|
|
26
|
-
useLayoutEffect,
|
|
27
|
-
useMemo,
|
|
28
|
-
useRef,
|
|
29
|
-
useState,
|
|
30
|
-
} from '../hooks/index.ts'
|
|
31
|
-
import type { NodeObject } from './render.ts'
|
|
32
|
-
import { build, buildNode } from './render.ts'
|
|
33
|
-
|
|
34
|
-
describe('Common', () => {
|
|
35
|
-
;[createElement, createElementForDom].forEach((createElement) => {
|
|
36
|
-
describe('createElement', () => {
|
|
37
|
-
it('simple', () => {
|
|
38
|
-
const element = createElement('div', { id: 'app' })
|
|
39
|
-
expect(element).toEqual(expect.objectContaining({ tag: 'div', props: { id: 'app' } }))
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('children', () => {
|
|
43
|
-
const element = createElement('div', { id: 'app' }, 'Hello')
|
|
44
|
-
expect(element).toEqual(
|
|
45
|
-
expect.objectContaining({ tag: 'div', props: { id: 'app', children: 'Hello' } })
|
|
46
|
-
)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('multiple children', () => {
|
|
50
|
-
const element = createElement('div', { id: 'app' }, 'Hello', 'World')
|
|
51
|
-
expect(element).toEqual(
|
|
52
|
-
expect.objectContaining({
|
|
53
|
-
tag: 'div',
|
|
54
|
-
props: { id: 'app', children: ['Hello', 'World'] },
|
|
55
|
-
})
|
|
56
|
-
)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('key', () => {
|
|
60
|
-
const element = createElement('div', { id: 'app', key: 'key' })
|
|
61
|
-
expect(element).toEqual(
|
|
62
|
-
expect.objectContaining({ tag: 'div', props: { id: 'app' }, key: 'key' })
|
|
63
|
-
)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('ref', () => {
|
|
67
|
-
const ref = { current: null }
|
|
68
|
-
const element = createElement('div', { id: 'app', ref })
|
|
69
|
-
expect(element).toEqual(expect.objectContaining({ tag: 'div', props: { id: 'app', ref } }))
|
|
70
|
-
expect(element.ref).toBe(ref)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('type', () => {
|
|
74
|
-
const element = createElement('div', { id: 'app' })
|
|
75
|
-
expect(element.type).toBe('div')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('null props', () => {
|
|
79
|
-
const element = createElement('div', null)
|
|
80
|
-
expect(element).toEqual(expect.objectContaining({ tag: 'div', props: {} }))
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
describe('DOM', () => {
|
|
87
|
-
beforeAll(() => {
|
|
88
|
-
global.requestAnimationFrame = (cb) => setTimeout(cb)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
let dom: JSDOM
|
|
92
|
-
let root: HTMLElement
|
|
93
|
-
beforeEach(() => {
|
|
94
|
-
dom = new JSDOM('<html><body><div id="root"></div></body></html>', {
|
|
95
|
-
runScripts: 'dangerously',
|
|
96
|
-
})
|
|
97
|
-
global.document = dom.window.document
|
|
98
|
-
global.HTMLElement = dom.window.HTMLElement
|
|
99
|
-
global.SVGElement = dom.window.SVGElement
|
|
100
|
-
global.Text = dom.window.Text
|
|
101
|
-
global.DOMException = dom.window.DOMException
|
|
102
|
-
root = document.getElementById('root') as HTMLElement
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('simple App', () => {
|
|
106
|
-
const App = <h1>Hello</h1>
|
|
107
|
-
render(App, root)
|
|
108
|
-
expect(root.innerHTML).toBe('<h1>Hello</h1>')
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('replace', () => {
|
|
112
|
-
dom.window.document.body.innerHTML = '<div id="root">Existing content</div>'
|
|
113
|
-
root = document.getElementById('root') as HTMLElement
|
|
114
|
-
const App = <h1>Hello</h1>
|
|
115
|
-
render(App, root)
|
|
116
|
-
expect(root.innerHTML).toBe('<h1>Hello</h1>')
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('render text directly', () => {
|
|
120
|
-
const App = () => <>{'Hello'}</>
|
|
121
|
-
render(<App />, root)
|
|
122
|
-
expect(root.innerHTML).toBe('Hello')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
describe('performance', () => {
|
|
126
|
-
it('should be O(N) for each additional element', () => {
|
|
127
|
-
const App = () => (
|
|
128
|
-
<>
|
|
129
|
-
{Array.from({ length: 1000 }, (_, i) => (
|
|
130
|
-
<div>
|
|
131
|
-
<span>{i}</span>
|
|
132
|
-
</div>
|
|
133
|
-
))}
|
|
134
|
-
</>
|
|
135
|
-
)
|
|
136
|
-
render(<App />, root)
|
|
137
|
-
expect(root.innerHTML).toBe(
|
|
138
|
-
Array.from({ length: 1000 }, (_, i) => `<div><span>${i}</span></div>`).join('')
|
|
139
|
-
)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it.each(['same-order', 'remove-second', 'adjacent-swap'])(
|
|
143
|
-
'preserves keyed children during common updates: %s',
|
|
144
|
-
(kind) => {
|
|
145
|
-
const size = 100
|
|
146
|
-
const ids = Array.from({ length: size }, (_, i) => i)
|
|
147
|
-
const createRow = (id: number) => <div key={id}>{id}</div>
|
|
148
|
-
const parent = buildNode(<section>{ids.map(createRow)}</section>) as NodeObject
|
|
149
|
-
build([], parent)
|
|
150
|
-
const initialChildren = parent.vC
|
|
151
|
-
const nextIds = [...ids]
|
|
152
|
-
if (kind === 'remove-second') {
|
|
153
|
-
nextIds.splice(1, 1)
|
|
154
|
-
} else if (kind === 'adjacent-swap') {
|
|
155
|
-
;[nextIds[1], nextIds[2]] = [nextIds[2], nextIds[1]]
|
|
156
|
-
}
|
|
157
|
-
parent.props.children = [[], ...nextIds.map(createRow)]
|
|
158
|
-
|
|
159
|
-
build([], parent)
|
|
160
|
-
|
|
161
|
-
expect(parent.vC).toHaveLength(nextIds.length)
|
|
162
|
-
nextIds.forEach((id, i) => expect(parent.vC[i]).toBe(initialChildren[id]))
|
|
163
|
-
expect(parent.vR).toHaveLength(kind === 'remove-second' ? 1 : 0)
|
|
164
|
-
if (kind === 'remove-second') {
|
|
165
|
-
expect(parent.vR[0]).toBe(initialChildren[1])
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
describe('large keyed updates', () => {
|
|
172
|
-
it.each([
|
|
173
|
-
'reverse',
|
|
174
|
-
'rotate-half',
|
|
175
|
-
'replace-all',
|
|
176
|
-
'footer-unkeyed',
|
|
177
|
-
'footer-text',
|
|
178
|
-
'insert-text',
|
|
179
|
-
'move-text',
|
|
180
|
-
])('matches keyed list updates in linear work: %s', (kind) => {
|
|
181
|
-
const measure = (size: number) => {
|
|
182
|
-
let oldKeyReads = 0
|
|
183
|
-
let newKeyReads = 0
|
|
184
|
-
const createRow = (id: number, old = false) => {
|
|
185
|
-
const row = <div>{id}</div>
|
|
186
|
-
// Count both sides so node normalization cannot silently disable the check.
|
|
187
|
-
Object.defineProperty(row, 'key', {
|
|
188
|
-
configurable: true,
|
|
189
|
-
get: () => {
|
|
190
|
-
if (old) {
|
|
191
|
-
oldKeyReads++
|
|
192
|
-
} else {
|
|
193
|
-
newKeyReads++
|
|
194
|
-
}
|
|
195
|
-
return id
|
|
196
|
-
},
|
|
197
|
-
})
|
|
198
|
-
return row
|
|
199
|
-
}
|
|
200
|
-
const ids = Array.from({ length: size }, (_, i) => i)
|
|
201
|
-
const before: Child[] = ids.map((id) => createRow(id, true))
|
|
202
|
-
if (kind === 'footer-unkeyed') {
|
|
203
|
-
before.push(<div>footer</div>)
|
|
204
|
-
} else if (kind === 'footer-text' || kind === 'move-text') {
|
|
205
|
-
before.push('original')
|
|
206
|
-
}
|
|
207
|
-
const parent = buildNode(<section>{before}</section>) as NodeObject
|
|
208
|
-
build([], parent)
|
|
209
|
-
const initial = parent.vC
|
|
210
|
-
const next = [...ids]
|
|
211
|
-
if (kind === 'rotate-half') {
|
|
212
|
-
next.push(...next.splice(0, size / 2))
|
|
213
|
-
} else if (kind === 'replace-all') {
|
|
214
|
-
next.splice(0, size, ...ids.map((id) => id + size))
|
|
215
|
-
} else {
|
|
216
|
-
next.reverse()
|
|
217
|
-
}
|
|
218
|
-
const after: Child[] = next.map((id) => createRow(id))
|
|
219
|
-
const expected = next.map((id) => (id < size ? initial[id] : undefined))
|
|
220
|
-
if (kind === 'footer-unkeyed' || kind === 'footer-text') {
|
|
221
|
-
after.push(kind === 'footer-unkeyed' ? <div>footer</div> : 'updated')
|
|
222
|
-
expected.push(initial[size])
|
|
223
|
-
} else if (kind === 'insert-text' || kind === 'move-text') {
|
|
224
|
-
after.splice(4, 0, 'updated')
|
|
225
|
-
expected.splice(4, 0, kind === 'move-text' ? initial[size] : undefined)
|
|
226
|
-
if (kind === 'move-text') {
|
|
227
|
-
after.splice(8, 0, 'extra')
|
|
228
|
-
expected.splice(8, 0, undefined)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
parent.props.children = after
|
|
232
|
-
oldKeyReads = newKeyReads = 0
|
|
233
|
-
build([], parent)
|
|
234
|
-
const reads = { old: oldKeyReads, new: newKeyReads }
|
|
235
|
-
|
|
236
|
-
expect(parent.vC).toHaveLength(expected.length)
|
|
237
|
-
expected.forEach((child, i) => {
|
|
238
|
-
if (child) {
|
|
239
|
-
expect(parent.vC[i]).toBe(child)
|
|
240
|
-
} else {
|
|
241
|
-
expect(initial.includes(parent.vC[i])).toBe(false)
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
if (kind === 'footer-text' || kind === 'insert-text' || kind === 'move-text') {
|
|
245
|
-
expect(parent.vC[kind === 'footer-text' ? size : 4]).toMatchObject({ t: 'updated' })
|
|
246
|
-
}
|
|
247
|
-
if (kind === 'move-text') {
|
|
248
|
-
expect(parent.vC[8]).toMatchObject({ t: 'extra' })
|
|
249
|
-
}
|
|
250
|
-
expect(parent.vR).toHaveLength(kind === 'replace-all' ? size : 0)
|
|
251
|
-
parent.vR.forEach((child, i) => expect(child).toBe(initial[i]))
|
|
252
|
-
return reads
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const small = measure(64)
|
|
256
|
-
const large = measure(128)
|
|
257
|
-
expect(small.old).toBeGreaterThan(0)
|
|
258
|
-
expect(small.new).toBeGreaterThan(0)
|
|
259
|
-
expect(large.old).toBeLessThan(small.old * 3)
|
|
260
|
-
expect(large.old + large.new).toBeLessThan((small.old + small.new) * 3)
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it.each(['unkeyed-to-text', 'undefined-tag-to-text', 'text-to-unkeyed'])(
|
|
264
|
-
'replaces incompatible children after indexing: %s',
|
|
265
|
-
(kind) => {
|
|
266
|
-
const ids = Array.from({ length: 40 }, (_, i) => i)
|
|
267
|
-
const createRow = (id: number) => <div key={id}>{id}</div>
|
|
268
|
-
let footer: Child = kind === 'text-to-unkeyed' ? 'original' : <span>footer</span>
|
|
269
|
-
if (kind === 'undefined-tag-to-text') {
|
|
270
|
-
// @ts-expect-error An undefined component still creates a NodeObject at runtime.
|
|
271
|
-
footer = createElementForDom(undefined, {})
|
|
272
|
-
}
|
|
273
|
-
const parent = buildNode(<section>{[...ids.map(createRow), footer]}</section>) as NodeObject
|
|
274
|
-
build([], parent)
|
|
275
|
-
const initial = parent.vC
|
|
276
|
-
const next = [...ids].reverse()
|
|
277
|
-
const after: Child[] = next.map(createRow)
|
|
278
|
-
// Consume an indexed child before replacing the incompatible footer.
|
|
279
|
-
after.splice(4, 0, kind === 'text-to-unkeyed' ? <span>updated</span> : 'updated')
|
|
280
|
-
parent.props.children = after
|
|
281
|
-
|
|
282
|
-
build([], parent)
|
|
283
|
-
|
|
284
|
-
expect(parent.vC).toHaveLength(ids.length + 1)
|
|
285
|
-
next.forEach((id, i) => expect(parent.vC[i < 4 ? i : i + 1]).toBe(initial[id]))
|
|
286
|
-
expect(initial.includes(parent.vC[4])).toBe(false)
|
|
287
|
-
expect(parent.vC[4]).toMatchObject(
|
|
288
|
-
kind === 'text-to-unkeyed' ? { tag: 'span' } : { t: 'updated', d: true }
|
|
289
|
-
)
|
|
290
|
-
expect(parent.vR).toHaveLength(1)
|
|
291
|
-
expect(parent.vR[0]).toBe(initial[ids.length])
|
|
292
|
-
}
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
it('restores previous children with indexed matching and ordered removals', () => {
|
|
296
|
-
const ids = Array.from({ length: 40 }, (_, i) => i)
|
|
297
|
-
const createRow = (id: number) => <div key={id}>{id}</div>
|
|
298
|
-
const parent = buildNode(<section>{ids.map(createRow)}</section>) as NodeObject
|
|
299
|
-
build([], parent)
|
|
300
|
-
const initial = parent.vC
|
|
301
|
-
let keyReads = 0
|
|
302
|
-
initial.forEach((child, id) => {
|
|
303
|
-
Object.defineProperty(child, 'key', {
|
|
304
|
-
configurable: true,
|
|
305
|
-
get: () => {
|
|
306
|
-
keyReads++
|
|
307
|
-
return id
|
|
308
|
-
},
|
|
309
|
-
})
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
// Explicit fallback children preserve the original list in pC.
|
|
313
|
-
build([], parent, [<span>Loading</span>])
|
|
314
|
-
const fallback = parent.vC
|
|
315
|
-
expect(parent.pC).toBe(initial)
|
|
316
|
-
const next = [...ids].reverse().filter((id) => id !== 10 && id !== 20)
|
|
317
|
-
parent.props.children = next.map(createRow)
|
|
318
|
-
keyReads = 0
|
|
319
|
-
build([], parent)
|
|
320
|
-
const matchingReads = keyReads
|
|
321
|
-
|
|
322
|
-
expect(matchingReads).toBeGreaterThan(0)
|
|
323
|
-
expect(matchingReads).toBeLessThan(ids.length * 8)
|
|
324
|
-
expect(parent.vC).toHaveLength(next.length)
|
|
325
|
-
next.forEach((id, i) => expect(parent.vC[i]).toBe(initial[id]))
|
|
326
|
-
const removed = [...fallback, initial[10], initial[20]]
|
|
327
|
-
expect(parent.vR).toHaveLength(removed.length)
|
|
328
|
-
removed.forEach((child, i) => expect(parent.vR[i]).toBe(child))
|
|
329
|
-
expect(parent.pC).toBeUndefined()
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
it.each([
|
|
333
|
-
{ name: 'duplicate old keys', oldKeys: [0, 0], newKeys: [0, 0], matches: [0, 1] },
|
|
334
|
-
{ name: 'an unkeyed old child', oldKeys: [undefined, 1], newKeys: [undefined], matches: [0] },
|
|
335
|
-
{
|
|
336
|
-
name: 'multiple unkeyed old children',
|
|
337
|
-
oldKeys: [undefined, undefined],
|
|
338
|
-
newKeys: [undefined, undefined],
|
|
339
|
-
matches: [0, 1],
|
|
340
|
-
},
|
|
341
|
-
{ name: 'NaN keys', oldKeys: [NaN, 1], newKeys: [NaN, 1], matches: [-1, 1] },
|
|
342
|
-
{
|
|
343
|
-
name: 'a different tag for the same key',
|
|
344
|
-
oldKeys: [0, 1],
|
|
345
|
-
newKeys: [0, 0],
|
|
346
|
-
firstNewTag: 'span',
|
|
347
|
-
matches: [-1, 0],
|
|
348
|
-
},
|
|
349
|
-
{ name: 'a repeated new key', oldKeys: [0, 1], newKeys: [0, 0], matches: [0, -1] },
|
|
350
|
-
{
|
|
351
|
-
name: 'different old tags sharing a key',
|
|
352
|
-
oldKeys: [0, 0],
|
|
353
|
-
oldSecondTag: 'span',
|
|
354
|
-
newKeys: [0, 0],
|
|
355
|
-
firstNewTag: 'span',
|
|
356
|
-
matches: [1, 0],
|
|
357
|
-
},
|
|
358
|
-
])(
|
|
359
|
-
'preserves matching and removal order with $name',
|
|
360
|
-
({ oldKeys, newKeys, matches, oldSecondTag, firstNewTag }) => {
|
|
361
|
-
const before = Array.from({ length: 40 }, (_, i) =>
|
|
362
|
-
createElement(i === 1 ? oldSecondTag || 'div' : 'div', {
|
|
363
|
-
key: i < oldKeys.length ? oldKeys[i] : i,
|
|
364
|
-
})
|
|
365
|
-
)
|
|
366
|
-
const parent = buildNode(<section>{before}</section>) as NodeObject
|
|
367
|
-
build([], parent)
|
|
368
|
-
const initial = parent.vC
|
|
369
|
-
// This prefix exceeds the scan budget and attempts to index the remaining nodes.
|
|
370
|
-
const prefix = [39, 38, 37, 36]
|
|
371
|
-
parent.props.children = [
|
|
372
|
-
...prefix.map((key) => <div key={key} />),
|
|
373
|
-
...newKeys.map((key, i) =>
|
|
374
|
-
createElement(i === 0 ? firstNewTag || 'div' : 'div', { key })
|
|
375
|
-
),
|
|
376
|
-
]
|
|
377
|
-
build([], parent)
|
|
378
|
-
|
|
379
|
-
const expected = [...prefix, ...matches]
|
|
380
|
-
expect(parent.vC).toHaveLength(expected.length)
|
|
381
|
-
expected.forEach((oldIndex, i) => {
|
|
382
|
-
if (oldIndex === -1) {
|
|
383
|
-
expect(initial.includes(parent.vC[i])).toBe(false)
|
|
384
|
-
} else {
|
|
385
|
-
expect(parent.vC[i]).toBe(initial[oldIndex])
|
|
386
|
-
}
|
|
387
|
-
})
|
|
388
|
-
const removed = initial.filter((_, i) => !expected.includes(i))
|
|
389
|
-
expect(parent.vR).toHaveLength(removed.length)
|
|
390
|
-
removed.forEach((child, i) => expect(parent.vR[i]).toBe(child))
|
|
391
|
-
}
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
it.each(['keyed', 'unkeyed', 'text'])(
|
|
395
|
-
'preserves state and cleanup order after indexing followed by %s children',
|
|
396
|
-
async (kind) => {
|
|
397
|
-
let update: () => void = () => {}
|
|
398
|
-
const cleaned: number[] = []
|
|
399
|
-
const Row = ({ id }: { id: number }) => {
|
|
400
|
-
const [initialId] = useState(id)
|
|
401
|
-
const [count, setCount] = useState(0)
|
|
402
|
-
useLayoutEffect(
|
|
403
|
-
() => () => {
|
|
404
|
-
cleaned.push(initialId)
|
|
405
|
-
},
|
|
406
|
-
[]
|
|
407
|
-
)
|
|
408
|
-
return (
|
|
409
|
-
<button onClick={() => setCount(count + 1)}>
|
|
410
|
-
{id}:{initialId}:{count}
|
|
411
|
-
</button>
|
|
412
|
-
)
|
|
413
|
-
}
|
|
414
|
-
const before = Array.from({ length: 40 }, (_, i) => i)
|
|
415
|
-
const after = [...before]
|
|
416
|
-
.reverse()
|
|
417
|
-
.filter((id) => id !== 10 && id !== 20 && (kind !== 'unkeyed' || id !== 0))
|
|
418
|
-
// Consume an indexed child before introducing an unkeyed or text child.
|
|
419
|
-
if (kind !== 'keyed') {
|
|
420
|
-
after.splice(4, 0, kind === 'unkeyed' ? 100 : 101)
|
|
421
|
-
}
|
|
422
|
-
const App = () => {
|
|
423
|
-
const [ids, setIds] = useState(before)
|
|
424
|
-
update = () => setIds(after)
|
|
425
|
-
return ids.map((id) =>
|
|
426
|
-
id === 101 ? 'inserted' : <Row key={id === 100 ? undefined : id} id={id} />
|
|
427
|
-
)
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
render(<App />, root)
|
|
431
|
-
const initial = [...root.querySelectorAll('button')]
|
|
432
|
-
initial[0].click()
|
|
433
|
-
await Promise.resolve()
|
|
434
|
-
update()
|
|
435
|
-
await Promise.resolve()
|
|
436
|
-
|
|
437
|
-
const rows = after.filter((id) => id !== 101)
|
|
438
|
-
expect(root.children).toHaveLength(rows.length)
|
|
439
|
-
rows.forEach((id, i) => {
|
|
440
|
-
const oldId = id === 100 ? 0 : id
|
|
441
|
-
expect(root.children[i]).toBe(initial[oldId])
|
|
442
|
-
expect(root.children[i].textContent).toBe(`${id}:${oldId}:${oldId === 0 ? 1 : 0}`)
|
|
443
|
-
})
|
|
444
|
-
if (kind === 'text') {
|
|
445
|
-
expect(root.childNodes[4].textContent).toBe('inserted')
|
|
446
|
-
}
|
|
447
|
-
expect(cleaned).toEqual([10, 20])
|
|
448
|
-
}
|
|
449
|
-
)
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
describe('attribute', () => {
|
|
453
|
-
it('simple', () => {
|
|
454
|
-
const App = () => <div id='app' class='app' />
|
|
455
|
-
render(<App />, root)
|
|
456
|
-
expect(root.innerHTML).toBe('<div id="app" class="app"></div>')
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
it('boolean', () => {
|
|
460
|
-
const App = () => <div hidden />
|
|
461
|
-
render(<App />, root)
|
|
462
|
-
expect(root.innerHTML).toBe('<div hidden=""></div>')
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
it('style', () => {
|
|
466
|
-
const App = () => <div style={{ fontSize: '10px' }} />
|
|
467
|
-
render(<App />, root)
|
|
468
|
-
expect(root.innerHTML).toBe('<div style="font-size: 10px;"></div>')
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
it('update style', () => {
|
|
472
|
-
const App = () => <div style={{ fontSize: '10px' }} />
|
|
473
|
-
render(<App />, root)
|
|
474
|
-
expect(root.innerHTML).toBe('<div style="font-size: 10px;"></div>')
|
|
475
|
-
})
|
|
476
|
-
|
|
477
|
-
it('style with CSS variables - 1', () => {
|
|
478
|
-
const App = () => <div style={{ '--my-var-1': '15px' }} />
|
|
479
|
-
render(<App />, root)
|
|
480
|
-
expect(root.innerHTML).toBe('<div style="--my-var-1: 15px;"></div>')
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
it('style with CSS variables - 2', () => {
|
|
484
|
-
const App = () => <div style={{ '--myVar-2': '20px' }} />
|
|
485
|
-
render(<App />, root)
|
|
486
|
-
expect(root.innerHTML).toBe('<div style="--myVar-2: 20px;"></div>')
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
it('style with string', async () => {
|
|
490
|
-
const App = () => {
|
|
491
|
-
const [style, setStyle] = useState<{ fontSize?: string; color?: string }>({
|
|
492
|
-
fontSize: '10px',
|
|
493
|
-
})
|
|
494
|
-
return <div style={style} onClick={() => setStyle({ color: 'red' })} />
|
|
495
|
-
}
|
|
496
|
-
render(<App />, root)
|
|
497
|
-
expect(root.innerHTML).toBe('<div style="font-size: 10px;"></div>')
|
|
498
|
-
root.querySelector('div')?.click()
|
|
499
|
-
await Promise.resolve()
|
|
500
|
-
expect(root.innerHTML).toBe('<div style="color: red;"></div>')
|
|
501
|
-
})
|
|
502
|
-
|
|
503
|
-
it('toString() is called', () => {
|
|
504
|
-
const App = () => <div x-value={{ toString: () => 'value' }} />
|
|
505
|
-
render(<App />, root)
|
|
506
|
-
expect(root.innerHTML).toBe('<div x-value="value"></div>')
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
it('ignores invalid attribute keys without interrupting updates', async () => {
|
|
510
|
-
const invalidKey = '" onfocus="alert(1)'
|
|
511
|
-
|
|
512
|
-
const App = () => {
|
|
513
|
-
const [includeInvalid, setIncludeInvalid] = useState(true)
|
|
514
|
-
return includeInvalid ? (
|
|
515
|
-
<div id='safe' {...{ [invalidKey]: 'x' }} onClick={() => setIncludeInvalid(false)}>
|
|
516
|
-
Hello
|
|
517
|
-
</div>
|
|
518
|
-
) : (
|
|
519
|
-
<div class='updated'>Hello</div>
|
|
520
|
-
)
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
render(<App />, root)
|
|
524
|
-
expect(root.innerHTML).toBe('<div id="safe">Hello</div>')
|
|
525
|
-
|
|
526
|
-
root.querySelector('div')?.click()
|
|
527
|
-
await Promise.resolve()
|
|
528
|
-
|
|
529
|
-
expect(root.innerHTML).toBe('<div class="updated">Hello</div>')
|
|
530
|
-
})
|
|
531
|
-
|
|
532
|
-
it('rethrows unexpected errors while setting attributes', () => {
|
|
533
|
-
const error = new Error('boom')
|
|
534
|
-
const originalSetAttribute = dom.window.Element.prototype.setAttribute
|
|
535
|
-
const setAttributeSpy = vi
|
|
536
|
-
.spyOn(dom.window.Element.prototype, 'setAttribute')
|
|
537
|
-
.mockImplementation(function (this: Element, key: string, value: string) {
|
|
538
|
-
if (key === 'data-boom') {
|
|
539
|
-
throw error
|
|
540
|
-
}
|
|
541
|
-
return originalSetAttribute.call(this, key, value)
|
|
542
|
-
})
|
|
543
|
-
|
|
544
|
-
try {
|
|
545
|
-
expect(() => render(<div data-boom='x'>Hello</div>, root)).toThrow(error)
|
|
546
|
-
} finally {
|
|
547
|
-
setAttributeSpy.mockRestore()
|
|
548
|
-
}
|
|
549
|
-
})
|
|
550
|
-
|
|
551
|
-
it('rethrows unexpected errors while removing attributes', () => {
|
|
552
|
-
render(<div data-boom='x'>Hello</div>, root)
|
|
553
|
-
|
|
554
|
-
const error = new Error('boom')
|
|
555
|
-
const originalRemoveAttribute = dom.window.Element.prototype.removeAttribute
|
|
556
|
-
const removeAttributeSpy = vi
|
|
557
|
-
.spyOn(dom.window.Element.prototype, 'removeAttribute')
|
|
558
|
-
.mockImplementation(function (this: Element, key: string) {
|
|
559
|
-
if (key === 'data-boom') {
|
|
560
|
-
throw error
|
|
561
|
-
}
|
|
562
|
-
return originalRemoveAttribute.call(this, key)
|
|
563
|
-
})
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
expect(() => render(<div data-boom={undefined}>Hello</div>, root)).toThrow(error)
|
|
567
|
-
} finally {
|
|
568
|
-
removeAttributeSpy.mockRestore()
|
|
569
|
-
}
|
|
570
|
-
})
|
|
571
|
-
|
|
572
|
-
it('ref', () => {
|
|
573
|
-
const App = () => {
|
|
574
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
575
|
-
return <div ref={ref} />
|
|
576
|
-
}
|
|
577
|
-
render(<App />, root)
|
|
578
|
-
expect(root.innerHTML).toBe('<div></div>')
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
it('ref with callback', () => {
|
|
582
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
583
|
-
const App = () => {
|
|
584
|
-
return <div ref={(node: HTMLDivElement) => (ref.current = node)} />
|
|
585
|
-
}
|
|
586
|
-
render(<App />, root)
|
|
587
|
-
expect(root.innerHTML).toBe('<div></div>')
|
|
588
|
-
expect(ref.current).toBeInstanceOf(HTMLElement)
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
it('ref with null', () => {
|
|
592
|
-
const App = () => {
|
|
593
|
-
return <div ref={null} />
|
|
594
|
-
}
|
|
595
|
-
render(<App />, root)
|
|
596
|
-
expect(root.innerHTML).toBe('<div></div>')
|
|
597
|
-
})
|
|
598
|
-
|
|
599
|
-
it('remove node with ref object', async () => {
|
|
600
|
-
const ref = createRef<HTMLDivElement>()
|
|
601
|
-
const App = () => {
|
|
602
|
-
const [show, setShow] = useState(true)
|
|
603
|
-
return (
|
|
604
|
-
<>
|
|
605
|
-
{show && <div ref={ref} />}
|
|
606
|
-
<button onClick={() => setShow(false)}>remove</button>
|
|
607
|
-
</>
|
|
608
|
-
)
|
|
609
|
-
}
|
|
610
|
-
render(<App />, root)
|
|
611
|
-
expect(root.innerHTML).toBe('<div></div><button>remove</button>')
|
|
612
|
-
expect(ref.current).toBeInstanceOf(dom.window.HTMLDivElement)
|
|
613
|
-
root.querySelector('button')?.click()
|
|
614
|
-
await Promise.resolve()
|
|
615
|
-
expect(root.innerHTML).toBe('<button>remove</button>')
|
|
616
|
-
expect(ref.current).toBe(null)
|
|
617
|
-
})
|
|
618
|
-
|
|
619
|
-
it('remove node with ref function', async () => {
|
|
620
|
-
const ref = vi.fn()
|
|
621
|
-
const App = () => {
|
|
622
|
-
const [show, setShow] = useState(true)
|
|
623
|
-
return (
|
|
624
|
-
<>
|
|
625
|
-
{show && <div ref={ref} />}
|
|
626
|
-
<button onClick={() => setShow(false)}>remove</button>
|
|
627
|
-
</>
|
|
628
|
-
)
|
|
629
|
-
}
|
|
630
|
-
render(<App />, root)
|
|
631
|
-
expect(root.innerHTML).toBe('<div></div><button>remove</button>')
|
|
632
|
-
expect(ref).toHaveBeenLastCalledWith(expect.any(dom.window.HTMLDivElement))
|
|
633
|
-
root.querySelector('button')?.click()
|
|
634
|
-
await Promise.resolve()
|
|
635
|
-
expect(root.innerHTML).toBe('<button>remove</button>')
|
|
636
|
-
expect(ref).toHaveBeenLastCalledWith(null)
|
|
637
|
-
})
|
|
638
|
-
|
|
639
|
-
it('ref cleanup function', async () => {
|
|
640
|
-
const cleanup = vi.fn()
|
|
641
|
-
const ref = vi.fn().mockReturnValue(cleanup)
|
|
642
|
-
const App = () => {
|
|
643
|
-
const [show, setShow] = useState(true)
|
|
644
|
-
return (
|
|
645
|
-
<>
|
|
646
|
-
{show && <div ref={ref} />}
|
|
647
|
-
<button onClick={() => setShow(false)}>remove</button>
|
|
648
|
-
</>
|
|
649
|
-
)
|
|
650
|
-
}
|
|
651
|
-
render(<App />, root)
|
|
652
|
-
expect(root.innerHTML).toBe('<div></div><button>remove</button>')
|
|
653
|
-
expect(ref).toHaveBeenLastCalledWith(expect.any(dom.window.HTMLDivElement))
|
|
654
|
-
root.querySelector('button')?.click()
|
|
655
|
-
await Promise.resolve()
|
|
656
|
-
expect(root.innerHTML).toBe('<button>remove</button>')
|
|
657
|
-
expect(ref).toBeCalledTimes(1)
|
|
658
|
-
expect(cleanup).toBeCalledTimes(1)
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
it('ref cleanup on ref prop change', async () => {
|
|
662
|
-
const cleanup1 = vi.fn()
|
|
663
|
-
const ref1 = vi.fn().mockReturnValue(cleanup1)
|
|
664
|
-
const cleanup2 = vi.fn()
|
|
665
|
-
const ref2 = vi.fn().mockReturnValue(cleanup2)
|
|
666
|
-
|
|
667
|
-
const App = () => {
|
|
668
|
-
const [currentRef, setCurrentRef] = useState(() => ref1)
|
|
669
|
-
return (
|
|
670
|
-
<>
|
|
671
|
-
<div ref={currentRef} />
|
|
672
|
-
<button onClick={() => setCurrentRef(() => ref2)}>switch</button>
|
|
673
|
-
</>
|
|
674
|
-
)
|
|
675
|
-
}
|
|
676
|
-
render(<App />, root)
|
|
677
|
-
expect(ref1).toHaveBeenCalledTimes(1)
|
|
678
|
-
expect(ref1).toHaveBeenLastCalledWith(expect.any(dom.window.HTMLDivElement))
|
|
679
|
-
expect(cleanup1).toHaveBeenCalledTimes(0)
|
|
680
|
-
expect(ref2).toHaveBeenCalledTimes(0)
|
|
681
|
-
|
|
682
|
-
root.querySelector('button')?.click()
|
|
683
|
-
await Promise.resolve()
|
|
684
|
-
|
|
685
|
-
expect(cleanup1).toHaveBeenCalledTimes(1)
|
|
686
|
-
expect(ref2).toHaveBeenCalledTimes(1)
|
|
687
|
-
expect(ref2).toHaveBeenLastCalledWith(expect.any(dom.window.HTMLDivElement))
|
|
688
|
-
expect(cleanup2).toHaveBeenCalledTimes(0)
|
|
689
|
-
})
|
|
690
|
-
|
|
691
|
-
it('does not call ref cleanup when ref reference does not change on re-render', async () => {
|
|
692
|
-
const cleanup = vi.fn()
|
|
693
|
-
const ref = vi.fn().mockReturnValue(cleanup)
|
|
694
|
-
|
|
695
|
-
const App = () => {
|
|
696
|
-
const [count, setCount] = useState(0)
|
|
697
|
-
return (
|
|
698
|
-
<>
|
|
699
|
-
<div ref={ref} />
|
|
700
|
-
<button onClick={() => setCount(count + 1)}>rerender</button>
|
|
701
|
-
</>
|
|
702
|
-
)
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
render(<App />, root)
|
|
706
|
-
expect(ref).toHaveBeenCalledTimes(1)
|
|
707
|
-
expect(cleanup).toHaveBeenCalledTimes(0)
|
|
708
|
-
|
|
709
|
-
root.querySelector('button')?.click()
|
|
710
|
-
await Promise.resolve()
|
|
711
|
-
|
|
712
|
-
expect(ref).toHaveBeenCalledTimes(1)
|
|
713
|
-
expect(cleanup).toHaveBeenCalledTimes(0)
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
it('ref cleanup on ref prop removal', async () => {
|
|
717
|
-
const cleanup = vi.fn()
|
|
718
|
-
const ref = vi.fn().mockReturnValue(cleanup)
|
|
719
|
-
|
|
720
|
-
const App = () => {
|
|
721
|
-
const [hasRef, setHasRef] = useState(true)
|
|
722
|
-
return (
|
|
723
|
-
<>
|
|
724
|
-
<div {...(hasRef ? { ref } : {})} />
|
|
725
|
-
<button onClick={() => setHasRef(false)}>remove</button>
|
|
726
|
-
</>
|
|
727
|
-
)
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
render(<App />, root)
|
|
731
|
-
expect(ref).toHaveBeenCalledTimes(1)
|
|
732
|
-
expect(cleanup).toHaveBeenCalledTimes(0)
|
|
733
|
-
|
|
734
|
-
root.querySelector('button')?.click()
|
|
735
|
-
await Promise.resolve()
|
|
736
|
-
|
|
737
|
-
expect(cleanup).toHaveBeenCalledTimes(1)
|
|
738
|
-
})
|
|
739
|
-
})
|
|
740
|
-
|
|
741
|
-
describe('child component', () => {
|
|
742
|
-
it('simple', async () => {
|
|
743
|
-
const Child = vi.fn(({ count }: { count: number }) => <div>{count}</div>)
|
|
744
|
-
const App = () => {
|
|
745
|
-
const [count, setCount] = useState(0)
|
|
746
|
-
return (
|
|
747
|
-
<>
|
|
748
|
-
<div>{count}</div>
|
|
749
|
-
<Child count={Math.floor(count / 2)} />
|
|
750
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
751
|
-
</>
|
|
752
|
-
)
|
|
753
|
-
}
|
|
754
|
-
render(<App />, root)
|
|
755
|
-
expect(root.innerHTML).toBe('<div>0</div><div>0</div><button>+</button>')
|
|
756
|
-
expect(Child).toBeCalledTimes(1)
|
|
757
|
-
root.querySelector('button')?.click()
|
|
758
|
-
await Promise.resolve()
|
|
759
|
-
expect(root.innerHTML).toBe('<div>1</div><div>0</div><button>+</button>')
|
|
760
|
-
expect(Child).toBeCalledTimes(2)
|
|
761
|
-
root.querySelector('button')?.click()
|
|
762
|
-
await Promise.resolve()
|
|
763
|
-
expect(root.innerHTML).toBe('<div>2</div><div>1</div><button>+</button>')
|
|
764
|
-
expect(Child).toBeCalledTimes(3)
|
|
765
|
-
})
|
|
766
|
-
|
|
767
|
-
it('returning an array', async () => {
|
|
768
|
-
const App = () => {
|
|
769
|
-
const [count, setCount] = useState(0)
|
|
770
|
-
return [<div>{count}</div>, <button onClick={() => setCount(count + 1)}>+</button>]
|
|
771
|
-
}
|
|
772
|
-
render(<App />, root)
|
|
773
|
-
expect(root.innerHTML).toBe('<div>0</div><button>+</button>')
|
|
774
|
-
root.querySelector('button')?.click()
|
|
775
|
-
await Promise.resolve()
|
|
776
|
-
expect(root.innerHTML).toBe('<div>1</div><button>+</button>')
|
|
777
|
-
})
|
|
778
|
-
|
|
779
|
-
it('typed as FC returning an array', async () => {
|
|
780
|
-
const App: FC = () => {
|
|
781
|
-
const [count, setCount] = useState(0)
|
|
782
|
-
return [<div>{count}</div>, <button onClick={() => setCount(count + 1)}>+</button>]
|
|
783
|
-
}
|
|
784
|
-
render(<App />, root)
|
|
785
|
-
expect(root.innerHTML).toBe('<div>0</div><button>+</button>')
|
|
786
|
-
root.querySelector('button')?.click()
|
|
787
|
-
await Promise.resolve()
|
|
788
|
-
expect(root.innerHTML).toBe('<div>1</div><button>+</button>')
|
|
789
|
-
})
|
|
790
|
-
|
|
791
|
-
it('multiple children', async () => {
|
|
792
|
-
const Child = ({ name }: { name: string }) => {
|
|
793
|
-
const [count, setCount] = useState(0)
|
|
794
|
-
return (
|
|
795
|
-
<div>
|
|
796
|
-
<div>
|
|
797
|
-
{name} {count}
|
|
798
|
-
</div>
|
|
799
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
800
|
-
</div>
|
|
801
|
-
)
|
|
802
|
-
}
|
|
803
|
-
const App = () => {
|
|
804
|
-
const [count, setCount] = useState(0)
|
|
805
|
-
return (
|
|
806
|
-
<div>
|
|
807
|
-
<div>parent {count}</div>
|
|
808
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
809
|
-
<div>
|
|
810
|
-
<Child name='child 1' />
|
|
811
|
-
<Child name='child 2' />
|
|
812
|
-
<Child name='child 3' />
|
|
813
|
-
</div>
|
|
814
|
-
</div>
|
|
815
|
-
)
|
|
816
|
-
}
|
|
817
|
-
render(<App />, root)
|
|
818
|
-
expect(root.innerHTML).toBe(
|
|
819
|
-
'<div><div>parent 0</div><button>+</button><div><div><div>child 1 0</div><button>+</button></div><div><div>child 2 0</div><button>+</button></div><div><div>child 3 0</div><button>+</button></div></div></div>'
|
|
820
|
-
)
|
|
821
|
-
const [parentButton, child1Button, child2Button, child3Button] =
|
|
822
|
-
root.querySelectorAll('button')
|
|
823
|
-
parentButton?.click()
|
|
824
|
-
await Promise.resolve()
|
|
825
|
-
expect(root.innerHTML).toBe(
|
|
826
|
-
'<div><div>parent 1</div><button>+</button><div><div><div>child 1 0</div><button>+</button></div><div><div>child 2 0</div><button>+</button></div><div><div>child 3 0</div><button>+</button></div></div></div>'
|
|
827
|
-
)
|
|
828
|
-
child2Button?.click()
|
|
829
|
-
await Promise.resolve()
|
|
830
|
-
expect(root.innerHTML).toBe(
|
|
831
|
-
'<div><div>parent 1</div><button>+</button><div><div><div>child 1 0</div><button>+</button></div><div><div>child 2 1</div><button>+</button></div><div><div>child 3 0</div><button>+</button></div></div></div>'
|
|
832
|
-
)
|
|
833
|
-
child1Button?.click()
|
|
834
|
-
await Promise.resolve()
|
|
835
|
-
expect(root.innerHTML).toBe(
|
|
836
|
-
'<div><div>parent 1</div><button>+</button><div><div><div>child 1 1</div><button>+</button></div><div><div>child 2 1</div><button>+</button></div><div><div>child 3 0</div><button>+</button></div></div></div>'
|
|
837
|
-
)
|
|
838
|
-
child3Button?.click()
|
|
839
|
-
await Promise.resolve()
|
|
840
|
-
expect(root.innerHTML).toBe(
|
|
841
|
-
'<div><div>parent 1</div><button>+</button><div><div><div>child 1 1</div><button>+</button></div><div><div>child 2 1</div><button>+</button></div><div><div>child 3 1</div><button>+</button></div></div></div>'
|
|
842
|
-
)
|
|
843
|
-
})
|
|
844
|
-
|
|
845
|
-
it('keeps sibling order when a null sibling exists after parent update', async () => {
|
|
846
|
-
const Empty = () => null
|
|
847
|
-
const Child = () => {
|
|
848
|
-
const [count, setCount] = useState(0)
|
|
849
|
-
return count === 0 ? (
|
|
850
|
-
<>
|
|
851
|
-
<div>A0</div>
|
|
852
|
-
<button id='child' onClick={() => setCount(1)}>
|
|
853
|
-
child+
|
|
854
|
-
</button>
|
|
855
|
-
</>
|
|
856
|
-
) : (
|
|
857
|
-
<>
|
|
858
|
-
<span>A1</span>
|
|
859
|
-
<span>A2</span>
|
|
860
|
-
</>
|
|
861
|
-
)
|
|
862
|
-
}
|
|
863
|
-
const App = () => {
|
|
864
|
-
const [count, setCount] = useState(0)
|
|
865
|
-
return (
|
|
866
|
-
<>
|
|
867
|
-
<Child />
|
|
868
|
-
<Empty />
|
|
869
|
-
<div id='tail'>T{count}</div>
|
|
870
|
-
<button id='parent' onClick={() => setCount(count + 1)}>
|
|
871
|
-
parent+
|
|
872
|
-
</button>
|
|
873
|
-
</>
|
|
874
|
-
)
|
|
875
|
-
}
|
|
876
|
-
render(<App />, root)
|
|
877
|
-
expect(root.innerHTML).toBe(
|
|
878
|
-
'<div>A0</div><button id="child">child+</button><div id="tail">T0</div><button id="parent">parent+</button>'
|
|
879
|
-
)
|
|
880
|
-
root.querySelector<HTMLButtonElement>('#parent')?.click()
|
|
881
|
-
await Promise.resolve()
|
|
882
|
-
expect(root.innerHTML).toBe(
|
|
883
|
-
'<div>A0</div><button id="child">child+</button><div id="tail">T1</div><button id="parent">parent+</button>'
|
|
884
|
-
)
|
|
885
|
-
root.querySelector<HTMLButtonElement>('#child')?.click()
|
|
886
|
-
await Promise.resolve()
|
|
887
|
-
expect(root.innerHTML).toBe(
|
|
888
|
-
'<span>A1</span><span>A2</span><div id="tail">T1</div><button id="parent">parent+</button>'
|
|
889
|
-
)
|
|
890
|
-
})
|
|
891
|
-
|
|
892
|
-
it('multiple children with dynamic addition and rerender', async () => {
|
|
893
|
-
const Child = ({ name }: { name: string }) => {
|
|
894
|
-
const [count, setCount] = useState(0)
|
|
895
|
-
return (
|
|
896
|
-
<div>
|
|
897
|
-
<div>
|
|
898
|
-
{name} {count}
|
|
899
|
-
</div>
|
|
900
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
901
|
-
</div>
|
|
902
|
-
)
|
|
903
|
-
}
|
|
904
|
-
const App = () => {
|
|
905
|
-
const [showThird, setShowThird] = useState(false)
|
|
906
|
-
return (
|
|
907
|
-
<div>
|
|
908
|
-
<Child name='child 1' />
|
|
909
|
-
<Child name='child 2' />
|
|
910
|
-
{showThird && <Child name='child 3' />}
|
|
911
|
-
<button onClick={() => setShowThird(true)}>add</button>
|
|
912
|
-
</div>
|
|
913
|
-
)
|
|
914
|
-
}
|
|
915
|
-
render(<App />, root)
|
|
916
|
-
expect(root.innerHTML).toBe(
|
|
917
|
-
'<div><div><div>child 1 0</div><button>+</button></div><div><div>child 2 0</div><button>+</button></div><button>add</button></div>'
|
|
918
|
-
)
|
|
919
|
-
// add child 3
|
|
920
|
-
let buttons = root.querySelectorAll('button')
|
|
921
|
-
buttons[2]?.click() // add
|
|
922
|
-
await Promise.resolve()
|
|
923
|
-
expect(root.innerHTML).toBe(
|
|
924
|
-
'<div><div><div>child 1 0</div><button>+</button></div><div><div>child 2 0</div><button>+</button></div><div><div>child 3 0</div><button>+</button></div><button>add</button></div>'
|
|
925
|
-
)
|
|
926
|
-
// click child 1
|
|
927
|
-
buttons = root.querySelectorAll('button')
|
|
928
|
-
buttons[0]?.click() // child 1
|
|
929
|
-
await Promise.resolve()
|
|
930
|
-
expect(root.innerHTML).toBe(
|
|
931
|
-
'<div><div><div>child 1 1</div><button>+</button></div><div><div>child 2 0</div><button>+</button></div><div><div>child 3 0</div><button>+</button></div><button>add</button></div>'
|
|
932
|
-
)
|
|
933
|
-
// click child 2 - verify child 2 and child 3 do not swap positions
|
|
934
|
-
buttons = root.querySelectorAll('button')
|
|
935
|
-
buttons[1]?.click() // child 2
|
|
936
|
-
await Promise.resolve()
|
|
937
|
-
expect(root.innerHTML).toBe(
|
|
938
|
-
'<div><div><div>child 1 1</div><button>+</button></div><div><div>child 2 1</div><button>+</button></div><div><div>child 3 0</div><button>+</button></div><button>add</button></div>'
|
|
939
|
-
)
|
|
940
|
-
})
|
|
941
|
-
})
|
|
942
|
-
|
|
943
|
-
describe('defaultProps', () => {
|
|
944
|
-
it('simple', () => {
|
|
945
|
-
const App: FC<{ name?: string }> = ({ name }) => <div>{name}</div>
|
|
946
|
-
App.defaultProps = { name: 'default' }
|
|
947
|
-
render(<App />, root)
|
|
948
|
-
expect(root.innerHTML).toBe('<div>default</div>')
|
|
949
|
-
})
|
|
950
|
-
|
|
951
|
-
it('override', () => {
|
|
952
|
-
const App: FC<{ name: string }> = ({ name }) => <div>{name}</div>
|
|
953
|
-
App.defaultProps = { name: 'default' }
|
|
954
|
-
render(<App name='override' />, root)
|
|
955
|
-
expect(root.innerHTML).toBe('<div>override</div>')
|
|
956
|
-
})
|
|
957
|
-
})
|
|
958
|
-
|
|
959
|
-
describe('replace content', () => {
|
|
960
|
-
it('text to text', async () => {
|
|
961
|
-
let setCount: (count: number) => void = () => {}
|
|
962
|
-
const App = () => {
|
|
963
|
-
const [count, _setCount] = useState(0)
|
|
964
|
-
setCount = _setCount
|
|
965
|
-
return <>{count}</>
|
|
966
|
-
}
|
|
967
|
-
render(<App />, root)
|
|
968
|
-
expect(root.innerHTML).toBe('0')
|
|
969
|
-
setCount(1)
|
|
970
|
-
await Promise.resolve()
|
|
971
|
-
expect(root.innerHTML).toBe('1')
|
|
972
|
-
})
|
|
973
|
-
|
|
974
|
-
it('text to element', async () => {
|
|
975
|
-
let setCount: (count: number) => void = () => {}
|
|
976
|
-
const App = () => {
|
|
977
|
-
const [count, _setCount] = useState(0)
|
|
978
|
-
setCount = _setCount
|
|
979
|
-
return count === 0 ? <>{count}</> : <div>{count}</div>
|
|
980
|
-
}
|
|
981
|
-
render(<App />, root)
|
|
982
|
-
expect(root.innerHTML).toBe('0')
|
|
983
|
-
setCount(1)
|
|
984
|
-
await Promise.resolve()
|
|
985
|
-
expect(root.innerHTML).toBe('<div>1</div>')
|
|
986
|
-
})
|
|
987
|
-
|
|
988
|
-
it('element to element', async () => {
|
|
989
|
-
let setCount: (count: number) => void = () => {}
|
|
990
|
-
const App = () => {
|
|
991
|
-
const [count, _setCount] = useState(0)
|
|
992
|
-
setCount = _setCount
|
|
993
|
-
return <div>{count}</div>
|
|
994
|
-
}
|
|
995
|
-
const app = <App />
|
|
996
|
-
render(app, root)
|
|
997
|
-
expect(root.innerHTML).toBe('<div>0</div>')
|
|
998
|
-
|
|
999
|
-
const insertBeforeSpy = vi.spyOn(dom.window.Node.prototype, 'insertBefore')
|
|
1000
|
-
setCount(1)
|
|
1001
|
-
await Promise.resolve()
|
|
1002
|
-
expect(root.innerHTML).toBe('<div>1</div>')
|
|
1003
|
-
expect(insertBeforeSpy).not.toHaveBeenCalled()
|
|
1004
|
-
})
|
|
1005
|
-
|
|
1006
|
-
it('element to text to element', async () => {
|
|
1007
|
-
let setCount: (count: number) => void = () => {}
|
|
1008
|
-
const App = () => {
|
|
1009
|
-
const [count, _setCount] = useState(0)
|
|
1010
|
-
setCount = _setCount
|
|
1011
|
-
return count % 2 === 0 ? <div>{count}</div> : <>{count}</>
|
|
1012
|
-
}
|
|
1013
|
-
render(<App />, root)
|
|
1014
|
-
expect(root.innerHTML).toBe('<div>0</div>')
|
|
1015
|
-
setCount(1)
|
|
1016
|
-
await Promise.resolve()
|
|
1017
|
-
expect(root.innerHTML).toBe('1')
|
|
1018
|
-
setCount(2)
|
|
1019
|
-
await Promise.resolve()
|
|
1020
|
-
expect(root.innerHTML).toBe('<div>2</div>')
|
|
1021
|
-
})
|
|
1022
|
-
|
|
1023
|
-
it('text to child component to text', async () => {
|
|
1024
|
-
let setCount: (count: number) => void = () => {}
|
|
1025
|
-
const Child = () => {
|
|
1026
|
-
return <div>Child</div>
|
|
1027
|
-
}
|
|
1028
|
-
const App = () => {
|
|
1029
|
-
const [count, _setCount] = useState(0)
|
|
1030
|
-
setCount = _setCount
|
|
1031
|
-
return count % 2 === 0 ? <>{count}</> : <Child />
|
|
1032
|
-
}
|
|
1033
|
-
render(<App />, root)
|
|
1034
|
-
expect(root.innerHTML).toBe('0')
|
|
1035
|
-
setCount(1)
|
|
1036
|
-
await Promise.resolve()
|
|
1037
|
-
expect(root.innerHTML).toBe('<div>Child</div>')
|
|
1038
|
-
setCount(2)
|
|
1039
|
-
await Promise.resolve()
|
|
1040
|
-
expect(root.innerHTML).toBe('2')
|
|
1041
|
-
})
|
|
1042
|
-
|
|
1043
|
-
it('one child is updated', async () => {
|
|
1044
|
-
let setCount: (count: number) => void = () => {}
|
|
1045
|
-
const App = () => {
|
|
1046
|
-
const [count, _setCount] = useState(0)
|
|
1047
|
-
setCount = _setCount
|
|
1048
|
-
return <div>{count}</div>
|
|
1049
|
-
}
|
|
1050
|
-
const app = (
|
|
1051
|
-
<>
|
|
1052
|
-
<App />
|
|
1053
|
-
<div>Footer</div>
|
|
1054
|
-
</>
|
|
1055
|
-
)
|
|
1056
|
-
render(app, root)
|
|
1057
|
-
expect(root.innerHTML).toBe('<div>0</div><div>Footer</div>')
|
|
1058
|
-
|
|
1059
|
-
const insertBeforeSpy = vi.spyOn(dom.window.Node.prototype, 'insertBefore')
|
|
1060
|
-
setCount(1)
|
|
1061
|
-
await Promise.resolve()
|
|
1062
|
-
expect(root.innerHTML).toBe('<div>1</div><div>Footer</div>')
|
|
1063
|
-
expect(insertBeforeSpy).not.toHaveBeenCalled()
|
|
1064
|
-
})
|
|
1065
|
-
|
|
1066
|
-
it('should not call insertBefore for unchanged complex dom tree', async () => {
|
|
1067
|
-
let setCount: (count: number) => void = () => {}
|
|
1068
|
-
const App = () => {
|
|
1069
|
-
const [count, _setCount] = useState(0)
|
|
1070
|
-
setCount = _setCount
|
|
1071
|
-
return (
|
|
1072
|
-
<form>
|
|
1073
|
-
<div>
|
|
1074
|
-
<label>label</label>
|
|
1075
|
-
<input />
|
|
1076
|
-
</div>
|
|
1077
|
-
<p>{count}</p>
|
|
1078
|
-
</form>
|
|
1079
|
-
)
|
|
1080
|
-
}
|
|
1081
|
-
const app = <App />
|
|
1082
|
-
|
|
1083
|
-
render(app, root)
|
|
1084
|
-
expect(root.innerHTML).toBe('<form><div><label>label</label><input></div><p>0</p></form>')
|
|
1085
|
-
|
|
1086
|
-
const insertBeforeSpy = vi.spyOn(dom.window.Node.prototype, 'insertBefore')
|
|
1087
|
-
setCount(1)
|
|
1088
|
-
await Promise.resolve()
|
|
1089
|
-
expect(root.innerHTML).toBe('<form><div><label>label</label><input></div><p>1</p></form>')
|
|
1090
|
-
expect(insertBeforeSpy).not.toHaveBeenCalled()
|
|
1091
|
-
})
|
|
1092
|
-
|
|
1093
|
-
it('should not call textContent for unchanged text', async () => {
|
|
1094
|
-
let setCount: (count: number) => void = () => {}
|
|
1095
|
-
const App = () => {
|
|
1096
|
-
const [count, _setCount] = useState(0)
|
|
1097
|
-
setCount = _setCount
|
|
1098
|
-
return (
|
|
1099
|
-
<>
|
|
1100
|
-
<span>acryl</span>
|
|
1101
|
-
<input value={count} />
|
|
1102
|
-
</>
|
|
1103
|
-
)
|
|
1104
|
-
}
|
|
1105
|
-
render(<App />, root)
|
|
1106
|
-
expect(root.innerHTML).toBe('<span>acryl</span><input value="0">')
|
|
1107
|
-
setCount(1)
|
|
1108
|
-
|
|
1109
|
-
const textContentSpy = vi.fn()
|
|
1110
|
-
Object.defineProperty(dom.window.Text.prototype, 'textContent', {
|
|
1111
|
-
set: textContentSpy,
|
|
1112
|
-
})
|
|
1113
|
-
await Promise.resolve()
|
|
1114
|
-
expect(root.innerHTML).toBe('<span>acryl</span><input value="1">')
|
|
1115
|
-
expect(textContentSpy).not.toHaveBeenCalled()
|
|
1116
|
-
})
|
|
1117
|
-
})
|
|
1118
|
-
|
|
1119
|
-
describe('children', () => {
|
|
1120
|
-
it('element', async () => {
|
|
1121
|
-
const Container = ({ children }: { children: Child }) => <div>{children}</div>
|
|
1122
|
-
const App = () => (
|
|
1123
|
-
<Container>
|
|
1124
|
-
<span>Content</span>
|
|
1125
|
-
</Container>
|
|
1126
|
-
)
|
|
1127
|
-
render(<App />, root)
|
|
1128
|
-
expect(root.innerHTML).toBe('<div><span>Content</span></div>')
|
|
1129
|
-
})
|
|
1130
|
-
|
|
1131
|
-
it('array', async () => {
|
|
1132
|
-
const Container = ({ children }: { children: Child }) => <div>{children}</div>
|
|
1133
|
-
const App = () => <Container>{[<span>1</span>, <span>2</span>]}</Container>
|
|
1134
|
-
render(<App />, root)
|
|
1135
|
-
expect(root.innerHTML).toBe('<div><span>1</span><span>2</span></div>')
|
|
1136
|
-
})
|
|
1137
|
-
|
|
1138
|
-
it('empty array and non-empty array', async () => {
|
|
1139
|
-
const App = () => (
|
|
1140
|
-
<div>
|
|
1141
|
-
{[]}
|
|
1142
|
-
{[<span>1</span>]}
|
|
1143
|
-
</div>
|
|
1144
|
-
)
|
|
1145
|
-
render(<App />, root)
|
|
1146
|
-
expect(root.innerHTML).toBe('<div><span>1</span></div>')
|
|
1147
|
-
})
|
|
1148
|
-
|
|
1149
|
-
it('nested array', async () => {
|
|
1150
|
-
const nestedChildren: Child = [[[<span>1</span>], <span>2</span>]]
|
|
1151
|
-
const App = () => <div>{nestedChildren}</div>
|
|
1152
|
-
render(<App />, root)
|
|
1153
|
-
expect(root.innerHTML).toBe('<div><span>1</span><span>2</span></div>')
|
|
1154
|
-
})
|
|
1155
|
-
|
|
1156
|
-
it('sparse array with nested child', async () => {
|
|
1157
|
-
const sparseChildren: Child[] = []
|
|
1158
|
-
sparseChildren[1] = [<span>1</span>]
|
|
1159
|
-
const App = () => <div>{sparseChildren}</div>
|
|
1160
|
-
render(<App />, root)
|
|
1161
|
-
expect(root.innerHTML).toBe('<div><span>1</span></div>')
|
|
1162
|
-
})
|
|
1163
|
-
|
|
1164
|
-
it('toggle empty array and non-empty array on update', async () => {
|
|
1165
|
-
let setVisible: (value: boolean) => void = () => {}
|
|
1166
|
-
const App = () => {
|
|
1167
|
-
const [visible, _setVisible] = useState(false)
|
|
1168
|
-
setVisible = _setVisible
|
|
1169
|
-
return (
|
|
1170
|
-
<div>
|
|
1171
|
-
{visible ? [] : [<span key='a'>A</span>]}
|
|
1172
|
-
{visible ? [<span key='b'>B</span>] : []}
|
|
1173
|
-
</div>
|
|
1174
|
-
)
|
|
1175
|
-
}
|
|
1176
|
-
render(<App />, root)
|
|
1177
|
-
expect(root.innerHTML).toBe('<div><span>A</span></div>')
|
|
1178
|
-
|
|
1179
|
-
setVisible(true)
|
|
1180
|
-
await Promise.resolve()
|
|
1181
|
-
expect(root.innerHTML).toBe('<div><span>B</span></div>')
|
|
1182
|
-
|
|
1183
|
-
setVisible(false)
|
|
1184
|
-
await Promise.resolve()
|
|
1185
|
-
expect(root.innerHTML).toBe('<div><span>A</span></div>')
|
|
1186
|
-
})
|
|
1187
|
-
|
|
1188
|
-
it('reshape nested array on update', async () => {
|
|
1189
|
-
let setPattern: (value: number) => void = () => {}
|
|
1190
|
-
const App = () => {
|
|
1191
|
-
const [pattern, _setPattern] = useState(0)
|
|
1192
|
-
setPattern = _setPattern
|
|
1193
|
-
const children: Child =
|
|
1194
|
-
pattern === 0
|
|
1195
|
-
? [[<span key='a'>A</span>], <span key='b'>B</span>]
|
|
1196
|
-
: [<span key='a'>A</span>, [<span key='b'>B</span>]]
|
|
1197
|
-
return <div>{children}</div>
|
|
1198
|
-
}
|
|
1199
|
-
render(<App />, root)
|
|
1200
|
-
expect(root.innerHTML).toBe('<div><span>A</span><span>B</span></div>')
|
|
1201
|
-
|
|
1202
|
-
setPattern(1)
|
|
1203
|
-
await Promise.resolve()
|
|
1204
|
-
expect(root.innerHTML).toBe('<div><span>A</span><span>B</span></div>')
|
|
1205
|
-
|
|
1206
|
-
setPattern(0)
|
|
1207
|
-
await Promise.resolve()
|
|
1208
|
-
expect(root.innerHTML).toBe('<div><span>A</span><span>B</span></div>')
|
|
1209
|
-
})
|
|
1210
|
-
|
|
1211
|
-
it('use the same children multiple times', async () => {
|
|
1212
|
-
const MultiChildren = ({ children }: { children: Child }) => (
|
|
1213
|
-
<>
|
|
1214
|
-
{children}
|
|
1215
|
-
<div>{children}</div>
|
|
1216
|
-
</>
|
|
1217
|
-
)
|
|
1218
|
-
const App = () => (
|
|
1219
|
-
<MultiChildren>
|
|
1220
|
-
<span>Content</span>
|
|
1221
|
-
</MultiChildren>
|
|
1222
|
-
)
|
|
1223
|
-
render(<App />, root)
|
|
1224
|
-
expect(root.innerHTML).toBe('<span>Content</span><div><span>Content</span></div>')
|
|
1225
|
-
})
|
|
1226
|
-
})
|
|
1227
|
-
|
|
1228
|
-
describe('update properties', () => {
|
|
1229
|
-
describe('input', () => {
|
|
1230
|
-
it('value', async () => {
|
|
1231
|
-
let setValue: (value: string) => void = () => {}
|
|
1232
|
-
const App = () => {
|
|
1233
|
-
const [value, _setValue] = useState('a')
|
|
1234
|
-
setValue = _setValue
|
|
1235
|
-
return <input value={value} />
|
|
1236
|
-
}
|
|
1237
|
-
render(<App />, root)
|
|
1238
|
-
expect(root.innerHTML).toBe('<input value="a">')
|
|
1239
|
-
const valueSpy = vi.fn()
|
|
1240
|
-
Object.defineProperty(dom.window.HTMLInputElement.prototype, 'value', {
|
|
1241
|
-
set: valueSpy,
|
|
1242
|
-
})
|
|
1243
|
-
setValue('b')
|
|
1244
|
-
await Promise.resolve()
|
|
1245
|
-
expect(root.innerHTML).toBe('<input value="b">')
|
|
1246
|
-
expect(valueSpy).toHaveBeenCalledWith('b')
|
|
1247
|
-
})
|
|
1248
|
-
|
|
1249
|
-
it('assign undefined', async () => {
|
|
1250
|
-
let setValue: (value: string | undefined) => void = () => {}
|
|
1251
|
-
const App = () => {
|
|
1252
|
-
const [value, _setValue] = useState<string | undefined>('a')
|
|
1253
|
-
setValue = _setValue
|
|
1254
|
-
return <input value={value} />
|
|
1255
|
-
}
|
|
1256
|
-
render(<App />, root)
|
|
1257
|
-
expect(root.innerHTML).toBe('<input value="a">')
|
|
1258
|
-
const valueSpy = vi.fn()
|
|
1259
|
-
Object.defineProperty(dom.window.HTMLInputElement.prototype, 'value', {
|
|
1260
|
-
set: valueSpy,
|
|
1261
|
-
})
|
|
1262
|
-
setValue(undefined)
|
|
1263
|
-
await Promise.resolve()
|
|
1264
|
-
expect(root.innerHTML).toBe('<input>')
|
|
1265
|
-
expect(valueSpy).toHaveBeenCalledWith(null) // assign null means empty string
|
|
1266
|
-
})
|
|
1267
|
-
|
|
1268
|
-
it('checked', async () => {
|
|
1269
|
-
let setValue: (value: string) => void = () => {}
|
|
1270
|
-
const App = () => {
|
|
1271
|
-
const [value, _setValue] = useState('a')
|
|
1272
|
-
setValue = _setValue
|
|
1273
|
-
return <input type='checkbox' checked={value === 'b'} />
|
|
1274
|
-
}
|
|
1275
|
-
render(<App />, root)
|
|
1276
|
-
expect(root.innerHTML).toBe('<input type="checkbox">')
|
|
1277
|
-
const checkedSpy = vi.fn()
|
|
1278
|
-
Object.defineProperty(dom.window.HTMLInputElement.prototype, 'checked', {
|
|
1279
|
-
set: checkedSpy,
|
|
1280
|
-
})
|
|
1281
|
-
setValue('b')
|
|
1282
|
-
await Promise.resolve()
|
|
1283
|
-
expect(root.innerHTML).toBe('<input type="checkbox" checked="">')
|
|
1284
|
-
expect(checkedSpy).toHaveBeenCalledWith(true)
|
|
1285
|
-
setValue('a')
|
|
1286
|
-
await Promise.resolve()
|
|
1287
|
-
expect(root.innerHTML).toBe('<input type="checkbox">')
|
|
1288
|
-
expect(checkedSpy).toHaveBeenCalledWith(false)
|
|
1289
|
-
})
|
|
1290
|
-
})
|
|
1291
|
-
|
|
1292
|
-
describe('textarea', () => {
|
|
1293
|
-
it('value', async () => {
|
|
1294
|
-
let setValue: (value: string) => void = () => {}
|
|
1295
|
-
const App = () => {
|
|
1296
|
-
const [value, _setValue] = useState('a')
|
|
1297
|
-
setValue = _setValue
|
|
1298
|
-
return <textarea value={value} />
|
|
1299
|
-
}
|
|
1300
|
-
render(<App />, root)
|
|
1301
|
-
expect(root.innerHTML).toBe('<textarea>a</textarea>')
|
|
1302
|
-
const valueSpy = vi.fn()
|
|
1303
|
-
Object.defineProperty(dom.window.HTMLTextAreaElement.prototype, 'value', {
|
|
1304
|
-
set: valueSpy,
|
|
1305
|
-
})
|
|
1306
|
-
setValue('b')
|
|
1307
|
-
await Promise.resolve()
|
|
1308
|
-
expect(root.innerHTML).toBe('<textarea>b</textarea>')
|
|
1309
|
-
expect(valueSpy).toHaveBeenCalledWith('b')
|
|
1310
|
-
})
|
|
1311
|
-
|
|
1312
|
-
it('assign undefined', async () => {
|
|
1313
|
-
let setValue: (value: string | undefined) => void = () => {}
|
|
1314
|
-
const App = () => {
|
|
1315
|
-
const [value, _setValue] = useState<string | undefined>('a')
|
|
1316
|
-
setValue = _setValue
|
|
1317
|
-
return <textarea value={value} />
|
|
1318
|
-
}
|
|
1319
|
-
render(<App />, root)
|
|
1320
|
-
expect(root.innerHTML).toBe('<textarea>a</textarea>')
|
|
1321
|
-
const valueSpy = vi.fn()
|
|
1322
|
-
Object.defineProperty(dom.window.HTMLTextAreaElement.prototype, 'value', {
|
|
1323
|
-
set: valueSpy,
|
|
1324
|
-
})
|
|
1325
|
-
setValue(undefined)
|
|
1326
|
-
await Promise.resolve()
|
|
1327
|
-
expect(root.innerHTML).toBe('<textarea></textarea>')
|
|
1328
|
-
expect(valueSpy).toHaveBeenCalledWith(null) // assign null means empty string
|
|
1329
|
-
})
|
|
1330
|
-
})
|
|
1331
|
-
|
|
1332
|
-
describe('select', () => {
|
|
1333
|
-
it('value', async () => {
|
|
1334
|
-
let setValue: (value: string) => void = () => {}
|
|
1335
|
-
const App = () => {
|
|
1336
|
-
const [value, _setValue] = useState('a')
|
|
1337
|
-
setValue = _setValue
|
|
1338
|
-
return (
|
|
1339
|
-
<select value={value}>
|
|
1340
|
-
<option value='a'>A</option>
|
|
1341
|
-
<option value='b'>B</option>
|
|
1342
|
-
<option value='c'>C</option>
|
|
1343
|
-
</select>
|
|
1344
|
-
)
|
|
1345
|
-
}
|
|
1346
|
-
render(<App />, root)
|
|
1347
|
-
expect(root.innerHTML).toBe(
|
|
1348
|
-
'<select><option value="a">A</option><option value="b">B</option><option value="c">C</option></select>'
|
|
1349
|
-
)
|
|
1350
|
-
const valueSpy = vi.fn()
|
|
1351
|
-
Object.defineProperty(dom.window.HTMLSelectElement.prototype, 'value', {
|
|
1352
|
-
set: valueSpy,
|
|
1353
|
-
})
|
|
1354
|
-
setValue('b')
|
|
1355
|
-
await Promise.resolve()
|
|
1356
|
-
expect(valueSpy).toHaveBeenCalledWith('b')
|
|
1357
|
-
})
|
|
1358
|
-
|
|
1359
|
-
it('invalid value', async () => {
|
|
1360
|
-
let setValue: (value: string) => void = () => {}
|
|
1361
|
-
const App = () => {
|
|
1362
|
-
const [value, _setValue] = useState('a')
|
|
1363
|
-
setValue = _setValue
|
|
1364
|
-
return (
|
|
1365
|
-
<select value={value}>
|
|
1366
|
-
<option value='a'>A</option>
|
|
1367
|
-
<option value='b'>B</option>
|
|
1368
|
-
<option value='c'>C</option>
|
|
1369
|
-
</select>
|
|
1370
|
-
)
|
|
1371
|
-
}
|
|
1372
|
-
render(<App />, root)
|
|
1373
|
-
expect(root.innerHTML).toBe(
|
|
1374
|
-
'<select><option value="a">A</option><option value="b">B</option><option value="c">C</option></select>'
|
|
1375
|
-
)
|
|
1376
|
-
setValue('z')
|
|
1377
|
-
await Promise.resolve()
|
|
1378
|
-
const select = root.querySelector('select') as HTMLSelectElement
|
|
1379
|
-
expect(select.value).toBe('a') // invalid value is ignored
|
|
1380
|
-
})
|
|
1381
|
-
|
|
1382
|
-
it('assign undefined', async () => {
|
|
1383
|
-
let setValue: (value: string | undefined) => void = () => {}
|
|
1384
|
-
const App = () => {
|
|
1385
|
-
const [value, _setValue] = useState<string | undefined>('b')
|
|
1386
|
-
setValue = _setValue
|
|
1387
|
-
return (
|
|
1388
|
-
<select value={value}>
|
|
1389
|
-
<option value='a'>A</option>
|
|
1390
|
-
<option value='b'>B</option>
|
|
1391
|
-
<option value='c'>C</option>
|
|
1392
|
-
</select>
|
|
1393
|
-
)
|
|
1394
|
-
}
|
|
1395
|
-
render(<App />, root)
|
|
1396
|
-
expect(root.innerHTML).toBe(
|
|
1397
|
-
'<select><option value="a">A</option><option value="b">B</option><option value="c">C</option></select>'
|
|
1398
|
-
)
|
|
1399
|
-
setValue(undefined)
|
|
1400
|
-
await Promise.resolve()
|
|
1401
|
-
const select = root.querySelector('select') as HTMLSelectElement
|
|
1402
|
-
expect(select.value).toBe('a') // select the first option
|
|
1403
|
-
expect(select.selectedIndex).toBe(0)
|
|
1404
|
-
})
|
|
1405
|
-
|
|
1406
|
-
it('apply value after options are added', async () => {
|
|
1407
|
-
let setOptions: (options: string[]) => void = () => {}
|
|
1408
|
-
const App = () => {
|
|
1409
|
-
const [options, _setOptions] = useState<string[]>([])
|
|
1410
|
-
setOptions = _setOptions
|
|
1411
|
-
return (
|
|
1412
|
-
<select value='option2'>
|
|
1413
|
-
{options.map((opt) => (
|
|
1414
|
-
<option key={opt} value={opt}>
|
|
1415
|
-
{opt}
|
|
1416
|
-
</option>
|
|
1417
|
-
))}
|
|
1418
|
-
</select>
|
|
1419
|
-
)
|
|
1420
|
-
}
|
|
1421
|
-
render(<App />, root)
|
|
1422
|
-
setOptions(['option1', 'option2', 'option3'])
|
|
1423
|
-
await Promise.resolve()
|
|
1424
|
-
const select = root.querySelector('select') as HTMLSelectElement
|
|
1425
|
-
expect(select.value).toBe('option2')
|
|
1426
|
-
expect(select.selectedIndex).toBe(1)
|
|
1427
|
-
expect(select.options[1].selected).toBe(true)
|
|
1428
|
-
})
|
|
1429
|
-
|
|
1430
|
-
it('select the first option when undefined after options are added', async () => {
|
|
1431
|
-
let setOptions: (options: string[]) => void = () => {}
|
|
1432
|
-
const App = () => {
|
|
1433
|
-
const [options, _setOptions] = useState<string[]>([])
|
|
1434
|
-
setOptions = _setOptions
|
|
1435
|
-
return (
|
|
1436
|
-
<select value={undefined}>
|
|
1437
|
-
{options.map((opt) => (
|
|
1438
|
-
<option key={opt} value={opt}>
|
|
1439
|
-
{opt}
|
|
1440
|
-
</option>
|
|
1441
|
-
))}
|
|
1442
|
-
</select>
|
|
1443
|
-
)
|
|
1444
|
-
}
|
|
1445
|
-
render(<App />, root)
|
|
1446
|
-
setOptions(['option1', 'option2', 'option3'])
|
|
1447
|
-
await Promise.resolve()
|
|
1448
|
-
const select = root.querySelector('select') as HTMLSelectElement
|
|
1449
|
-
expect(select.value).toBe('option1')
|
|
1450
|
-
expect(select.selectedIndex).toBe(0)
|
|
1451
|
-
expect(select.options[0].selected).toBe(true)
|
|
1452
|
-
})
|
|
1453
|
-
|
|
1454
|
-
it('do not select the first option for invalid multiple value', () => {
|
|
1455
|
-
const App = () => {
|
|
1456
|
-
return (
|
|
1457
|
-
<select multiple value='z'>
|
|
1458
|
-
<option value='a'>A</option>
|
|
1459
|
-
<option value='b'>B</option>
|
|
1460
|
-
<option value='c'>C</option>
|
|
1461
|
-
</select>
|
|
1462
|
-
)
|
|
1463
|
-
}
|
|
1464
|
-
render(<App />, root)
|
|
1465
|
-
const select = root.querySelector('select') as HTMLSelectElement
|
|
1466
|
-
expect(select.value).toBe('')
|
|
1467
|
-
expect(select.selectedIndex).toBe(-1)
|
|
1468
|
-
expect([...select.options].every((option) => !option.selected)).toBe(true)
|
|
1469
|
-
})
|
|
1470
|
-
|
|
1471
|
-
it('keep invalid multiple value unselected after options are added', async () => {
|
|
1472
|
-
let setOptions: (options: string[]) => void = () => {}
|
|
1473
|
-
const App = () => {
|
|
1474
|
-
const [options, _setOptions] = useState<string[]>([])
|
|
1475
|
-
setOptions = _setOptions
|
|
1476
|
-
return (
|
|
1477
|
-
<select multiple value='z'>
|
|
1478
|
-
{options.map((opt) => (
|
|
1479
|
-
<option key={opt} value={opt}>
|
|
1480
|
-
{opt}
|
|
1481
|
-
</option>
|
|
1482
|
-
))}
|
|
1483
|
-
</select>
|
|
1484
|
-
)
|
|
1485
|
-
}
|
|
1486
|
-
render(<App />, root)
|
|
1487
|
-
setOptions(['a', 'b', 'c'])
|
|
1488
|
-
await Promise.resolve()
|
|
1489
|
-
const select = root.querySelector('select') as HTMLSelectElement
|
|
1490
|
-
expect(select.value).toBe('')
|
|
1491
|
-
expect(select.selectedIndex).toBe(-1)
|
|
1492
|
-
expect([...select.options].every((option) => !option.selected)).toBe(true)
|
|
1493
|
-
})
|
|
1494
|
-
})
|
|
1495
|
-
|
|
1496
|
-
describe('option', () => {
|
|
1497
|
-
it('selected', async () => {
|
|
1498
|
-
let setValue: (value: string) => void = () => {}
|
|
1499
|
-
const App = () => {
|
|
1500
|
-
const [value, _setValue] = useState('a')
|
|
1501
|
-
setValue = _setValue
|
|
1502
|
-
return (
|
|
1503
|
-
<select>
|
|
1504
|
-
<option value='a'>A</option>
|
|
1505
|
-
<option value='b' selected={value === 'b'}>
|
|
1506
|
-
B
|
|
1507
|
-
</option>
|
|
1508
|
-
<option value='c'>C</option>
|
|
1509
|
-
</select>
|
|
1510
|
-
)
|
|
1511
|
-
}
|
|
1512
|
-
render(<App />, root)
|
|
1513
|
-
expect(root.innerHTML).toBe(
|
|
1514
|
-
'<select><option value="a">A</option><option value="b">B</option><option value="c">C</option></select>'
|
|
1515
|
-
)
|
|
1516
|
-
setValue('b')
|
|
1517
|
-
await Promise.resolve()
|
|
1518
|
-
expect(root.innerHTML).toBe(
|
|
1519
|
-
'<select><option value="a">A</option><option value="b" selected="">B</option><option value="c">C</option></select>'
|
|
1520
|
-
)
|
|
1521
|
-
const select = root.querySelector('select') as HTMLSelectElement
|
|
1522
|
-
expect(select.value).toBe('b')
|
|
1523
|
-
setValue('a')
|
|
1524
|
-
await Promise.resolve()
|
|
1525
|
-
expect(root.innerHTML).toBe(
|
|
1526
|
-
'<select><option value="a">A</option><option value="b">B</option><option value="c">C</option></select>'
|
|
1527
|
-
)
|
|
1528
|
-
expect(select.value).toBe('a')
|
|
1529
|
-
})
|
|
1530
|
-
})
|
|
1531
|
-
})
|
|
1532
|
-
|
|
1533
|
-
describe('dangerouslySetInnerHTML', () => {
|
|
1534
|
-
it('string', () => {
|
|
1535
|
-
const App = () => {
|
|
1536
|
-
return <div dangerouslySetInnerHTML={{ __html: '<p>Hello</p>' }} />
|
|
1537
|
-
}
|
|
1538
|
-
render(<App />, root)
|
|
1539
|
-
expect(root.innerHTML).toBe('<div><p>Hello</p></div>')
|
|
1540
|
-
})
|
|
1541
|
-
})
|
|
1542
|
-
|
|
1543
|
-
describe('Event', () => {
|
|
1544
|
-
it('bubbling phase', async () => {
|
|
1545
|
-
const clicked: string[] = []
|
|
1546
|
-
const App = () => {
|
|
1547
|
-
return (
|
|
1548
|
-
<div
|
|
1549
|
-
onClick={() => {
|
|
1550
|
-
clicked.push('div')
|
|
1551
|
-
}}
|
|
1552
|
-
>
|
|
1553
|
-
<button
|
|
1554
|
-
onClick={() => {
|
|
1555
|
-
clicked.push('button')
|
|
1556
|
-
}}
|
|
1557
|
-
>
|
|
1558
|
-
Click
|
|
1559
|
-
</button>
|
|
1560
|
-
</div>
|
|
1561
|
-
)
|
|
1562
|
-
}
|
|
1563
|
-
render(<App />, root)
|
|
1564
|
-
expect(root.innerHTML).toBe('<div><button>Click</button></div>')
|
|
1565
|
-
root.querySelector('button')?.click()
|
|
1566
|
-
expect(clicked).toEqual(['button', 'div'])
|
|
1567
|
-
})
|
|
1568
|
-
|
|
1569
|
-
it('ev.stopPropagation()', async () => {
|
|
1570
|
-
const clicked: string[] = []
|
|
1571
|
-
const App = () => {
|
|
1572
|
-
return (
|
|
1573
|
-
<div
|
|
1574
|
-
onClick={() => {
|
|
1575
|
-
clicked.push('div')
|
|
1576
|
-
}}
|
|
1577
|
-
>
|
|
1578
|
-
<button
|
|
1579
|
-
onClick={(ev) => {
|
|
1580
|
-
ev.stopPropagation()
|
|
1581
|
-
clicked.push('button')
|
|
1582
|
-
}}
|
|
1583
|
-
>
|
|
1584
|
-
Click
|
|
1585
|
-
</button>
|
|
1586
|
-
</div>
|
|
1587
|
-
)
|
|
1588
|
-
}
|
|
1589
|
-
render(<App />, root)
|
|
1590
|
-
expect(root.innerHTML).toBe('<div><button>Click</button></div>')
|
|
1591
|
-
root.querySelector('button')?.click()
|
|
1592
|
-
expect(clicked).toEqual(['button'])
|
|
1593
|
-
})
|
|
1594
|
-
|
|
1595
|
-
it('capture phase', async () => {
|
|
1596
|
-
const clicked: string[] = []
|
|
1597
|
-
const App = () => {
|
|
1598
|
-
return (
|
|
1599
|
-
<div
|
|
1600
|
-
onClickCapture={(ev) => {
|
|
1601
|
-
ev.stopPropagation()
|
|
1602
|
-
clicked.push('div')
|
|
1603
|
-
}}
|
|
1604
|
-
>
|
|
1605
|
-
<button
|
|
1606
|
-
onClickCapture={() => {
|
|
1607
|
-
clicked.push('button')
|
|
1608
|
-
}}
|
|
1609
|
-
>
|
|
1610
|
-
Click
|
|
1611
|
-
</button>
|
|
1612
|
-
</div>
|
|
1613
|
-
)
|
|
1614
|
-
}
|
|
1615
|
-
render(<App />, root)
|
|
1616
|
-
expect(root.innerHTML).toBe('<div><button>Click</button></div>')
|
|
1617
|
-
root.querySelector('button')?.click()
|
|
1618
|
-
expect(clicked).toEqual(['div'])
|
|
1619
|
-
})
|
|
1620
|
-
|
|
1621
|
-
it('remove capture phase event', async () => {
|
|
1622
|
-
const clicked: string[] = []
|
|
1623
|
-
const App = () => {
|
|
1624
|
-
const [canceled, setCanceled] = useState(false)
|
|
1625
|
-
return (
|
|
1626
|
-
<div
|
|
1627
|
-
{...(canceled
|
|
1628
|
-
? {}
|
|
1629
|
-
: {
|
|
1630
|
-
onClickCapture: () => {
|
|
1631
|
-
clicked.push('div')
|
|
1632
|
-
},
|
|
1633
|
-
})}
|
|
1634
|
-
>
|
|
1635
|
-
<button
|
|
1636
|
-
onClickCapture={() => {
|
|
1637
|
-
setCanceled(true)
|
|
1638
|
-
clicked.push('button')
|
|
1639
|
-
}}
|
|
1640
|
-
>
|
|
1641
|
-
Click
|
|
1642
|
-
</button>
|
|
1643
|
-
</div>
|
|
1644
|
-
)
|
|
1645
|
-
}
|
|
1646
|
-
render(<App />, root)
|
|
1647
|
-
expect(root.innerHTML).toBe('<div><button>Click</button></div>')
|
|
1648
|
-
root.querySelector('button')?.click()
|
|
1649
|
-
expect(clicked).toEqual(['div', 'button'])
|
|
1650
|
-
await Promise.resolve()
|
|
1651
|
-
root.querySelector('button')?.click()
|
|
1652
|
-
expect(clicked).toEqual(['div', 'button', 'button'])
|
|
1653
|
-
})
|
|
1654
|
-
|
|
1655
|
-
it('onGotPointerCapture', async () => {
|
|
1656
|
-
const App = () => {
|
|
1657
|
-
return <div onGotPointerCapture={() => {}}></div>
|
|
1658
|
-
}
|
|
1659
|
-
const addEventListenerSpy = vi.spyOn(dom.window.Node.prototype, 'addEventListener')
|
|
1660
|
-
render(<App />, root)
|
|
1661
|
-
expect(addEventListenerSpy).toHaveBeenCalledOnce()
|
|
1662
|
-
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
|
1663
|
-
'gotpointercapture',
|
|
1664
|
-
expect.any(Function),
|
|
1665
|
-
false
|
|
1666
|
-
)
|
|
1667
|
-
})
|
|
1668
|
-
|
|
1669
|
-
it('onGotPointerCaptureCapture', async () => {
|
|
1670
|
-
const App = () => {
|
|
1671
|
-
return <div onGotPointerCaptureCapture={() => {}}></div>
|
|
1672
|
-
}
|
|
1673
|
-
const addEventListenerSpy = vi.spyOn(dom.window.Node.prototype, 'addEventListener')
|
|
1674
|
-
render(<App />, root)
|
|
1675
|
-
expect(addEventListenerSpy).toHaveBeenCalledOnce()
|
|
1676
|
-
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
|
1677
|
-
'gotpointercapture',
|
|
1678
|
-
expect.any(Function),
|
|
1679
|
-
true
|
|
1680
|
-
)
|
|
1681
|
-
})
|
|
1682
|
-
|
|
1683
|
-
it('undefined', async () => {
|
|
1684
|
-
const App = () => {
|
|
1685
|
-
return <div onClick={undefined}></div>
|
|
1686
|
-
}
|
|
1687
|
-
const addEventListenerSpy = vi.spyOn(dom.window.Node.prototype, 'addEventListener')
|
|
1688
|
-
render(<App />, root)
|
|
1689
|
-
expect(addEventListenerSpy).not.toHaveBeenCalled()
|
|
1690
|
-
})
|
|
1691
|
-
|
|
1692
|
-
it('invalid event handler value', async () => {
|
|
1693
|
-
const App = () => {
|
|
1694
|
-
return <div onClick={1 as unknown as () => void}></div>
|
|
1695
|
-
}
|
|
1696
|
-
expect(() => render(<App />, root)).toThrow()
|
|
1697
|
-
})
|
|
1698
|
-
})
|
|
1699
|
-
|
|
1700
|
-
it('simple Counter', async () => {
|
|
1701
|
-
const Counter = () => {
|
|
1702
|
-
const [count, setCount] = useState(0)
|
|
1703
|
-
return (
|
|
1704
|
-
<div>
|
|
1705
|
-
<p>Count: {count}</p>
|
|
1706
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
1707
|
-
</div>
|
|
1708
|
-
)
|
|
1709
|
-
}
|
|
1710
|
-
const app = <Counter />
|
|
1711
|
-
render(app, root)
|
|
1712
|
-
expect(root.innerHTML).toBe('<div><p>Count: 0</p><button>+</button></div>')
|
|
1713
|
-
const button = root.querySelector('button') as HTMLButtonElement
|
|
1714
|
-
button.click()
|
|
1715
|
-
await Promise.resolve()
|
|
1716
|
-
expect(root.innerHTML).toBe('<div><p>Count: 1</p><button>+</button></div>')
|
|
1717
|
-
})
|
|
1718
|
-
|
|
1719
|
-
it('multiple useState()', async () => {
|
|
1720
|
-
let called = 0
|
|
1721
|
-
const Counter = () => {
|
|
1722
|
-
const [countA, setCountA] = useState(0)
|
|
1723
|
-
const [countB, setCountB] = useState(0)
|
|
1724
|
-
called++
|
|
1725
|
-
return (
|
|
1726
|
-
<div>
|
|
1727
|
-
<p>A: {countA}</p>
|
|
1728
|
-
<button onClick={() => setCountA(countA + 1)}>+</button>
|
|
1729
|
-
<p>B: {countB}</p>
|
|
1730
|
-
<button onClick={() => setCountB(countB + 1)}>+</button>
|
|
1731
|
-
</div>
|
|
1732
|
-
)
|
|
1733
|
-
}
|
|
1734
|
-
const app = <Counter />
|
|
1735
|
-
render(app, root)
|
|
1736
|
-
expect(root.innerHTML).toBe(
|
|
1737
|
-
'<div><p>A: 0</p><button>+</button><p>B: 0</p><button>+</button></div>'
|
|
1738
|
-
)
|
|
1739
|
-
expect(called).toBe(1)
|
|
1740
|
-
const [buttonA, buttonB] = root.querySelectorAll('button')
|
|
1741
|
-
for (let i = 0; i < 3; i++) {
|
|
1742
|
-
buttonA.click()
|
|
1743
|
-
await Promise.resolve()
|
|
1744
|
-
}
|
|
1745
|
-
for (let i = 0; i < 4; i++) {
|
|
1746
|
-
buttonB.click()
|
|
1747
|
-
await Promise.resolve()
|
|
1748
|
-
}
|
|
1749
|
-
expect(root.innerHTML).toBe(
|
|
1750
|
-
'<div><p>A: 3</p><button>+</button><p>B: 4</p><button>+</button></div>'
|
|
1751
|
-
)
|
|
1752
|
-
expect(called).toBe(8)
|
|
1753
|
-
})
|
|
1754
|
-
|
|
1755
|
-
it('multiple update state calls at once in onClick attributes', async () => {
|
|
1756
|
-
let called = 0
|
|
1757
|
-
const Counter = () => {
|
|
1758
|
-
const [countA, setCountA] = useState(0)
|
|
1759
|
-
const [countB, setCountB] = useState(0)
|
|
1760
|
-
called++
|
|
1761
|
-
return (
|
|
1762
|
-
<div>
|
|
1763
|
-
<button
|
|
1764
|
-
onClick={() => {
|
|
1765
|
-
setCountA(countA + 1)
|
|
1766
|
-
setCountB(countB + 2)
|
|
1767
|
-
}}
|
|
1768
|
-
>
|
|
1769
|
-
+
|
|
1770
|
-
</button>
|
|
1771
|
-
{countA} {countB}
|
|
1772
|
-
</div>
|
|
1773
|
-
)
|
|
1774
|
-
}
|
|
1775
|
-
const app = <Counter />
|
|
1776
|
-
render(app, root)
|
|
1777
|
-
expect(root.innerHTML).toBe('<div><button>+</button>0 0</div>')
|
|
1778
|
-
expect(called).toBe(1)
|
|
1779
|
-
root.querySelector('button')?.click()
|
|
1780
|
-
expect(called).toBe(1)
|
|
1781
|
-
await Promise.resolve()
|
|
1782
|
-
expect(called).toBe(2)
|
|
1783
|
-
})
|
|
1784
|
-
|
|
1785
|
-
it('multiple update state calls at once in dom events', async () => {
|
|
1786
|
-
let called = 0
|
|
1787
|
-
const Counter = () => {
|
|
1788
|
-
const [countA, setCountA] = useState(0)
|
|
1789
|
-
const [countB, setCountB] = useState(0)
|
|
1790
|
-
const buttonRef = useRef<HTMLButtonElement>(null)
|
|
1791
|
-
called++
|
|
1792
|
-
|
|
1793
|
-
useEffect(() => {
|
|
1794
|
-
buttonRef.current?.addEventListener('click', () => {
|
|
1795
|
-
setCountA(countA + 1)
|
|
1796
|
-
setCountB(countB + 2)
|
|
1797
|
-
})
|
|
1798
|
-
}, [])
|
|
1799
|
-
|
|
1800
|
-
return (
|
|
1801
|
-
<div>
|
|
1802
|
-
<button ref={buttonRef}>+</button>
|
|
1803
|
-
{countA} {countB}
|
|
1804
|
-
</div>
|
|
1805
|
-
)
|
|
1806
|
-
}
|
|
1807
|
-
const app = <Counter />
|
|
1808
|
-
render(app, root)
|
|
1809
|
-
expect(root.innerHTML).toBe('<div><button>+</button>0 0</div>')
|
|
1810
|
-
expect(called).toBe(1)
|
|
1811
|
-
await new Promise((resolve) => setTimeout(resolve))
|
|
1812
|
-
root.querySelector('button')?.click()
|
|
1813
|
-
expect(called).toBe(1)
|
|
1814
|
-
await Promise.resolve()
|
|
1815
|
-
expect(called).toBe(2)
|
|
1816
|
-
})
|
|
1817
|
-
|
|
1818
|
-
it('nested useState()', async () => {
|
|
1819
|
-
const ChildCounter = () => {
|
|
1820
|
-
const [count, setCount] = useState(0)
|
|
1821
|
-
return (
|
|
1822
|
-
<div>
|
|
1823
|
-
<p>Child Count: {count}</p>
|
|
1824
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
1825
|
-
</div>
|
|
1826
|
-
)
|
|
1827
|
-
}
|
|
1828
|
-
const Counter = () => {
|
|
1829
|
-
const [count, setCount] = useState(0)
|
|
1830
|
-
return (
|
|
1831
|
-
<div>
|
|
1832
|
-
<p>Count: {count}</p>
|
|
1833
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
1834
|
-
<ChildCounter />
|
|
1835
|
-
</div>
|
|
1836
|
-
)
|
|
1837
|
-
}
|
|
1838
|
-
const app = <Counter />
|
|
1839
|
-
render(app, root)
|
|
1840
|
-
expect(root.innerHTML).toBe(
|
|
1841
|
-
'<div><p>Count: 0</p><button>+</button><div><p>Child Count: 0</p><button>+</button></div></div>'
|
|
1842
|
-
)
|
|
1843
|
-
const [button, childButton] = root.querySelectorAll('button')
|
|
1844
|
-
for (let i = 0; i < 3; i++) {
|
|
1845
|
-
childButton.click()
|
|
1846
|
-
await Promise.resolve()
|
|
1847
|
-
}
|
|
1848
|
-
for (let i = 0; i < 2; i++) {
|
|
1849
|
-
button.click()
|
|
1850
|
-
await Promise.resolve()
|
|
1851
|
-
}
|
|
1852
|
-
for (let i = 0; i < 3; i++) {
|
|
1853
|
-
childButton.click()
|
|
1854
|
-
await Promise.resolve()
|
|
1855
|
-
}
|
|
1856
|
-
for (let i = 0; i < 3; i++) {
|
|
1857
|
-
button.click()
|
|
1858
|
-
await Promise.resolve()
|
|
1859
|
-
}
|
|
1860
|
-
expect(root.innerHTML).toBe(
|
|
1861
|
-
'<div><p>Count: 5</p><button>+</button><div><p>Child Count: 6</p><button>+</button></div></div>'
|
|
1862
|
-
)
|
|
1863
|
-
})
|
|
1864
|
-
|
|
1865
|
-
it('nested useState() with children', async () => {
|
|
1866
|
-
const ChildCounter = () => {
|
|
1867
|
-
const [count, setCount] = useState(0)
|
|
1868
|
-
return (
|
|
1869
|
-
<div>
|
|
1870
|
-
<p>Child Count: {count}</p>
|
|
1871
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
1872
|
-
</div>
|
|
1873
|
-
)
|
|
1874
|
-
}
|
|
1875
|
-
const Counter = ({ children }: { children: Child }) => {
|
|
1876
|
-
const [count, setCount] = useState(0)
|
|
1877
|
-
return (
|
|
1878
|
-
<div>
|
|
1879
|
-
<p>Count: {count}</p>
|
|
1880
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
1881
|
-
{children}
|
|
1882
|
-
</div>
|
|
1883
|
-
)
|
|
1884
|
-
}
|
|
1885
|
-
const app = (
|
|
1886
|
-
<Counter>
|
|
1887
|
-
<ChildCounter />
|
|
1888
|
-
</Counter>
|
|
1889
|
-
)
|
|
1890
|
-
render(app, root)
|
|
1891
|
-
expect(root.innerHTML).toBe(
|
|
1892
|
-
'<div><p>Count: 0</p><button>+</button><div><p>Child Count: 0</p><button>+</button></div></div>'
|
|
1893
|
-
)
|
|
1894
|
-
const [button, childButton] = root.querySelectorAll('button')
|
|
1895
|
-
for (let i = 0; i < 3; i++) {
|
|
1896
|
-
childButton.click()
|
|
1897
|
-
await Promise.resolve()
|
|
1898
|
-
}
|
|
1899
|
-
for (let i = 0; i < 2; i++) {
|
|
1900
|
-
button.click()
|
|
1901
|
-
await Promise.resolve()
|
|
1902
|
-
}
|
|
1903
|
-
for (let i = 0; i < 3; i++) {
|
|
1904
|
-
childButton.click()
|
|
1905
|
-
await Promise.resolve()
|
|
1906
|
-
}
|
|
1907
|
-
for (let i = 0; i < 3; i++) {
|
|
1908
|
-
button.click()
|
|
1909
|
-
await Promise.resolve()
|
|
1910
|
-
}
|
|
1911
|
-
expect(root.innerHTML).toBe(
|
|
1912
|
-
'<div><p>Count: 5</p><button>+</button><div><p>Child Count: 6</p><button>+</button></div></div>'
|
|
1913
|
-
)
|
|
1914
|
-
})
|
|
1915
|
-
|
|
1916
|
-
it('consecutive fragment', async () => {
|
|
1917
|
-
const ComponentA = () => {
|
|
1918
|
-
const [count, setCount] = useState(0)
|
|
1919
|
-
return (
|
|
1920
|
-
<>
|
|
1921
|
-
<div>A: {count}</div>
|
|
1922
|
-
<button id='a-button' onClick={() => setCount(count + 1)}>
|
|
1923
|
-
A: +
|
|
1924
|
-
</button>
|
|
1925
|
-
</>
|
|
1926
|
-
)
|
|
1927
|
-
}
|
|
1928
|
-
const App = () => {
|
|
1929
|
-
const [count, setCount] = useState(0)
|
|
1930
|
-
return (
|
|
1931
|
-
<>
|
|
1932
|
-
<ComponentA />
|
|
1933
|
-
<div>B: {count}</div>
|
|
1934
|
-
<button id='b-button' onClick={() => setCount(count + 1)}>
|
|
1935
|
-
B: +
|
|
1936
|
-
</button>
|
|
1937
|
-
</>
|
|
1938
|
-
)
|
|
1939
|
-
}
|
|
1940
|
-
render(<App />, root)
|
|
1941
|
-
expect(root.innerHTML).toBe(
|
|
1942
|
-
'<div>A: 0</div><button id="a-button">A: +</button><div>B: 0</div><button id="b-button">B: +</button>'
|
|
1943
|
-
)
|
|
1944
|
-
root.querySelector<HTMLButtonElement>('#b-button')?.click()
|
|
1945
|
-
await Promise.resolve()
|
|
1946
|
-
expect(root.innerHTML).toBe(
|
|
1947
|
-
'<div>A: 0</div><button id="a-button">A: +</button><div>B: 1</div><button id="b-button">B: +</button>'
|
|
1948
|
-
)
|
|
1949
|
-
root.querySelector<HTMLButtonElement>('#a-button')?.click()
|
|
1950
|
-
await Promise.resolve()
|
|
1951
|
-
expect(root.innerHTML).toBe(
|
|
1952
|
-
'<div>A: 1</div><button id="a-button">A: +</button><div>B: 1</div><button id="b-button">B: +</button>'
|
|
1953
|
-
)
|
|
1954
|
-
})
|
|
1955
|
-
|
|
1956
|
-
it('switch child component', async () => {
|
|
1957
|
-
const Even = () => <p>Even</p>
|
|
1958
|
-
const Odd = () => <div>Odd</div>
|
|
1959
|
-
const Counter = () => {
|
|
1960
|
-
const [count, setCount] = useState(0)
|
|
1961
|
-
return (
|
|
1962
|
-
<div>
|
|
1963
|
-
{count % 2 === 0 ? <Even /> : <Odd />}
|
|
1964
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
1965
|
-
</div>
|
|
1966
|
-
)
|
|
1967
|
-
}
|
|
1968
|
-
const app = <Counter />
|
|
1969
|
-
render(app, root)
|
|
1970
|
-
expect(root.innerHTML).toBe('<div><p>Even</p><button>+</button></div>')
|
|
1971
|
-
const button = root.querySelector('button') as HTMLButtonElement
|
|
1972
|
-
button.click()
|
|
1973
|
-
await Promise.resolve()
|
|
1974
|
-
expect(root.innerHTML).toBe('<div><div>Odd</div><button>+</button></div>')
|
|
1975
|
-
button.click()
|
|
1976
|
-
await Promise.resolve()
|
|
1977
|
-
expect(root.innerHTML).toBe('<div><p>Even</p><button>+</button></div>')
|
|
1978
|
-
})
|
|
1979
|
-
|
|
1980
|
-
it('add/remove/swap item', async () => {
|
|
1981
|
-
const TodoApp = () => {
|
|
1982
|
-
const [todos, setTodos] = useState(['a', 'b', 'c'])
|
|
1983
|
-
return (
|
|
1984
|
-
<div>
|
|
1985
|
-
{todos.map((todo) => (
|
|
1986
|
-
<div key={todo}>{todo}</div>
|
|
1987
|
-
))}
|
|
1988
|
-
<button onClick={() => setTodos([...todos, 'd'])}>add</button>
|
|
1989
|
-
<button onClick={() => setTodos(todos.slice(0, -1))}>remove</button>
|
|
1990
|
-
<button onClick={() => setTodos([todos[0], todos[2], todos[1], todos[3] || ''])}>
|
|
1991
|
-
swap
|
|
1992
|
-
</button>
|
|
1993
|
-
</div>
|
|
1994
|
-
)
|
|
1995
|
-
}
|
|
1996
|
-
const app = <TodoApp />
|
|
1997
|
-
render(app, root)
|
|
1998
|
-
expect(root.innerHTML).toBe(
|
|
1999
|
-
'<div><div>a</div><div>b</div><div>c</div><button>add</button><button>remove</button><button>swap</button></div>'
|
|
2000
|
-
)
|
|
2001
|
-
const [addButton] = root.querySelectorAll('button')
|
|
2002
|
-
addButton.click()
|
|
2003
|
-
await Promise.resolve()
|
|
2004
|
-
expect(root.innerHTML).toBe(
|
|
2005
|
-
'<div><div>a</div><div>b</div><div>c</div><div>d</div><button>add</button><button>remove</button><button>swap</button></div>'
|
|
2006
|
-
)
|
|
2007
|
-
const [, , swapButton] = root.querySelectorAll('button')
|
|
2008
|
-
swapButton.click()
|
|
2009
|
-
await Promise.resolve()
|
|
2010
|
-
expect(root.innerHTML).toBe(
|
|
2011
|
-
'<div><div>a</div><div>c</div><div>b</div><div>d</div><button>add</button><button>remove</button><button>swap</button></div>'
|
|
2012
|
-
)
|
|
2013
|
-
const [, removeButton] = root.querySelectorAll('button')
|
|
2014
|
-
removeButton.click()
|
|
2015
|
-
await Promise.resolve()
|
|
2016
|
-
expect(root.innerHTML).toBe(
|
|
2017
|
-
'<div><div>a</div><div>c</div><div>b</div><button>add</button><button>remove</button><button>swap</button></div>'
|
|
2018
|
-
)
|
|
2019
|
-
})
|
|
2020
|
-
|
|
2021
|
-
it('reconciles large keyed list updates and reordering efficiently', async () => {
|
|
2022
|
-
let findIndexCalls = 0
|
|
2023
|
-
const originalFindIndex = Array.prototype.findIndex
|
|
2024
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2025
|
-
Array.prototype.findIndex = function (this: unknown[], ...args: [any]) {
|
|
2026
|
-
findIndexCalls++
|
|
2027
|
-
return originalFindIndex.apply(this, args)
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
try {
|
|
2031
|
-
const ListApp = () => {
|
|
2032
|
-
const [items, setItems] = useState(() => Array.from({ length: 100 }, (_, i) => `item-${i}`))
|
|
2033
|
-
return (
|
|
2034
|
-
<div>
|
|
2035
|
-
<ul>
|
|
2036
|
-
{items.map((item) => (
|
|
2037
|
-
<li key={item}>{item}</li>
|
|
2038
|
-
))}
|
|
2039
|
-
</ul>
|
|
2040
|
-
<button id='reverse' onClick={() => setItems((prev) => [...prev].reverse())}>
|
|
2041
|
-
reverse
|
|
2042
|
-
</button>
|
|
2043
|
-
<button id='update' onClick={() => setItems((prev) => [...prev])}>
|
|
2044
|
-
update
|
|
2045
|
-
</button>
|
|
2046
|
-
</div>
|
|
2047
|
-
)
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
render(<ListApp />, root)
|
|
2051
|
-
expect(root.querySelectorAll('li')).toHaveLength(100)
|
|
2052
|
-
expect(root.querySelector('li')?.textContent).toBe('item-0')
|
|
2053
|
-
|
|
2054
|
-
findIndexCalls = 0
|
|
2055
|
-
const updateBtn = root.querySelector('#update') as HTMLButtonElement
|
|
2056
|
-
updateBtn.click()
|
|
2057
|
-
await Promise.resolve()
|
|
2058
|
-
|
|
2059
|
-
expect(findIndexCalls).toBe(0)
|
|
2060
|
-
expect(root.querySelectorAll('li')).toHaveLength(100)
|
|
2061
|
-
expect(root.querySelector('li')?.textContent).toBe('item-0')
|
|
2062
|
-
|
|
2063
|
-
const reverseBtn = root.querySelector('#reverse') as HTMLButtonElement
|
|
2064
|
-
reverseBtn.click()
|
|
2065
|
-
await Promise.resolve()
|
|
2066
|
-
|
|
2067
|
-
expect(root.querySelectorAll('li')).toHaveLength(100)
|
|
2068
|
-
expect(root.querySelector('li')?.textContent).toBe('item-99')
|
|
2069
|
-
} finally {
|
|
2070
|
-
Array.prototype.findIndex = originalFindIndex
|
|
2071
|
-
}
|
|
2072
|
-
})
|
|
2073
|
-
|
|
2074
|
-
it('preserves keyed child identity and state when reordering', async () => {
|
|
2075
|
-
let reverse: () => void = () => {}
|
|
2076
|
-
const Row = ({ id }: { id: string }) => {
|
|
2077
|
-
const [count, setCount] = useState(0)
|
|
2078
|
-
return (
|
|
2079
|
-
<button data-id={id} onClick={() => setCount(count + 1)}>
|
|
2080
|
-
{id}:{count}
|
|
2081
|
-
</button>
|
|
2082
|
-
)
|
|
2083
|
-
}
|
|
2084
|
-
const App = () => {
|
|
2085
|
-
const [ids, setIds] = useState(['a', 'b', 'c'])
|
|
2086
|
-
reverse = () => setIds((ids) => [...ids].reverse())
|
|
2087
|
-
return ids.map((id) => <Row key={id} id={id} />)
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
render(<App />, root)
|
|
2091
|
-
const initialRows = Object.fromEntries(
|
|
2092
|
-
[...root.querySelectorAll<HTMLButtonElement>('button')].map((element) => [
|
|
2093
|
-
element.dataset.id,
|
|
2094
|
-
element,
|
|
2095
|
-
])
|
|
2096
|
-
)
|
|
2097
|
-
|
|
2098
|
-
initialRows.b.click()
|
|
2099
|
-
await Promise.resolve()
|
|
2100
|
-
reverse()
|
|
2101
|
-
await Promise.resolve()
|
|
2102
|
-
|
|
2103
|
-
const reorderedRows = [...root.querySelectorAll<HTMLButtonElement>('button')]
|
|
2104
|
-
expect(reorderedRows.map((element) => element.textContent)).toEqual(['c:0', 'b:1', 'a:0'])
|
|
2105
|
-
expect(reorderedRows[0]).toBe(initialRows.c)
|
|
2106
|
-
expect(reorderedRows[1]).toBe(initialRows.b)
|
|
2107
|
-
expect(reorderedRows[2]).toBe(initialRows.a)
|
|
2108
|
-
})
|
|
2109
|
-
|
|
2110
|
-
it('reuses duplicate keyed children in their previous order', async () => {
|
|
2111
|
-
let rotate: () => void = () => {}
|
|
2112
|
-
const Row = ({ label }: { label: string }) => {
|
|
2113
|
-
const [initialLabel] = useState(label)
|
|
2114
|
-
return <div>{`${label}:${initialLabel}`}</div>
|
|
2115
|
-
}
|
|
2116
|
-
const App = () => {
|
|
2117
|
-
const [labels, setLabels] = useState(['other', 'first', 'second'])
|
|
2118
|
-
rotate = () => setLabels(['first', 'second', 'other'])
|
|
2119
|
-
return labels.map((label) => (
|
|
2120
|
-
<Row key={label === 'other' ? 'other' : 'duplicate'} label={label} />
|
|
2121
|
-
))
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
render(<App />, root)
|
|
2125
|
-
const initialRows = [...root.querySelectorAll('div')]
|
|
2126
|
-
rotate()
|
|
2127
|
-
await Promise.resolve()
|
|
2128
|
-
|
|
2129
|
-
const reorderedRows = [...root.querySelectorAll('div')]
|
|
2130
|
-
expect(reorderedRows.map((element) => element.textContent)).toEqual([
|
|
2131
|
-
'first:first',
|
|
2132
|
-
'second:second',
|
|
2133
|
-
'other:other',
|
|
2134
|
-
])
|
|
2135
|
-
expect(reorderedRows[0]).toBe(initialRows[1])
|
|
2136
|
-
expect(reorderedRows[1]).toBe(initialRows[2])
|
|
2137
|
-
expect(reorderedRows[2]).toBe(initialRows[0])
|
|
2138
|
-
})
|
|
2139
|
-
|
|
2140
|
-
it.each([
|
|
2141
|
-
{
|
|
2142
|
-
consumedBy: 'an earlier keyed child',
|
|
2143
|
-
before: [2, 1, 5, 1],
|
|
2144
|
-
after: [9, 2, 1, 1],
|
|
2145
|
-
matches: [-1, 0, 1, 3],
|
|
2146
|
-
statefulIndex: 3,
|
|
2147
|
-
removedIndices: [2],
|
|
2148
|
-
},
|
|
2149
|
-
{
|
|
2150
|
-
consumedBy: 'an earlier unkeyed child',
|
|
2151
|
-
before: ['other', 1, 1, 1, 3],
|
|
2152
|
-
after: [1, undefined, 1, 1],
|
|
2153
|
-
matches: [1, 2, 3, -1],
|
|
2154
|
-
statefulIndex: 2,
|
|
2155
|
-
removedIndices: [0, 4],
|
|
2156
|
-
},
|
|
2157
|
-
])(
|
|
2158
|
-
'does not reuse children already consumed by $consumedBy',
|
|
2159
|
-
async ({ before, after, matches, statefulIndex, removedIndices }) => {
|
|
2160
|
-
let reorder: () => void = () => {}
|
|
2161
|
-
const Other = () => <button>Other</button>
|
|
2162
|
-
const Row = () => {
|
|
2163
|
-
const [count, setCount] = useState(0)
|
|
2164
|
-
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
2165
|
-
}
|
|
2166
|
-
const App = () => {
|
|
2167
|
-
const [keys, setKeys] = useState<Array<number | string | undefined>>(before)
|
|
2168
|
-
reorder = () => setKeys(after)
|
|
2169
|
-
return keys.map((key) => (key === 'other' ? <Other /> : <Row key={key} />))
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
render(<App />, root)
|
|
2173
|
-
const initialRows = [...root.querySelectorAll('button')]
|
|
2174
|
-
initialRows[statefulIndex].click()
|
|
2175
|
-
await Promise.resolve()
|
|
2176
|
-
reorder()
|
|
2177
|
-
await Promise.resolve()
|
|
2178
|
-
|
|
2179
|
-
const reorderedRows = [...root.querySelectorAll('button')]
|
|
2180
|
-
expect(reorderedRows).toHaveLength(matches.length)
|
|
2181
|
-
matches.forEach((oldIndex, newIndex) => {
|
|
2182
|
-
if (oldIndex === -1) {
|
|
2183
|
-
expect(initialRows.includes(reorderedRows[newIndex])).toBe(false)
|
|
2184
|
-
} else {
|
|
2185
|
-
expect(reorderedRows[newIndex]).toBe(initialRows[oldIndex])
|
|
2186
|
-
}
|
|
2187
|
-
expect(reorderedRows[newIndex].textContent).toBe(oldIndex === statefulIndex ? '1' : '0')
|
|
2188
|
-
})
|
|
2189
|
-
removedIndices.forEach((index) => {
|
|
2190
|
-
expect(initialRows[index].isConnected).toBe(false)
|
|
2191
|
-
})
|
|
2192
|
-
}
|
|
2193
|
-
)
|
|
2194
|
-
|
|
2195
|
-
it('preserves existing keyed children when prepending', async () => {
|
|
2196
|
-
let prepend: () => void = () => {}
|
|
2197
|
-
const App = () => {
|
|
2198
|
-
const [ids, setIds] = useState(['a', 'b'])
|
|
2199
|
-
prepend = () => setIds(['x', 'a', 'b'])
|
|
2200
|
-
return ids.map((id) => <div key={id}>{id}</div>)
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
render(<App />, root)
|
|
2204
|
-
const initialRows = [...root.querySelectorAll('div')]
|
|
2205
|
-
prepend()
|
|
2206
|
-
await Promise.resolve()
|
|
2207
|
-
|
|
2208
|
-
const prependedRows = [...root.querySelectorAll('div')]
|
|
2209
|
-
expect(prependedRows.map((element) => element.textContent)).toEqual(['x', 'a', 'b'])
|
|
2210
|
-
expect(prependedRows[1]).toBe(initialRows[0])
|
|
2211
|
-
expect(prependedRows[2]).toBe(initialRows[1])
|
|
2212
|
-
})
|
|
2213
|
-
|
|
2214
|
-
it('unmounts a keyed child removed from the middle', async () => {
|
|
2215
|
-
let removeMiddle: () => void = () => {}
|
|
2216
|
-
const cleanup = vi.fn()
|
|
2217
|
-
const ref = vi.fn().mockReturnValue(cleanup)
|
|
2218
|
-
const App = () => {
|
|
2219
|
-
const [ids, setIds] = useState(['a', 'b', 'c'])
|
|
2220
|
-
removeMiddle = () => setIds(['a', 'c'])
|
|
2221
|
-
return ids.map((id) => (
|
|
2222
|
-
<div key={id} ref={id === 'b' ? ref : undefined}>
|
|
2223
|
-
{id}
|
|
2224
|
-
</div>
|
|
2225
|
-
))
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
render(<App />, root)
|
|
2229
|
-
const initialRows = [...root.querySelectorAll('div')]
|
|
2230
|
-
removeMiddle()
|
|
2231
|
-
await Promise.resolve()
|
|
2232
|
-
|
|
2233
|
-
const remainingRows = [...root.querySelectorAll('div')]
|
|
2234
|
-
expect(remainingRows.map((element) => element.textContent)).toEqual(['a', 'c'])
|
|
2235
|
-
expect(remainingRows[0]).toBe(initialRows[0])
|
|
2236
|
-
expect(remainingRows[1]).toBe(initialRows[2])
|
|
2237
|
-
expect(initialRows[1].isConnected).toBe(false)
|
|
2238
|
-
expect(cleanup).toHaveBeenCalledOnce()
|
|
2239
|
-
})
|
|
2240
|
-
|
|
2241
|
-
it('cleans up removed keyed children in their previous order', async () => {
|
|
2242
|
-
let remove: () => void = () => {}
|
|
2243
|
-
const cleanedUp: string[] = []
|
|
2244
|
-
const Row = ({ id }: { id: string }) => {
|
|
2245
|
-
useLayoutEffect(
|
|
2246
|
-
() => () => {
|
|
2247
|
-
cleanedUp.push(id)
|
|
2248
|
-
},
|
|
2249
|
-
[]
|
|
2250
|
-
)
|
|
2251
|
-
return <div>{id}</div>
|
|
2252
|
-
}
|
|
2253
|
-
const App = () => {
|
|
2254
|
-
const [ids, setIds] = useState(['a', 'b', 'c', 'd'])
|
|
2255
|
-
remove = () => setIds(['b'])
|
|
2256
|
-
return ids.map((id) => <Row key={id} id={id} />)
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
render(<App />, root)
|
|
2260
|
-
const initialRows = [...root.children]
|
|
2261
|
-
remove()
|
|
2262
|
-
await Promise.resolve()
|
|
2263
|
-
|
|
2264
|
-
expect(root.children).toHaveLength(1)
|
|
2265
|
-
expect(root.firstElementChild).toBe(initialRows[1])
|
|
2266
|
-
expect(cleanedUp).toEqual(['a', 'c', 'd'])
|
|
2267
|
-
expect(initialRows.filter((row) => row.isConnected)).toHaveLength(1)
|
|
2268
|
-
})
|
|
2269
|
-
|
|
2270
|
-
it('allows an unkeyed child to reuse a keyed child with the same tag', async () => {
|
|
2271
|
-
let removeKeyedSiblings: () => void = () => {}
|
|
2272
|
-
const Row = ({ label }: { label: string }) => {
|
|
2273
|
-
const [initialLabel] = useState(label)
|
|
2274
|
-
return <div>{`${label}:${initialLabel}`}</div>
|
|
2275
|
-
}
|
|
2276
|
-
const App = () => {
|
|
2277
|
-
const [showSingle, setShowSingle] = useState(false)
|
|
2278
|
-
removeKeyedSiblings = () => setShowSingle(true)
|
|
2279
|
-
return showSingle ? (
|
|
2280
|
-
<Row label='replacement' />
|
|
2281
|
-
) : (
|
|
2282
|
-
[<Row key='keyed' label='keyed' />, <Row label='unkeyed' />]
|
|
2283
|
-
)
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
render(<App />, root)
|
|
2287
|
-
const initialRows = [...root.querySelectorAll('div')]
|
|
2288
|
-
removeKeyedSiblings()
|
|
2289
|
-
await Promise.resolve()
|
|
2290
|
-
|
|
2291
|
-
const replacement = root.querySelector('div') as HTMLDivElement
|
|
2292
|
-
expect(replacement.textContent).toBe('replacement:keyed')
|
|
2293
|
-
expect(replacement).toBe(initialRows[0])
|
|
2294
|
-
expect(initialRows[1].isConnected).toBe(false)
|
|
2295
|
-
})
|
|
2296
|
-
|
|
2297
|
-
it('preserves unkeyed elements and text when removing a leading sibling', async () => {
|
|
2298
|
-
let hideHeading: () => void = () => {}
|
|
2299
|
-
const App = () => {
|
|
2300
|
-
const [showHeading, setShowHeading] = useState(true)
|
|
2301
|
-
hideHeading = () => setShowHeading(false)
|
|
2302
|
-
return [
|
|
2303
|
-
showHeading && <h2>Heading</h2>,
|
|
2304
|
-
<p>First paragraph</p>,
|
|
2305
|
-
'First text',
|
|
2306
|
-
<span>First span</span>,
|
|
2307
|
-
<p>Second paragraph</p>,
|
|
2308
|
-
'Second text',
|
|
2309
|
-
<span>Second span</span>,
|
|
2310
|
-
]
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
render(<App />, root)
|
|
2314
|
-
const initialNodes = [...root.childNodes]
|
|
2315
|
-
hideHeading()
|
|
2316
|
-
await Promise.resolve()
|
|
2317
|
-
|
|
2318
|
-
expect(root.childNodes).toHaveLength(initialNodes.length - 1)
|
|
2319
|
-
initialNodes.slice(1).forEach((node, i) => {
|
|
2320
|
-
expect(root.childNodes[i]).toBe(node)
|
|
2321
|
-
})
|
|
2322
|
-
expect(initialNodes[0].isConnected).toBe(false)
|
|
2323
|
-
})
|
|
2324
|
-
|
|
2325
|
-
it('resumes unkeyed matching after a keyed sibling consumes the next candidate', async () => {
|
|
2326
|
-
let reorder: () => void = () => {}
|
|
2327
|
-
const App = () => {
|
|
2328
|
-
const [updated, setUpdated] = useState(false)
|
|
2329
|
-
reorder = () => setUpdated(true)
|
|
2330
|
-
return updated
|
|
2331
|
-
? [<p>A</p>, <p key='b'>B</p>, <p>C</p>, <p>D</p>, <span>X</span>, <span>Y</span>]
|
|
2332
|
-
: [<h2>Heading</h2>, <p key='a'>A</p>, <p key='b'>B</p>, <p>C</p>]
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
render(<App />, root)
|
|
2336
|
-
const initialNodes = [...root.children]
|
|
2337
|
-
reorder()
|
|
2338
|
-
await Promise.resolve()
|
|
2339
|
-
|
|
2340
|
-
expect(root.innerHTML).toBe('<p>A</p><p>B</p><p>C</p><p>D</p><span>X</span><span>Y</span>')
|
|
2341
|
-
initialNodes.slice(1).forEach((node, i) => {
|
|
2342
|
-
expect(root.children[i]).toBe(node)
|
|
2343
|
-
})
|
|
2344
|
-
expect(initialNodes[0].isConnected).toBe(false)
|
|
2345
|
-
})
|
|
2346
|
-
|
|
2347
|
-
it('does not reuse children with NaN keys', async () => {
|
|
2348
|
-
let rerender: () => void = () => {}
|
|
2349
|
-
const App = () => {
|
|
2350
|
-
const [count, setCount] = useState(0)
|
|
2351
|
-
rerender = () => setCount((count) => count + 1)
|
|
2352
|
-
return <div key={Number.NaN}>{count}</div>
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
render(<App />, root)
|
|
2356
|
-
const initialChild = root.firstElementChild
|
|
2357
|
-
rerender()
|
|
2358
|
-
await Promise.resolve()
|
|
2359
|
-
|
|
2360
|
-
expect(root.textContent).toBe('1')
|
|
2361
|
-
expect(root.firstElementChild).not.toBe(initialChild)
|
|
2362
|
-
})
|
|
2363
|
-
|
|
2364
|
-
it('swap deferent type of child component', async () => {
|
|
2365
|
-
const Even = () => <p>Even</p>
|
|
2366
|
-
const Odd = () => <div>Odd</div>
|
|
2367
|
-
const Counter = () => {
|
|
2368
|
-
const [count, setCount] = useState(0)
|
|
2369
|
-
return (
|
|
2370
|
-
<div>
|
|
2371
|
-
{count % 2 === 0 ? (
|
|
2372
|
-
<>
|
|
2373
|
-
<Even />
|
|
2374
|
-
<Odd />
|
|
2375
|
-
</>
|
|
2376
|
-
) : (
|
|
2377
|
-
<>
|
|
2378
|
-
<Odd />
|
|
2379
|
-
<Even />
|
|
2380
|
-
</>
|
|
2381
|
-
)}
|
|
2382
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
2383
|
-
</div>
|
|
2384
|
-
)
|
|
2385
|
-
}
|
|
2386
|
-
const app = <Counter />
|
|
2387
|
-
render(app, root)
|
|
2388
|
-
expect(root.innerHTML).toBe('<div><p>Even</p><div>Odd</div><button>+</button></div>')
|
|
2389
|
-
const button = root.querySelector('button') as HTMLButtonElement
|
|
2390
|
-
|
|
2391
|
-
const createElementSpy = vi.spyOn(dom.window.document, 'createElement')
|
|
2392
|
-
|
|
2393
|
-
button.click()
|
|
2394
|
-
await Promise.resolve()
|
|
2395
|
-
expect(root.innerHTML).toBe('<div><div>Odd</div><p>Even</p><button>+</button></div>')
|
|
2396
|
-
button.click()
|
|
2397
|
-
await Promise.resolve()
|
|
2398
|
-
expect(root.innerHTML).toBe('<div><p>Even</p><div>Odd</div><button>+</button></div>')
|
|
2399
|
-
|
|
2400
|
-
expect(createElementSpy).not.toHaveBeenCalled()
|
|
2401
|
-
})
|
|
2402
|
-
|
|
2403
|
-
it('useState for unnamed function', async () => {
|
|
2404
|
-
const Input = ({ label, onInput }: { label: string; onInput: (value: string) => void }) => {
|
|
2405
|
-
return (
|
|
2406
|
-
<div>
|
|
2407
|
-
<label>{label}</label>
|
|
2408
|
-
<input
|
|
2409
|
-
onInput={(e: InputEvent) => onInput((e.target as HTMLInputElement)?.value || '')}
|
|
2410
|
-
/>
|
|
2411
|
-
</div>
|
|
2412
|
-
)
|
|
2413
|
-
}
|
|
2414
|
-
const Form = () => {
|
|
2415
|
-
const [values, setValues] = useState<{ [key: string]: string }>({})
|
|
2416
|
-
return (
|
|
2417
|
-
<form>
|
|
2418
|
-
<Input label='Name' onInput={(value) => setValues({ ...values, name: value })} />
|
|
2419
|
-
<Input label='Email' onInput={(value) => setValues({ ...values, email: value })} />
|
|
2420
|
-
<span>{JSON.stringify(values)}</span>
|
|
2421
|
-
</form>
|
|
2422
|
-
)
|
|
2423
|
-
}
|
|
2424
|
-
const app = <Form />
|
|
2425
|
-
render(app, root)
|
|
2426
|
-
expect(root.innerHTML).toBe(
|
|
2427
|
-
'<form><div><label>Name</label><input></div><div><label>Email</label><input></div><span>{}</span></form>'
|
|
2428
|
-
)
|
|
2429
|
-
const [nameInput] = root.querySelectorAll('input')
|
|
2430
|
-
nameInput.value = 'John'
|
|
2431
|
-
nameInput.dispatchEvent(new dom.window.Event('input'))
|
|
2432
|
-
await Promise.resolve()
|
|
2433
|
-
expect(root.innerHTML).toBe(
|
|
2434
|
-
'<form><div><label>Name</label><input></div><div><label>Email</label><input></div><span>{"name":"John"}</span></form>'
|
|
2435
|
-
)
|
|
2436
|
-
const [, emailInput] = root.querySelectorAll('input')
|
|
2437
|
-
emailInput.value = 'john@example.com'
|
|
2438
|
-
emailInput.dispatchEvent(new dom.window.Event('input'))
|
|
2439
|
-
await Promise.resolve()
|
|
2440
|
-
expect(root.innerHTML).toBe(
|
|
2441
|
-
'<form><div><label>Name</label><input></div><div><label>Email</label><input></div><span>{"name":"John","email":"john@example.com"}</span></form>'
|
|
2442
|
-
)
|
|
2443
|
-
})
|
|
2444
|
-
|
|
2445
|
-
it('useState for grand child function', async () => {
|
|
2446
|
-
const GrandChild = () => {
|
|
2447
|
-
const [count, setCount] = useState(0)
|
|
2448
|
-
return (
|
|
2449
|
-
<>
|
|
2450
|
-
{count === 0 ? <p>Zero</p> : <span>Not Zero</span>}
|
|
2451
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
2452
|
-
</>
|
|
2453
|
-
)
|
|
2454
|
-
}
|
|
2455
|
-
const Child = () => {
|
|
2456
|
-
return <GrandChild />
|
|
2457
|
-
}
|
|
2458
|
-
const App = () => {
|
|
2459
|
-
const [show, setShow] = useState(false)
|
|
2460
|
-
return (
|
|
2461
|
-
<div>
|
|
2462
|
-
{show && <Child />}
|
|
2463
|
-
<button onClick={() => setShow(!show)}>toggle</button>
|
|
2464
|
-
</div>
|
|
2465
|
-
)
|
|
2466
|
-
}
|
|
2467
|
-
render(<App />, root)
|
|
2468
|
-
expect(root.innerHTML).toBe('<div><button>toggle</button></div>')
|
|
2469
|
-
root.querySelector('button')?.click()
|
|
2470
|
-
await Promise.resolve()
|
|
2471
|
-
expect(root.innerHTML).toBe('<div><p>Zero</p><button>+</button><button>toggle</button></div>')
|
|
2472
|
-
root.querySelector('button')?.click()
|
|
2473
|
-
await Promise.resolve()
|
|
2474
|
-
expect(root.innerHTML).toBe(
|
|
2475
|
-
'<div><span>Not Zero</span><button>+</button><button>toggle</button></div>'
|
|
2476
|
-
)
|
|
2477
|
-
})
|
|
2478
|
-
|
|
2479
|
-
describe('className', () => {
|
|
2480
|
-
it('should convert to class attribute for intrinsic elements', () => {
|
|
2481
|
-
const App = <h1 className='h1'>Hello</h1>
|
|
2482
|
-
render(App, root)
|
|
2483
|
-
expect(root.innerHTML).toBe('<h1 class="h1">Hello</h1>')
|
|
2484
|
-
})
|
|
2485
|
-
|
|
2486
|
-
it('should convert to class attribute for custom elements', () => {
|
|
2487
|
-
const App = <custom-element className='h1'>Hello</custom-element>
|
|
2488
|
-
render(App, root)
|
|
2489
|
-
expect(root.innerHTML).toBe('<custom-element class="h1">Hello</custom-element>')
|
|
2490
|
-
})
|
|
2491
|
-
|
|
2492
|
-
it('should not convert to class attribute for custom components', () => {
|
|
2493
|
-
const App: FC<{ className: string }> = ({ className }) => (
|
|
2494
|
-
<div data-class-name={className}>Hello</div>
|
|
2495
|
-
)
|
|
2496
|
-
render(<App className='h1' />, root)
|
|
2497
|
-
expect(root.innerHTML).toBe('<div data-class-name="h1">Hello</div>')
|
|
2498
|
-
})
|
|
2499
|
-
})
|
|
2500
|
-
|
|
2501
|
-
describe('memo', () => {
|
|
2502
|
-
it('simple', async () => {
|
|
2503
|
-
let renderCount = 0
|
|
2504
|
-
const Counter = ({ count }: { count: number }) => {
|
|
2505
|
-
renderCount++
|
|
2506
|
-
return (
|
|
2507
|
-
<div>
|
|
2508
|
-
<p>Count: {count}</p>
|
|
2509
|
-
</div>
|
|
2510
|
-
)
|
|
2511
|
-
}
|
|
2512
|
-
const MemoCounter = memo(Counter)
|
|
2513
|
-
const App = () => {
|
|
2514
|
-
const [count, setCount] = useState(0)
|
|
2515
|
-
return (
|
|
2516
|
-
<div>
|
|
2517
|
-
<MemoCounter count={Math.min(count, 1)} />
|
|
2518
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
2519
|
-
</div>
|
|
2520
|
-
)
|
|
2521
|
-
}
|
|
2522
|
-
const app = <App />
|
|
2523
|
-
render(app, root)
|
|
2524
|
-
expect(root.innerHTML).toBe('<div><div><p>Count: 0</p></div><button>+</button></div>')
|
|
2525
|
-
expect(renderCount).toBe(1)
|
|
2526
|
-
root.querySelector('button')?.click()
|
|
2527
|
-
await Promise.resolve()
|
|
2528
|
-
expect(root.innerHTML).toBe('<div><div><p>Count: 1</p></div><button>+</button></div>')
|
|
2529
|
-
expect(renderCount).toBe(2)
|
|
2530
|
-
root.querySelector('button')?.click()
|
|
2531
|
-
await Promise.resolve()
|
|
2532
|
-
expect(root.innerHTML).toBe('<div><div><p>Count: 1</p></div><button>+</button></div>')
|
|
2533
|
-
expect(renderCount).toBe(2)
|
|
2534
|
-
})
|
|
2535
|
-
|
|
2536
|
-
it('useState', async () => {
|
|
2537
|
-
const Child = vi.fn(({ count }: { count: number }) => {
|
|
2538
|
-
const [count2, setCount2] = useState(0)
|
|
2539
|
-
return (
|
|
2540
|
-
<>
|
|
2541
|
-
<div>
|
|
2542
|
-
{count} : {count2}
|
|
2543
|
-
</div>
|
|
2544
|
-
<button id='child-button' onClick={() => setCount2(count2 + 1)}>
|
|
2545
|
-
Child +
|
|
2546
|
-
</button>
|
|
2547
|
-
</>
|
|
2548
|
-
)
|
|
2549
|
-
})
|
|
2550
|
-
const MemoChild = memo(Child)
|
|
2551
|
-
const App = () => {
|
|
2552
|
-
const [count, setCount] = useState(0)
|
|
2553
|
-
return (
|
|
2554
|
-
<>
|
|
2555
|
-
<button id='app-button' onClick={() => setCount(count + 1)}>
|
|
2556
|
-
App +
|
|
2557
|
-
</button>
|
|
2558
|
-
<MemoChild count={Math.floor(count / 2)} />
|
|
2559
|
-
</>
|
|
2560
|
-
)
|
|
2561
|
-
}
|
|
2562
|
-
render(<App />, root)
|
|
2563
|
-
expect(root.innerHTML).toBe(
|
|
2564
|
-
'<button id="app-button">App +</button><div>0 : 0</div><button id="child-button">Child +</button>'
|
|
2565
|
-
)
|
|
2566
|
-
root.querySelector<HTMLButtonElement>('button#app-button')?.click()
|
|
2567
|
-
await Promise.resolve()
|
|
2568
|
-
expect(Child).toBeCalledTimes(1)
|
|
2569
|
-
expect(root.innerHTML).toBe(
|
|
2570
|
-
'<button id="app-button">App +</button><div>0 : 0</div><button id="child-button">Child +</button>'
|
|
2571
|
-
)
|
|
2572
|
-
root.querySelector<HTMLButtonElement>('button#app-button')?.click()
|
|
2573
|
-
await Promise.resolve()
|
|
2574
|
-
expect(Child).toBeCalledTimes(2)
|
|
2575
|
-
expect(root.innerHTML).toBe(
|
|
2576
|
-
'<button id="app-button">App +</button><div>1 : 0</div><button id="child-button">Child +</button>'
|
|
2577
|
-
)
|
|
2578
|
-
root.querySelector<HTMLButtonElement>('button#child-button')?.click()
|
|
2579
|
-
await Promise.resolve()
|
|
2580
|
-
expect(Child).toBeCalledTimes(3)
|
|
2581
|
-
expect(root.innerHTML).toBe(
|
|
2582
|
-
'<button id="app-button">App +</button><div>1 : 1</div><button id="child-button">Child +</button>'
|
|
2583
|
-
)
|
|
2584
|
-
})
|
|
2585
|
-
|
|
2586
|
-
// The react compiler generates code like the following for memoization.
|
|
2587
|
-
it('react compiler', async () => {
|
|
2588
|
-
let renderCount = 0
|
|
2589
|
-
const Counter = ({ count }: { count: number }) => {
|
|
2590
|
-
renderCount++
|
|
2591
|
-
return (
|
|
2592
|
-
<div>
|
|
2593
|
-
<p>Count: {count}</p>
|
|
2594
|
-
</div>
|
|
2595
|
-
)
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
const App = () => {
|
|
2599
|
-
const [cache] = useState<unknown[]>(() => [])
|
|
2600
|
-
const [count, setCount] = useState(0)
|
|
2601
|
-
const countForDisplay = Math.floor(count / 2)
|
|
2602
|
-
|
|
2603
|
-
let localCounter
|
|
2604
|
-
if (cache[0] !== countForDisplay) {
|
|
2605
|
-
localCounter = <Counter count={countForDisplay} />
|
|
2606
|
-
cache[0] = countForDisplay
|
|
2607
|
-
cache[1] = localCounter
|
|
2608
|
-
} else {
|
|
2609
|
-
localCounter = cache[1]
|
|
2610
|
-
}
|
|
2611
|
-
|
|
2612
|
-
return (
|
|
2613
|
-
<div>
|
|
2614
|
-
{localCounter}
|
|
2615
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
2616
|
-
</div>
|
|
2617
|
-
)
|
|
2618
|
-
}
|
|
2619
|
-
const app = <App />
|
|
2620
|
-
render(app, root)
|
|
2621
|
-
expect(root.innerHTML).toBe('<div><div><p>Count: 0</p></div><button>+</button></div>')
|
|
2622
|
-
expect(renderCount).toBe(1)
|
|
2623
|
-
root.querySelector('button')?.click()
|
|
2624
|
-
await Promise.resolve()
|
|
2625
|
-
expect(root.innerHTML).toBe('<div><div><p>Count: 0</p></div><button>+</button></div>')
|
|
2626
|
-
expect(renderCount).toBe(1)
|
|
2627
|
-
root.querySelector('button')?.click()
|
|
2628
|
-
await Promise.resolve()
|
|
2629
|
-
expect(root.innerHTML).toBe('<div><div><p>Count: 1</p></div><button>+</button></div>')
|
|
2630
|
-
expect(renderCount).toBe(2)
|
|
2631
|
-
})
|
|
2632
|
-
|
|
2633
|
-
it('should not return memoized result when context is not changed', async () => {
|
|
2634
|
-
const Context = createContext<[number, (arg: number | ((value: number) => number)) => void]>([
|
|
2635
|
-
0,
|
|
2636
|
-
() => {},
|
|
2637
|
-
])
|
|
2638
|
-
const Container: FC<{ children: Child }> = ({ children }) => {
|
|
2639
|
-
const [count, setCount] = useState(0)
|
|
2640
|
-
return <Context.Provider value={[count, setCount]}>{children}</Context.Provider>
|
|
2641
|
-
}
|
|
2642
|
-
const Content = () => {
|
|
2643
|
-
const [count, setCount] = useContext(Context)
|
|
2644
|
-
return (
|
|
2645
|
-
<>
|
|
2646
|
-
<span>{count}</span>
|
|
2647
|
-
<button onClick={() => setCount((c) => c + 1)}>+</button>
|
|
2648
|
-
</>
|
|
2649
|
-
)
|
|
2650
|
-
}
|
|
2651
|
-
const app = (
|
|
2652
|
-
<Container>
|
|
2653
|
-
<Content />
|
|
2654
|
-
</Container>
|
|
2655
|
-
)
|
|
2656
|
-
render(app, root)
|
|
2657
|
-
expect(root.innerHTML).toBe('<span>0</span><button>+</button>')
|
|
2658
|
-
root.querySelector('button')?.click()
|
|
2659
|
-
await Promise.resolve()
|
|
2660
|
-
expect(root.innerHTML).toBe('<span>1</span><button>+</button>')
|
|
2661
|
-
})
|
|
2662
|
-
})
|
|
2663
|
-
|
|
2664
|
-
describe('useRef', async () => {
|
|
2665
|
-
it('types: non-null initial value returns RefObject<T>', () => {
|
|
2666
|
-
const ref = useRef(new Map<string, number>())
|
|
2667
|
-
// .current is non-nullable; this line would not type-check without the overload
|
|
2668
|
-
ref.current.set('a', 1)
|
|
2669
|
-
expect(ref.current.get('a')).toBe(1)
|
|
2670
|
-
})
|
|
2671
|
-
|
|
2672
|
-
it('types: null initial value returns RefObject<T | null>', () => {
|
|
2673
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
2674
|
-
expect(ref.current).toBeNull()
|
|
2675
|
-
})
|
|
2676
|
-
|
|
2677
|
-
it('types: undefined initial value returns RefObject<T | undefined>', () => {
|
|
2678
|
-
const ref = useRef<number>(undefined)
|
|
2679
|
-
expect(ref.current).toBeUndefined()
|
|
2680
|
-
ref.current = 1
|
|
2681
|
-
expect(ref.current).toBe(1)
|
|
2682
|
-
})
|
|
2683
|
-
|
|
2684
|
-
it('simple', async () => {
|
|
2685
|
-
const Input = ({
|
|
2686
|
-
label,
|
|
2687
|
-
ref,
|
|
2688
|
-
}: {
|
|
2689
|
-
label: string
|
|
2690
|
-
ref: RefObject<HTMLInputElement | null>
|
|
2691
|
-
}) => {
|
|
2692
|
-
return (
|
|
2693
|
-
<div>
|
|
2694
|
-
<label>{label}</label>
|
|
2695
|
-
<input ref={ref} />
|
|
2696
|
-
</div>
|
|
2697
|
-
)
|
|
2698
|
-
}
|
|
2699
|
-
const Form = () => {
|
|
2700
|
-
const [values, setValues] = useState<{ [key: string]: string }>({})
|
|
2701
|
-
const nameRef = useRef<HTMLInputElement>(null)
|
|
2702
|
-
const emailRef = useRef<HTMLInputElement>(null)
|
|
2703
|
-
return (
|
|
2704
|
-
<form>
|
|
2705
|
-
<Input label='Name' ref={nameRef} />
|
|
2706
|
-
<Input label='Email' ref={emailRef} />
|
|
2707
|
-
<button
|
|
2708
|
-
onClick={(ev: Event) => {
|
|
2709
|
-
ev.preventDefault()
|
|
2710
|
-
setValues({
|
|
2711
|
-
name: nameRef.current?.value || '',
|
|
2712
|
-
email: emailRef.current?.value || '',
|
|
2713
|
-
})
|
|
2714
|
-
}}
|
|
2715
|
-
>
|
|
2716
|
-
serialize
|
|
2717
|
-
</button>
|
|
2718
|
-
<span>{JSON.stringify(values)}</span>
|
|
2719
|
-
</form>
|
|
2720
|
-
)
|
|
2721
|
-
}
|
|
2722
|
-
const app = <Form />
|
|
2723
|
-
render(app, root)
|
|
2724
|
-
expect(root.innerHTML).toBe(
|
|
2725
|
-
'<form><div><label>Name</label><input></div><div><label>Email</label><input></div><button>serialize</button><span>{}</span></form>'
|
|
2726
|
-
)
|
|
2727
|
-
const [nameInput, emailInput] = root.querySelectorAll('input')
|
|
2728
|
-
nameInput.value = 'John'
|
|
2729
|
-
emailInput.value = 'john@example.com'
|
|
2730
|
-
const [button] = root.querySelectorAll('button')
|
|
2731
|
-
button.click()
|
|
2732
|
-
await Promise.resolve()
|
|
2733
|
-
expect(root.innerHTML).toBe(
|
|
2734
|
-
'<form><div><label>Name</label><input></div><div><label>Email</label><input></div><button>serialize</button><span>{"name":"John","email":"john@example.com"}</span></form>'
|
|
2735
|
-
)
|
|
2736
|
-
})
|
|
2737
|
-
|
|
2738
|
-
it('update current', async () => {
|
|
2739
|
-
const App = () => {
|
|
2740
|
-
const [, setState] = useState(0)
|
|
2741
|
-
const ref = useRef<boolean>(false)
|
|
2742
|
-
return (
|
|
2743
|
-
<>
|
|
2744
|
-
<button
|
|
2745
|
-
onClick={() => {
|
|
2746
|
-
setState((c) => c + 1)
|
|
2747
|
-
ref.current = true
|
|
2748
|
-
}}
|
|
2749
|
-
>
|
|
2750
|
-
update
|
|
2751
|
-
</button>
|
|
2752
|
-
<span>{String(ref.current)}</span>
|
|
2753
|
-
</>
|
|
2754
|
-
)
|
|
2755
|
-
}
|
|
2756
|
-
const app = <App />
|
|
2757
|
-
render(app, root)
|
|
2758
|
-
expect(root.innerHTML).toBe('<button>update</button><span>false</span>')
|
|
2759
|
-
root.querySelector('button')?.click()
|
|
2760
|
-
await Promise.resolve()
|
|
2761
|
-
expect(root.innerHTML).toBe('<button>update</button><span>true</span>')
|
|
2762
|
-
})
|
|
2763
|
-
})
|
|
2764
|
-
|
|
2765
|
-
describe('useEffect', () => {
|
|
2766
|
-
it('simple', async () => {
|
|
2767
|
-
const Counter = () => {
|
|
2768
|
-
const [count, setCount] = useState(0)
|
|
2769
|
-
useEffect(() => {
|
|
2770
|
-
setCount(count + 1)
|
|
2771
|
-
}, [])
|
|
2772
|
-
return <div>{count}</div>
|
|
2773
|
-
}
|
|
2774
|
-
const app = <Counter />
|
|
2775
|
-
render(app, root)
|
|
2776
|
-
await new Promise((resolve) => setTimeout(resolve))
|
|
2777
|
-
await Promise.resolve()
|
|
2778
|
-
expect(root.innerHTML).toBe('<div>1</div>')
|
|
2779
|
-
})
|
|
2780
|
-
|
|
2781
|
-
it('multiple', async () => {
|
|
2782
|
-
const Counter = () => {
|
|
2783
|
-
const [count, setCount] = useState(0)
|
|
2784
|
-
useEffect(() => {
|
|
2785
|
-
setCount((c) => c + 1)
|
|
2786
|
-
}, [])
|
|
2787
|
-
useEffect(() => {
|
|
2788
|
-
setCount((c) => c + 1)
|
|
2789
|
-
}, [])
|
|
2790
|
-
return <div>{count}</div>
|
|
2791
|
-
}
|
|
2792
|
-
const app = <Counter />
|
|
2793
|
-
render(app, root)
|
|
2794
|
-
await new Promise((resolve) => setTimeout(resolve))
|
|
2795
|
-
await Promise.resolve()
|
|
2796
|
-
expect(root.innerHTML).toBe('<div>2</div>')
|
|
2797
|
-
})
|
|
2798
|
-
|
|
2799
|
-
it('cleanup', async () => {
|
|
2800
|
-
const Child = ({ parent }: { parent: RefObject<HTMLElement | null> }) => {
|
|
2801
|
-
useEffect(() => {
|
|
2802
|
-
return () => {
|
|
2803
|
-
parent.current?.setAttribute('data-cleanup', 'true')
|
|
2804
|
-
}
|
|
2805
|
-
}, [])
|
|
2806
|
-
return <div>Child</div>
|
|
2807
|
-
}
|
|
2808
|
-
const Parent = () => {
|
|
2809
|
-
const [show, setShow] = useState(true)
|
|
2810
|
-
const ref = useRef<HTMLElement>(null)
|
|
2811
|
-
return (
|
|
2812
|
-
<div ref={ref}>
|
|
2813
|
-
{show && <Child parent={ref} />}
|
|
2814
|
-
<button onClick={() => setShow(false)}>hide</button>
|
|
2815
|
-
</div>
|
|
2816
|
-
)
|
|
2817
|
-
}
|
|
2818
|
-
const app = <Parent />
|
|
2819
|
-
render(app, root)
|
|
2820
|
-
expect(root.innerHTML).toBe('<div><div>Child</div><button>hide</button></div>')
|
|
2821
|
-
await new Promise((resolve) => setTimeout(resolve))
|
|
2822
|
-
const [button] = root.querySelectorAll('button')
|
|
2823
|
-
button.click()
|
|
2824
|
-
await Promise.resolve()
|
|
2825
|
-
expect(root.innerHTML).toBe('<div data-cleanup="true"><button>hide</button></div>')
|
|
2826
|
-
})
|
|
2827
|
-
|
|
2828
|
-
it('cleanup for deps', async () => {
|
|
2829
|
-
let effectCount = 0
|
|
2830
|
-
let cleanupCount = 0
|
|
2831
|
-
|
|
2832
|
-
const App = () => {
|
|
2833
|
-
const [count, setCount] = useState(0)
|
|
2834
|
-
const [count2, setCount2] = useState(0)
|
|
2835
|
-
useEffect(() => {
|
|
2836
|
-
effectCount++
|
|
2837
|
-
return () => {
|
|
2838
|
-
cleanupCount++
|
|
2839
|
-
}
|
|
2840
|
-
}, [count])
|
|
2841
|
-
return (
|
|
2842
|
-
<div>
|
|
2843
|
-
<p>{count}</p>
|
|
2844
|
-
<p>{count2}</p>
|
|
2845
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
2846
|
-
<button onClick={() => setCount2(count2 + 1)}>+</button>
|
|
2847
|
-
</div>
|
|
2848
|
-
)
|
|
2849
|
-
}
|
|
2850
|
-
const app = <App />
|
|
2851
|
-
render(app, root)
|
|
2852
|
-
await new Promise((resolve) => setTimeout(resolve))
|
|
2853
|
-
expect(effectCount).toBe(1)
|
|
2854
|
-
expect(cleanupCount).toBe(0)
|
|
2855
|
-
root.querySelectorAll('button')[0].click() // count++
|
|
2856
|
-
await Promise.resolve()
|
|
2857
|
-
await new Promise((resolve) => setTimeout(resolve))
|
|
2858
|
-
expect(effectCount).toBe(2)
|
|
2859
|
-
expect(cleanupCount).toBe(1)
|
|
2860
|
-
root.querySelectorAll('button')[1].click() // count2++
|
|
2861
|
-
await Promise.resolve()
|
|
2862
|
-
expect(effectCount).toBe(2)
|
|
2863
|
-
expect(cleanupCount).toBe(1)
|
|
2864
|
-
})
|
|
2865
|
-
})
|
|
2866
|
-
|
|
2867
|
-
describe('useLayoutEffect', () => {
|
|
2868
|
-
it('simple', async () => {
|
|
2869
|
-
const Counter = () => {
|
|
2870
|
-
const [count, setCount] = useState(0)
|
|
2871
|
-
useLayoutEffect(() => {
|
|
2872
|
-
setCount(count + 1)
|
|
2873
|
-
}, [])
|
|
2874
|
-
return <div>{count}</div>
|
|
2875
|
-
}
|
|
2876
|
-
const app = <Counter />
|
|
2877
|
-
render(app, root)
|
|
2878
|
-
await Promise.resolve()
|
|
2879
|
-
expect(root.innerHTML).toBe('<div>1</div>')
|
|
2880
|
-
})
|
|
2881
|
-
|
|
2882
|
-
it('multiple', async () => {
|
|
2883
|
-
const Counter = () => {
|
|
2884
|
-
const [count, setCount] = useState(0)
|
|
2885
|
-
useLayoutEffect(() => {
|
|
2886
|
-
setCount((c) => c + 1)
|
|
2887
|
-
}, [])
|
|
2888
|
-
useLayoutEffect(() => {
|
|
2889
|
-
setCount((c) => c + 1)
|
|
2890
|
-
}, [])
|
|
2891
|
-
return <div>{count}</div>
|
|
2892
|
-
}
|
|
2893
|
-
const app = <Counter />
|
|
2894
|
-
render(app, root)
|
|
2895
|
-
await Promise.resolve()
|
|
2896
|
-
expect(root.innerHTML).toBe('<div>2</div>')
|
|
2897
|
-
})
|
|
2898
|
-
|
|
2899
|
-
it('cleanup', async () => {
|
|
2900
|
-
const Child = ({ parent }: { parent: RefObject<HTMLElement | null> }) => {
|
|
2901
|
-
useLayoutEffect(() => {
|
|
2902
|
-
return () => {
|
|
2903
|
-
parent.current?.setAttribute('data-cleanup', 'true')
|
|
2904
|
-
}
|
|
2905
|
-
}, [])
|
|
2906
|
-
return <div>Child</div>
|
|
2907
|
-
}
|
|
2908
|
-
const Parent = () => {
|
|
2909
|
-
const [show, setShow] = useState(true)
|
|
2910
|
-
const ref = useRef<HTMLElement>(null)
|
|
2911
|
-
return (
|
|
2912
|
-
<div ref={ref}>
|
|
2913
|
-
{show && <Child parent={ref} />}
|
|
2914
|
-
<button onClick={() => setShow(false)}>hide</button>
|
|
2915
|
-
</div>
|
|
2916
|
-
)
|
|
2917
|
-
}
|
|
2918
|
-
const app = <Parent />
|
|
2919
|
-
render(app, root)
|
|
2920
|
-
expect(root.innerHTML).toBe('<div><div>Child</div><button>hide</button></div>')
|
|
2921
|
-
const [button] = root.querySelectorAll('button')
|
|
2922
|
-
button.click()
|
|
2923
|
-
await Promise.resolve()
|
|
2924
|
-
expect(root.innerHTML).toBe('<div data-cleanup="true"><button>hide</button></div>')
|
|
2925
|
-
})
|
|
2926
|
-
|
|
2927
|
-
it('cleanup for deps', async () => {
|
|
2928
|
-
let effectCount = 0
|
|
2929
|
-
let cleanupCount = 0
|
|
2930
|
-
|
|
2931
|
-
const App = () => {
|
|
2932
|
-
const [count, setCount] = useState(0)
|
|
2933
|
-
const [count2, setCount2] = useState(0)
|
|
2934
|
-
useLayoutEffect(() => {
|
|
2935
|
-
effectCount++
|
|
2936
|
-
return () => {
|
|
2937
|
-
cleanupCount++
|
|
2938
|
-
}
|
|
2939
|
-
}, [count])
|
|
2940
|
-
return (
|
|
2941
|
-
<div>
|
|
2942
|
-
<p>{count}</p>
|
|
2943
|
-
<p>{count2}</p>
|
|
2944
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
2945
|
-
<button onClick={() => setCount2(count2 + 1)}>+</button>
|
|
2946
|
-
</div>
|
|
2947
|
-
)
|
|
2948
|
-
}
|
|
2949
|
-
const app = <App />
|
|
2950
|
-
render(app, root)
|
|
2951
|
-
expect(effectCount).toBe(1)
|
|
2952
|
-
expect(cleanupCount).toBe(0)
|
|
2953
|
-
root.querySelectorAll('button')[0].click() // count++
|
|
2954
|
-
await Promise.resolve()
|
|
2955
|
-
expect(effectCount).toBe(2)
|
|
2956
|
-
expect(cleanupCount).toBe(1)
|
|
2957
|
-
root.querySelectorAll('button')[1].click() // count2++
|
|
2958
|
-
await Promise.resolve()
|
|
2959
|
-
expect(effectCount).toBe(2)
|
|
2960
|
-
expect(cleanupCount).toBe(1)
|
|
2961
|
-
})
|
|
2962
|
-
})
|
|
2963
|
-
|
|
2964
|
-
describe('useInsertionEffect', () => {
|
|
2965
|
-
it('simple', async () => {
|
|
2966
|
-
const Counter = () => {
|
|
2967
|
-
const [count, setCount] = useState(0)
|
|
2968
|
-
useInsertionEffect(() => {
|
|
2969
|
-
setCount(count + 1)
|
|
2970
|
-
}, [])
|
|
2971
|
-
return <div>{count}</div>
|
|
2972
|
-
}
|
|
2973
|
-
const app = <Counter />
|
|
2974
|
-
render(app, root)
|
|
2975
|
-
await Promise.resolve()
|
|
2976
|
-
expect(root.innerHTML).toBe('<div>1</div>')
|
|
2977
|
-
})
|
|
2978
|
-
|
|
2979
|
-
it('multiple', async () => {
|
|
2980
|
-
const Counter = () => {
|
|
2981
|
-
const [count, setCount] = useState(0)
|
|
2982
|
-
useInsertionEffect(() => {
|
|
2983
|
-
setCount((c) => c + 1)
|
|
2984
|
-
}, [])
|
|
2985
|
-
useInsertionEffect(() => {
|
|
2986
|
-
setCount((c) => c + 1)
|
|
2987
|
-
}, [])
|
|
2988
|
-
return <div>{count}</div>
|
|
2989
|
-
}
|
|
2990
|
-
const app = <Counter />
|
|
2991
|
-
render(app, root)
|
|
2992
|
-
await Promise.resolve()
|
|
2993
|
-
expect(root.innerHTML).toBe('<div>2</div>')
|
|
2994
|
-
})
|
|
2995
|
-
|
|
2996
|
-
it('with useLayoutEffect', async () => {
|
|
2997
|
-
const Counter = () => {
|
|
2998
|
-
const [data, setData] = useState<string[]>([])
|
|
2999
|
-
useLayoutEffect(() => {
|
|
3000
|
-
setData((d) => [...d, 'useLayoutEffect'])
|
|
3001
|
-
}, [])
|
|
3002
|
-
useInsertionEffect(() => {
|
|
3003
|
-
setData((d) => [...d, 'useInsertionEffect'])
|
|
3004
|
-
}, [])
|
|
3005
|
-
return <div>{data.join(',')}</div>
|
|
3006
|
-
}
|
|
3007
|
-
const app = <Counter />
|
|
3008
|
-
render(app, root)
|
|
3009
|
-
await Promise.resolve()
|
|
3010
|
-
expect(root.innerHTML).toBe('<div>useInsertionEffect,useLayoutEffect</div>')
|
|
3011
|
-
})
|
|
3012
|
-
|
|
3013
|
-
it('cleanup', async () => {
|
|
3014
|
-
const Child = ({ parent }: { parent: RefObject<HTMLElement | null> }) => {
|
|
3015
|
-
useInsertionEffect(() => {
|
|
3016
|
-
return () => {
|
|
3017
|
-
parent.current?.setAttribute('data-cleanup', 'true')
|
|
3018
|
-
}
|
|
3019
|
-
}, [])
|
|
3020
|
-
return <div>Child</div>
|
|
3021
|
-
}
|
|
3022
|
-
const Parent = () => {
|
|
3023
|
-
const [show, setShow] = useState(true)
|
|
3024
|
-
const ref = useRef<HTMLElement>(null)
|
|
3025
|
-
return (
|
|
3026
|
-
<div ref={ref}>
|
|
3027
|
-
{show && <Child parent={ref} />}
|
|
3028
|
-
<button onClick={() => setShow(false)}>hide</button>
|
|
3029
|
-
</div>
|
|
3030
|
-
)
|
|
3031
|
-
}
|
|
3032
|
-
const app = <Parent />
|
|
3033
|
-
render(app, root)
|
|
3034
|
-
expect(root.innerHTML).toBe('<div><div>Child</div><button>hide</button></div>')
|
|
3035
|
-
const [button] = root.querySelectorAll('button')
|
|
3036
|
-
button.click()
|
|
3037
|
-
await Promise.resolve()
|
|
3038
|
-
expect(root.innerHTML).toBe('<div data-cleanup="true"><button>hide</button></div>')
|
|
3039
|
-
})
|
|
3040
|
-
|
|
3041
|
-
it('cleanup for deps', async () => {
|
|
3042
|
-
let effectCount = 0
|
|
3043
|
-
let cleanupCount = 0
|
|
3044
|
-
|
|
3045
|
-
const App = () => {
|
|
3046
|
-
const [count, setCount] = useState(0)
|
|
3047
|
-
const [count2, setCount2] = useState(0)
|
|
3048
|
-
useInsertionEffect(() => {
|
|
3049
|
-
effectCount++
|
|
3050
|
-
return () => {
|
|
3051
|
-
cleanupCount++
|
|
3052
|
-
}
|
|
3053
|
-
}, [count])
|
|
3054
|
-
return (
|
|
3055
|
-
<div>
|
|
3056
|
-
<p>{count}</p>
|
|
3057
|
-
<p>{count2}</p>
|
|
3058
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
3059
|
-
<button onClick={() => setCount2(count2 + 1)}>+</button>
|
|
3060
|
-
</div>
|
|
3061
|
-
)
|
|
3062
|
-
}
|
|
3063
|
-
const app = <App />
|
|
3064
|
-
render(app, root)
|
|
3065
|
-
expect(effectCount).toBe(1)
|
|
3066
|
-
expect(cleanupCount).toBe(0)
|
|
3067
|
-
root.querySelectorAll('button')[0].click() // count++
|
|
3068
|
-
await Promise.resolve()
|
|
3069
|
-
expect(effectCount).toBe(2)
|
|
3070
|
-
expect(cleanupCount).toBe(1)
|
|
3071
|
-
root.querySelectorAll('button')[1].click() // count2++
|
|
3072
|
-
await Promise.resolve()
|
|
3073
|
-
expect(effectCount).toBe(2)
|
|
3074
|
-
expect(cleanupCount).toBe(1)
|
|
3075
|
-
})
|
|
3076
|
-
})
|
|
3077
|
-
|
|
3078
|
-
describe('useCallback', () => {
|
|
3079
|
-
it('deferent callbacks', async () => {
|
|
3080
|
-
const callbackSet = new Set<Function>()
|
|
3081
|
-
const Counter = () => {
|
|
3082
|
-
const [count, setCount] = useState(0)
|
|
3083
|
-
const increment = useCallback(() => {
|
|
3084
|
-
setCount(count + 1)
|
|
3085
|
-
}, [count])
|
|
3086
|
-
callbackSet.add(increment)
|
|
3087
|
-
return (
|
|
3088
|
-
<div>
|
|
3089
|
-
<p>{count}</p>
|
|
3090
|
-
<button onClick={increment}>+</button>
|
|
3091
|
-
</div>
|
|
3092
|
-
)
|
|
3093
|
-
}
|
|
3094
|
-
const app = <Counter />
|
|
3095
|
-
render(app, root)
|
|
3096
|
-
expect(root.innerHTML).toBe('<div><p>0</p><button>+</button></div>')
|
|
3097
|
-
const button = root.querySelector('button') as HTMLButtonElement
|
|
3098
|
-
button.click()
|
|
3099
|
-
await Promise.resolve()
|
|
3100
|
-
expect(root.innerHTML).toBe('<div><p>1</p><button>+</button></div>')
|
|
3101
|
-
expect(callbackSet.size).toBe(2)
|
|
3102
|
-
})
|
|
3103
|
-
|
|
3104
|
-
it('same callback', async () => {
|
|
3105
|
-
const callbackSet = new Set<Function>()
|
|
3106
|
-
const Counter = () => {
|
|
3107
|
-
const [count, setCount] = useState(0)
|
|
3108
|
-
const increment = useCallback(() => {
|
|
3109
|
-
setCount(count + 1)
|
|
3110
|
-
}, [count])
|
|
3111
|
-
callbackSet.add(increment)
|
|
3112
|
-
|
|
3113
|
-
const [count2, setCount2] = useState(0)
|
|
3114
|
-
return (
|
|
3115
|
-
<div>
|
|
3116
|
-
<p>{count}</p>
|
|
3117
|
-
<button onClick={increment}>+</button>
|
|
3118
|
-
<p>{count2}</p>
|
|
3119
|
-
<button onClick={() => setCount2(count2 + 1)}>+</button>
|
|
3120
|
-
</div>
|
|
3121
|
-
)
|
|
3122
|
-
}
|
|
3123
|
-
const app = <Counter />
|
|
3124
|
-
render(app, root)
|
|
3125
|
-
expect(root.innerHTML).toBe('<div><p>0</p><button>+</button><p>0</p><button>+</button></div>')
|
|
3126
|
-
const [, button] = root.querySelectorAll('button')
|
|
3127
|
-
button.click()
|
|
3128
|
-
await Promise.resolve()
|
|
3129
|
-
expect(root.innerHTML).toBe('<div><p>0</p><button>+</button><p>1</p><button>+</button></div>')
|
|
3130
|
-
expect(callbackSet.size).toBe(1)
|
|
3131
|
-
})
|
|
3132
|
-
|
|
3133
|
-
it('deferent callbacks', async () => {
|
|
3134
|
-
const callbackSet = new Set<Function>()
|
|
3135
|
-
const Counter = () => {
|
|
3136
|
-
const [count, setCount] = useState(0)
|
|
3137
|
-
const double = useCallback((input: number): number => {
|
|
3138
|
-
return input * 2
|
|
3139
|
-
}, [])
|
|
3140
|
-
callbackSet.add(double)
|
|
3141
|
-
return (
|
|
3142
|
-
<div>
|
|
3143
|
-
<p>{double(count)}</p>
|
|
3144
|
-
<button onClick={() => setCount((c) => c + 1)}>+</button>
|
|
3145
|
-
</div>
|
|
3146
|
-
)
|
|
3147
|
-
}
|
|
3148
|
-
const app = <Counter />
|
|
3149
|
-
render(app, root)
|
|
3150
|
-
expect(root.innerHTML).toBe('<div><p>0</p><button>+</button></div>')
|
|
3151
|
-
const button = root.querySelector('button') as HTMLButtonElement
|
|
3152
|
-
button.click()
|
|
3153
|
-
await Promise.resolve()
|
|
3154
|
-
expect(root.innerHTML).toBe('<div><p>2</p><button>+</button></div>')
|
|
3155
|
-
expect(callbackSet.size).toBe(1)
|
|
3156
|
-
})
|
|
3157
|
-
})
|
|
3158
|
-
|
|
3159
|
-
describe('useMemo', () => {
|
|
3160
|
-
it('simple', async () => {
|
|
3161
|
-
let factoryCalled = 0
|
|
3162
|
-
const Counter = () => {
|
|
3163
|
-
const [count, setCount] = useState(0)
|
|
3164
|
-
const [count2, setCount2] = useState(0)
|
|
3165
|
-
const memo = useMemo(() => {
|
|
3166
|
-
factoryCalled++
|
|
3167
|
-
return count + 1
|
|
3168
|
-
}, [count])
|
|
3169
|
-
return (
|
|
3170
|
-
<div>
|
|
3171
|
-
<p>{count}</p>
|
|
3172
|
-
<p>{count2}</p>
|
|
3173
|
-
<p>{memo}</p>
|
|
3174
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
3175
|
-
<button onClick={() => setCount2(count2 + 1)}>+</button>
|
|
3176
|
-
</div>
|
|
3177
|
-
)
|
|
3178
|
-
}
|
|
3179
|
-
const app = <Counter />
|
|
3180
|
-
render(app, root)
|
|
3181
|
-
expect(root.innerHTML).toBe(
|
|
3182
|
-
'<div><p>0</p><p>0</p><p>1</p><button>+</button><button>+</button></div>'
|
|
3183
|
-
)
|
|
3184
|
-
expect(factoryCalled).toBe(1)
|
|
3185
|
-
root.querySelectorAll('button')[0].click()
|
|
3186
|
-
await Promise.resolve()
|
|
3187
|
-
expect(root.innerHTML).toBe(
|
|
3188
|
-
'<div><p>1</p><p>0</p><p>2</p><button>+</button><button>+</button></div>'
|
|
3189
|
-
)
|
|
3190
|
-
expect(factoryCalled).toBe(2)
|
|
3191
|
-
root.querySelectorAll('button')[1].click()
|
|
3192
|
-
await Promise.resolve()
|
|
3193
|
-
expect(root.innerHTML).toBe(
|
|
3194
|
-
'<div><p>1</p><p>1</p><p>2</p><button>+</button><button>+</button></div>'
|
|
3195
|
-
)
|
|
3196
|
-
expect(factoryCalled).toBe(2)
|
|
3197
|
-
})
|
|
3198
|
-
})
|
|
3199
|
-
|
|
3200
|
-
describe('isValidElement', () => {
|
|
3201
|
-
it('valid', () => {
|
|
3202
|
-
expect(isValidElement(<div />)).toBe(true)
|
|
3203
|
-
})
|
|
3204
|
-
|
|
3205
|
-
it('invalid', () => {
|
|
3206
|
-
expect(isValidElement({})).toBe(false)
|
|
3207
|
-
})
|
|
3208
|
-
})
|
|
3209
|
-
|
|
3210
|
-
describe('createElement', () => {
|
|
3211
|
-
it('simple', async () => {
|
|
3212
|
-
const App = () => {
|
|
3213
|
-
const [count, setCount] = useState(0)
|
|
3214
|
-
return (
|
|
3215
|
-
<div>{createElement('p', { onClick: () => setCount(count + 1) }, String(count))}</div>
|
|
3216
|
-
)
|
|
3217
|
-
}
|
|
3218
|
-
const app = <App />
|
|
3219
|
-
render(app, root)
|
|
3220
|
-
expect(root.innerHTML).toBe('<div><p>0</p></div>')
|
|
3221
|
-
root.querySelector('p')?.click()
|
|
3222
|
-
await Promise.resolve()
|
|
3223
|
-
expect(root.innerHTML).toBe('<div><p>1</p></div>')
|
|
3224
|
-
})
|
|
3225
|
-
|
|
3226
|
-
it('title', async () => {
|
|
3227
|
-
const App = () => {
|
|
3228
|
-
return <div>{createElement('title', {}, 'Hello')}</div>
|
|
3229
|
-
}
|
|
3230
|
-
const app = <App />
|
|
3231
|
-
render(app, root)
|
|
3232
|
-
expect(document.head.innerHTML).toBe('<title>Hello</title>')
|
|
3233
|
-
expect(root.innerHTML).toBe('<div></div>')
|
|
3234
|
-
})
|
|
3235
|
-
})
|
|
3236
|
-
|
|
3237
|
-
describe('dom-specific createElement', () => {
|
|
3238
|
-
it('simple', async () => {
|
|
3239
|
-
const App = () => {
|
|
3240
|
-
const [count, setCount] = useState(0)
|
|
3241
|
-
return <div>{createElementForDom('p', { onClick: () => setCount(count + 1) }, count)}</div>
|
|
3242
|
-
}
|
|
3243
|
-
const app = <App />
|
|
3244
|
-
render(app, root)
|
|
3245
|
-
expect(root.innerHTML).toBe('<div><p>0</p></div>')
|
|
3246
|
-
root.querySelector('p')?.click()
|
|
3247
|
-
await Promise.resolve()
|
|
3248
|
-
expect(root.innerHTML).toBe('<div><p>1</p></div>')
|
|
3249
|
-
})
|
|
3250
|
-
|
|
3251
|
-
it('title', async () => {
|
|
3252
|
-
const App = () => {
|
|
3253
|
-
return <div>{createElementForDom('title', {}, 'Hello')}</div>
|
|
3254
|
-
}
|
|
3255
|
-
const app = <App />
|
|
3256
|
-
render(app, root)
|
|
3257
|
-
expect(document.head.innerHTML).toBe('<title>Hello</title>')
|
|
3258
|
-
expect(root.innerHTML).toBe('<div></div>')
|
|
3259
|
-
})
|
|
3260
|
-
})
|
|
3261
|
-
|
|
3262
|
-
describe('cloneElement', () => {
|
|
3263
|
-
it('simple', async () => {
|
|
3264
|
-
const App = () => {
|
|
3265
|
-
const [count, setCount] = useState(0)
|
|
3266
|
-
return <div>{cloneElement(<p>{count}</p>, { onClick: () => setCount(count + 1) })}</div>
|
|
3267
|
-
}
|
|
3268
|
-
const app = <App />
|
|
3269
|
-
render(app, root)
|
|
3270
|
-
expect(root.innerHTML).toBe('<div><p>0</p></div>')
|
|
3271
|
-
root.querySelector('p')?.click()
|
|
3272
|
-
await Promise.resolve()
|
|
3273
|
-
expect(root.innerHTML).toBe('<div><p>1</p></div>')
|
|
3274
|
-
})
|
|
3275
|
-
})
|
|
3276
|
-
|
|
3277
|
-
describe('dom-specific cloneElement', () => {
|
|
3278
|
-
it('simple', async () => {
|
|
3279
|
-
const App = () => {
|
|
3280
|
-
const [count, setCount] = useState(0)
|
|
3281
|
-
return (
|
|
3282
|
-
<div>{cloneElementForDom(<p>{count}</p>, { onClick: () => setCount(count + 1) })}</div>
|
|
3283
|
-
)
|
|
3284
|
-
}
|
|
3285
|
-
const app = <App />
|
|
3286
|
-
render(app, root)
|
|
3287
|
-
expect(root.innerHTML).toBe('<div><p>0</p></div>')
|
|
3288
|
-
root.querySelector('p')?.click()
|
|
3289
|
-
await Promise.resolve()
|
|
3290
|
-
expect(root.innerHTML).toBe('<div><p>1</p></div>')
|
|
3291
|
-
})
|
|
3292
|
-
})
|
|
3293
|
-
|
|
3294
|
-
describe('flushSync', () => {
|
|
3295
|
-
it('simple', async () => {
|
|
3296
|
-
const SubApp = ({ id }: { id: string }) => {
|
|
3297
|
-
const [count, setCount] = useState(0)
|
|
3298
|
-
return (
|
|
3299
|
-
<div id={id}>
|
|
3300
|
-
<p>{count}</p>
|
|
3301
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
3302
|
-
</div>
|
|
3303
|
-
)
|
|
3304
|
-
}
|
|
3305
|
-
const App = () => {
|
|
3306
|
-
return (
|
|
3307
|
-
<div>
|
|
3308
|
-
<SubApp id='a' />
|
|
3309
|
-
<SubApp id='b' />
|
|
3310
|
-
</div>
|
|
3311
|
-
)
|
|
3312
|
-
}
|
|
3313
|
-
const app = <App />
|
|
3314
|
-
render(app, root)
|
|
3315
|
-
expect(root.innerHTML).toBe(
|
|
3316
|
-
'<div><div id="a"><p>0</p><button>+</button></div><div id="b"><p>0</p><button>+</button></div></div>'
|
|
3317
|
-
)
|
|
3318
|
-
root.querySelector<HTMLButtonElement>('#b button')?.click()
|
|
3319
|
-
flushSync(() => {
|
|
3320
|
-
root.querySelector<HTMLButtonElement>('#a button')?.click()
|
|
3321
|
-
})
|
|
3322
|
-
expect(root.innerHTML).toBe(
|
|
3323
|
-
'<div><div id="a"><p>1</p><button>+</button></div><div id="b"><p>0</p><button>+</button></div></div>'
|
|
3324
|
-
)
|
|
3325
|
-
await Promise.resolve()
|
|
3326
|
-
expect(root.innerHTML).toBe(
|
|
3327
|
-
'<div><div id="a"><p>1</p><button>+</button></div><div id="b"><p>1</p><button>+</button></div></div>'
|
|
3328
|
-
)
|
|
3329
|
-
})
|
|
3330
|
-
})
|
|
3331
|
-
|
|
3332
|
-
describe('createPortal', () => {
|
|
3333
|
-
it('simple', async () => {
|
|
3334
|
-
const App = () => {
|
|
3335
|
-
const [count, setCount] = useState(0)
|
|
3336
|
-
return (
|
|
3337
|
-
<div>
|
|
3338
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
3339
|
-
{count <= 1 && createPortal(<p>{count}</p>, document.body)}
|
|
3340
|
-
</div>
|
|
3341
|
-
)
|
|
3342
|
-
}
|
|
3343
|
-
const app = <App />
|
|
3344
|
-
render(app, root)
|
|
3345
|
-
expect(root.innerHTML).toBe('<div><button>+</button></div>')
|
|
3346
|
-
expect(document.body.innerHTML).toBe(
|
|
3347
|
-
'<div id="root"><div><button>+</button></div></div><p>0</p>'
|
|
3348
|
-
)
|
|
3349
|
-
document.body.querySelector('button')?.click()
|
|
3350
|
-
await Promise.resolve()
|
|
3351
|
-
expect(root.innerHTML).toBe('<div><button>+</button></div>')
|
|
3352
|
-
expect(document.body.innerHTML).toBe(
|
|
3353
|
-
'<div id="root"><div><button>+</button></div></div><p>1</p>'
|
|
3354
|
-
)
|
|
3355
|
-
document.body.querySelector('button')?.click()
|
|
3356
|
-
await Promise.resolve()
|
|
3357
|
-
expect(document.body.innerHTML).toBe('<div id="root"><div><button>+</button></div></div>')
|
|
3358
|
-
})
|
|
3359
|
-
|
|
3360
|
-
it('update', async () => {
|
|
3361
|
-
const App = () => {
|
|
3362
|
-
const [count, setCount] = useState(0)
|
|
3363
|
-
return (
|
|
3364
|
-
<div>
|
|
3365
|
-
{createPortal(<p>{count}</p>, document.body)}
|
|
3366
|
-
<button onClick={() => setCount(count + 1)}>+</button>
|
|
3367
|
-
<div>
|
|
3368
|
-
<p>{count}</p>
|
|
3369
|
-
</div>
|
|
3370
|
-
</div>
|
|
3371
|
-
)
|
|
3372
|
-
}
|
|
3373
|
-
const app = <App />
|
|
3374
|
-
render(app, root)
|
|
3375
|
-
expect(root.innerHTML).toBe('<div><button>+</button><div><p>0</p></div></div>')
|
|
3376
|
-
expect(document.body.innerHTML).toBe(
|
|
3377
|
-
'<div id="root"><div><button>+</button><div><p>0</p></div></div></div><p>0</p>'
|
|
3378
|
-
)
|
|
3379
|
-
|
|
3380
|
-
const createElementSpy = vi.spyOn(dom.window.document, 'createElement')
|
|
3381
|
-
|
|
3382
|
-
document.body.querySelector('button')?.click()
|
|
3383
|
-
await Promise.resolve()
|
|
3384
|
-
expect(root.innerHTML).toBe('<div><button>+</button><div><p>1</p></div></div>')
|
|
3385
|
-
expect(document.body.innerHTML).toBe(
|
|
3386
|
-
'<div id="root"><div><button>+</button><div><p>1</p></div></div></div><p>1</p>'
|
|
3387
|
-
)
|
|
3388
|
-
document.body.querySelector('button')?.click()
|
|
3389
|
-
await Promise.resolve()
|
|
3390
|
-
expect(document.body.innerHTML).toBe(
|
|
3391
|
-
'<div id="root"><div><button>+</button><div><p>2</p></div></div></div><p>2</p>'
|
|
3392
|
-
)
|
|
3393
|
-
|
|
3394
|
-
expect(createElementSpy).not.toHaveBeenCalled()
|
|
3395
|
-
})
|
|
3396
|
-
})
|
|
3397
|
-
|
|
3398
|
-
describe('SVG', () => {
|
|
3399
|
-
it('simple', () => {
|
|
3400
|
-
const App = () => {
|
|
3401
|
-
return (
|
|
3402
|
-
<svg>
|
|
3403
|
-
<circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red' />
|
|
3404
|
-
</svg>
|
|
3405
|
-
)
|
|
3406
|
-
}
|
|
3407
|
-
render(<App />, root)
|
|
3408
|
-
expect(root.innerHTML).toBe(
|
|
3409
|
-
'<svg><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red"></circle></svg>'
|
|
3410
|
-
)
|
|
3411
|
-
})
|
|
3412
|
-
|
|
3413
|
-
it('title element', () => {
|
|
3414
|
-
const App = () => {
|
|
3415
|
-
return (
|
|
3416
|
-
<>
|
|
3417
|
-
<title>Document Title</title>
|
|
3418
|
-
<svg>
|
|
3419
|
-
<title>SVG Title</title>
|
|
3420
|
-
</svg>
|
|
3421
|
-
</>
|
|
3422
|
-
)
|
|
3423
|
-
}
|
|
3424
|
-
render(<App />, root)
|
|
3425
|
-
expect(document.head.innerHTML).toBe('<title>Document Title</title>')
|
|
3426
|
-
expect(root.innerHTML).toBe('<svg><title>SVG Title</title></svg>')
|
|
3427
|
-
expect(document.querySelector('title')).toBeInstanceOf(dom.window.HTMLTitleElement)
|
|
3428
|
-
expect(document.querySelector('svg title')).toBeInstanceOf(dom.window.SVGTitleElement)
|
|
3429
|
-
})
|
|
3430
|
-
|
|
3431
|
-
it('skips invalid attribute keys in SVG while preserving valid ones', () => {
|
|
3432
|
-
const App = () => {
|
|
3433
|
-
return (
|
|
3434
|
-
<svg>
|
|
3435
|
-
<g {...{ ['" onload="alert(1)']: 'x', viewBox: '0 0 10 10' }} />
|
|
3436
|
-
</svg>
|
|
3437
|
-
)
|
|
3438
|
-
}
|
|
3439
|
-
render(<App />, root)
|
|
3440
|
-
expect(root.innerHTML).toBe('<svg><g viewBox="0 0 10 10"></g></svg>')
|
|
3441
|
-
})
|
|
3442
|
-
|
|
3443
|
-
describe('attribute', () => {
|
|
3444
|
-
describe('camelCase', () => {
|
|
3445
|
-
test.each`
|
|
3446
|
-
key
|
|
3447
|
-
${'attributeName'}
|
|
3448
|
-
${'baseFrequency'}
|
|
3449
|
-
${'calcMode'}
|
|
3450
|
-
${'clipPathUnits'}
|
|
3451
|
-
${'diffuseConstant'}
|
|
3452
|
-
${'edgeMode'}
|
|
3453
|
-
${'filterUnits'}
|
|
3454
|
-
${'gradientTransform'}
|
|
3455
|
-
${'gradientUnits'}
|
|
3456
|
-
${'kernelMatrix'}
|
|
3457
|
-
${'kernelUnitLength'}
|
|
3458
|
-
${'keyPoints'}
|
|
3459
|
-
${'keySplines'}
|
|
3460
|
-
${'keyTimes'}
|
|
3461
|
-
${'lengthAdjust'}
|
|
3462
|
-
${'limitingConeAngle'}
|
|
3463
|
-
${'markerHeight'}
|
|
3464
|
-
${'markerUnits'}
|
|
3465
|
-
${'markerWidth'}
|
|
3466
|
-
${'maskContentUnits'}
|
|
3467
|
-
${'maskUnits'}
|
|
3468
|
-
${'numOctaves'}
|
|
3469
|
-
${'pathLength'}
|
|
3470
|
-
${'patternContentUnits'}
|
|
3471
|
-
${'patternTransform'}
|
|
3472
|
-
${'patternUnits'}
|
|
3473
|
-
${'pointsAtX'}
|
|
3474
|
-
${'pointsAtY'}
|
|
3475
|
-
${'pointsAtZ'}
|
|
3476
|
-
${'preserveAlpha'}
|
|
3477
|
-
${'preserveAspectRatio'}
|
|
3478
|
-
${'primitiveUnits'}
|
|
3479
|
-
${'refX'}
|
|
3480
|
-
${'refY'}
|
|
3481
|
-
${'repeatCount'}
|
|
3482
|
-
${'repeatDur'}
|
|
3483
|
-
${'specularConstant'}
|
|
3484
|
-
${'specularExponent'}
|
|
3485
|
-
${'spreadMethod'}
|
|
3486
|
-
${'startOffset'}
|
|
3487
|
-
${'stdDeviation'}
|
|
3488
|
-
${'stitchTiles'}
|
|
3489
|
-
${'surfaceScale'}
|
|
3490
|
-
${'crossorigin'}
|
|
3491
|
-
${'systemLanguage'}
|
|
3492
|
-
${'tableValues'}
|
|
3493
|
-
${'targetX'}
|
|
3494
|
-
${'targetY'}
|
|
3495
|
-
${'textLength'}
|
|
3496
|
-
${'viewBox'}
|
|
3497
|
-
${'xChannelSelector'}
|
|
3498
|
-
${'yChannelSelector'}
|
|
3499
|
-
`('$key', ({ key }) => {
|
|
3500
|
-
const App = () => {
|
|
3501
|
-
return (
|
|
3502
|
-
<svg>
|
|
3503
|
-
<g {...{ [key]: 'test' }} />
|
|
3504
|
-
</svg>
|
|
3505
|
-
)
|
|
3506
|
-
}
|
|
3507
|
-
render(<App />, root)
|
|
3508
|
-
expect(root.innerHTML).toBe(`<svg><g ${key}="test"></g></svg>`)
|
|
3509
|
-
})
|
|
3510
|
-
})
|
|
3511
|
-
|
|
3512
|
-
describe('kebab-case', () => {
|
|
3513
|
-
test.each`
|
|
3514
|
-
key
|
|
3515
|
-
${'alignmentBaseline'}
|
|
3516
|
-
${'baselineShift'}
|
|
3517
|
-
${'clipPath'}
|
|
3518
|
-
${'clipRule'}
|
|
3519
|
-
${'colorInterpolation'}
|
|
3520
|
-
${'colorInterpolationFilters'}
|
|
3521
|
-
${'dominantBaseline'}
|
|
3522
|
-
${'fillOpacity'}
|
|
3523
|
-
${'fillRule'}
|
|
3524
|
-
${'floodColor'}
|
|
3525
|
-
${'floodOpacity'}
|
|
3526
|
-
${'fontFamily'}
|
|
3527
|
-
${'fontSize'}
|
|
3528
|
-
${'fontSizeAdjust'}
|
|
3529
|
-
${'fontStretch'}
|
|
3530
|
-
${'fontStyle'}
|
|
3531
|
-
${'fontVariant'}
|
|
3532
|
-
${'fontWeight'}
|
|
3533
|
-
${'imageRendering'}
|
|
3534
|
-
${'letterSpacing'}
|
|
3535
|
-
${'lightingColor'}
|
|
3536
|
-
${'markerEnd'}
|
|
3537
|
-
${'markerMid'}
|
|
3538
|
-
${'markerStart'}
|
|
3539
|
-
${'overlinePosition'}
|
|
3540
|
-
${'overlineThickness'}
|
|
3541
|
-
${'paintOrder'}
|
|
3542
|
-
${'pointerEvents'}
|
|
3543
|
-
${'shapeRendering'}
|
|
3544
|
-
${'stopColor'}
|
|
3545
|
-
${'stopOpacity'}
|
|
3546
|
-
${'strikethroughPosition'}
|
|
3547
|
-
${'strikethroughThickness'}
|
|
3548
|
-
${'strokeDasharray'}
|
|
3549
|
-
${'strokeDashoffset'}
|
|
3550
|
-
${'strokeLinecap'}
|
|
3551
|
-
${'strokeLinejoin'}
|
|
3552
|
-
${'strokeMiterlimit'}
|
|
3553
|
-
${'strokeOpacity'}
|
|
3554
|
-
${'strokeWidth'}
|
|
3555
|
-
${'textAnchor'}
|
|
3556
|
-
${'textDecoration'}
|
|
3557
|
-
${'textRendering'}
|
|
3558
|
-
${'transformOrigin'}
|
|
3559
|
-
${'underlinePosition'}
|
|
3560
|
-
${'underlineThickness'}
|
|
3561
|
-
${'unicodeBidi'}
|
|
3562
|
-
${'vectorEffect'}
|
|
3563
|
-
${'wordSpacing'}
|
|
3564
|
-
${'writingMode'}
|
|
3565
|
-
`('$key', ({ key }) => {
|
|
3566
|
-
const App = () => {
|
|
3567
|
-
return (
|
|
3568
|
-
<svg>
|
|
3569
|
-
<g {...{ [key]: 'test' }} />
|
|
3570
|
-
</svg>
|
|
3571
|
-
)
|
|
3572
|
-
}
|
|
3573
|
-
render(<App />, root)
|
|
3574
|
-
expect(root.innerHTML).toBe(
|
|
3575
|
-
`<svg><g ${key.replace(/([A-Z])/g, '-$1').toLowerCase()}="test"></g></svg>`
|
|
3576
|
-
)
|
|
3577
|
-
})
|
|
3578
|
-
})
|
|
3579
|
-
|
|
3580
|
-
describe('data-*', () => {
|
|
3581
|
-
test.each`
|
|
3582
|
-
key
|
|
3583
|
-
${'data-foo'}
|
|
3584
|
-
${'data-foo-bar'}
|
|
3585
|
-
${'data-fooBar'}
|
|
3586
|
-
`('$key', ({ key }) => {
|
|
3587
|
-
const App = () => {
|
|
3588
|
-
return (
|
|
3589
|
-
<svg>
|
|
3590
|
-
<g {...{ [key]: 'test' }} />
|
|
3591
|
-
</svg>
|
|
3592
|
-
)
|
|
3593
|
-
}
|
|
3594
|
-
render(<App />, root)
|
|
3595
|
-
expect(root.innerHTML).toBe(`<svg><g ${key}="test"></g></svg>`)
|
|
3596
|
-
})
|
|
3597
|
-
})
|
|
3598
|
-
})
|
|
3599
|
-
})
|
|
3600
|
-
|
|
3601
|
-
describe('MathML', () => {
|
|
3602
|
-
it('simple', () => {
|
|
3603
|
-
const createElementSpy = vi.spyOn(dom.window.document, 'createElement')
|
|
3604
|
-
const createElementNSSpy = vi.spyOn(dom.window.document, 'createElementNS')
|
|
3605
|
-
|
|
3606
|
-
const App = () => {
|
|
3607
|
-
return (
|
|
3608
|
-
<math>
|
|
3609
|
-
<mrow>
|
|
3610
|
-
<mn>1</mn>
|
|
3611
|
-
</mrow>
|
|
3612
|
-
</math>
|
|
3613
|
-
)
|
|
3614
|
-
}
|
|
3615
|
-
render(<App />, root)
|
|
3616
|
-
expect(root.innerHTML).toBe('<math><mrow><mn>1</mn></mrow></math>')
|
|
3617
|
-
|
|
3618
|
-
expect(createElementSpy).not.toHaveBeenCalled()
|
|
3619
|
-
expect(createElementNSSpy).toHaveBeenCalledWith('http://www.w3.org/1998/Math/MathML', 'math')
|
|
3620
|
-
expect(createElementNSSpy).toHaveBeenCalledWith('http://www.w3.org/1998/Math/MathML', 'mrow')
|
|
3621
|
-
})
|
|
3622
|
-
})
|
|
3623
|
-
})
|
|
3624
|
-
|
|
3625
|
-
describe('jsx', () => {
|
|
3626
|
-
it('exported as an alias of createElement', () => {
|
|
3627
|
-
expect(jsx).toBeDefined()
|
|
3628
|
-
expect(jsx('div', {}, 'Hello')).toBeInstanceOf(Object)
|
|
3629
|
-
})
|
|
3630
|
-
})
|
|
3631
|
-
|
|
3632
|
-
describe('version', () => {
|
|
3633
|
-
it('should be defined with semantic versioning format', () => {
|
|
3634
|
-
expect(version).toMatch(/^\d+\.\d+\.\d+-acryl-jsx$/)
|
|
3635
|
-
})
|
|
3636
|
-
})
|
|
3637
|
-
|
|
3638
|
-
describe('default export', () => {
|
|
3639
|
-
;[
|
|
3640
|
-
'version',
|
|
3641
|
-
'memo',
|
|
3642
|
-
'Fragment',
|
|
3643
|
-
'isValidElement',
|
|
3644
|
-
'createElement',
|
|
3645
|
-
'cloneElement',
|
|
3646
|
-
'ErrorBoundary',
|
|
3647
|
-
'createContext',
|
|
3648
|
-
'useContext',
|
|
3649
|
-
'useState',
|
|
3650
|
-
'useEffect',
|
|
3651
|
-
'useRef',
|
|
3652
|
-
'useCallback',
|
|
3653
|
-
'useReducer',
|
|
3654
|
-
'useDebugValue',
|
|
3655
|
-
'createRef',
|
|
3656
|
-
'forwardRef',
|
|
3657
|
-
'useImperativeHandle',
|
|
3658
|
-
'useSyncExternalStore',
|
|
3659
|
-
'use',
|
|
3660
|
-
'startTransition',
|
|
3661
|
-
'useTransition',
|
|
3662
|
-
'useDeferredValue',
|
|
3663
|
-
'startViewTransition',
|
|
3664
|
-
'useViewTransition',
|
|
3665
|
-
'useActionState',
|
|
3666
|
-
'useFormStatus',
|
|
3667
|
-
'useOptimistic',
|
|
3668
|
-
'useMemo',
|
|
3669
|
-
'useLayoutEffect',
|
|
3670
|
-
'Suspense',
|
|
3671
|
-
'Fragment',
|
|
3672
|
-
'flushSync',
|
|
3673
|
-
'createPortal',
|
|
3674
|
-
'StrictMode',
|
|
3675
|
-
].forEach((key) => {
|
|
3676
|
-
it(key, () => {
|
|
3677
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3678
|
-
expect((DefaultExport as any)[key]).toBeDefined()
|
|
3679
|
-
})
|
|
3680
|
-
})
|
|
3681
|
-
})
|