acrylblends 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/docs/framework/README.md +2 -2
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/guides/jsx.md +51 -1
- 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/{base-ddiVmiE7.js → base-NSN_SNv-.js} +1 -0
- package/lib/basic-auth.d.ts +1 -1
- package/lib/bearer-auth.d.ts +1 -1
- package/lib/body-limit.d.ts +1 -1
- package/lib/cache.d.ts +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-DYZdxJIJ.js → components-JXVm45kK.js} +1 -1
- package/lib/{components-Ce5FRyHU.js → components-cEW3ovK0.js} +3 -3
- 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-w72iLeFR.js} +6 -6
- 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 +2 -2
- package/lib/jsx/dom.js +4 -4
- package/lib/jsx/jsx-dev-runtime.js +1 -1
- package/lib/jsx/jsx-runtime.js +1 -1
- package/lib/jsx/streaming.js +1 -1
- package/lib/{jsx-dev-runtime-kAhR_4C7.js → jsx-dev-runtime-CWLRA2F3.js} +2 -2
- package/lib/jsx-renderer.d.ts +75 -0
- package/lib/jsx-renderer.js +99 -0
- package/lib/jsx.js +4 -4
- 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/register.d.ts +1 -0
- package/lib/node-server/register.js +73 -0
- 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 +13 -1
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-node-server/src/register.ts +86 -0
- package/plugins/acryl-node-server/upstream.json +5 -3
- 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/main.ts +4 -1
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/main.ts +3 -1
- package/plugins/desktop-web/upstream.json +1 -1
- package/plugins/extensions/resolve.ts +4 -0
- package/shared/AGENTS.core.md +3 -0
- package/templates/agent/package.json +7 -3
- package/templates/agent/tsconfig.json +10 -2
- package/templates/blank/package.json +7 -3
- package/templates/blank/src/about.tsx +14 -0
- package/templates/blank/src/routes.ts +2 -0
- package/templates/blank/test/app.test.ts +15 -0
- package/templates/blank/tsconfig.json +3 -1
- package/templates/todo/package.json +7 -3
- package/templates/todo/tsconfig.json +3 -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,1744 +0,0 @@
|
|
|
1
|
-
import DefaultExport, {
|
|
2
|
-
ErrorBoundary,
|
|
3
|
-
Fragment,
|
|
4
|
-
StrictMode,
|
|
5
|
-
createContext,
|
|
6
|
-
createElement,
|
|
7
|
-
jsx,
|
|
8
|
-
memo,
|
|
9
|
-
useContext,
|
|
10
|
-
version,
|
|
11
|
-
} from './index.ts'
|
|
12
|
-
import type { Context, FC, PropsWithChildren } from './index.ts'
|
|
13
|
-
/** @jsxImportSource ./ */
|
|
14
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
15
|
-
import { html, raw } from '../helper/html/index.ts'
|
|
16
|
-
import { Acryl } from '../app.ts'
|
|
17
|
-
import { DOM_MEMO } from './constants.ts'
|
|
18
|
-
import { captureRenderContext } from './context.ts'
|
|
19
|
-
import { Suspense, renderToReadableStream } from './streaming.ts'
|
|
20
|
-
|
|
21
|
-
interface SiteData {
|
|
22
|
-
title: string
|
|
23
|
-
children?: any
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe('JSX middleware', () => {
|
|
27
|
-
let app: Acryl
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
app = new Acryl()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('Should render HTML strings', async () => {
|
|
34
|
-
app.get('/', (c) => {
|
|
35
|
-
return c.html(<h1>Hello</h1>)
|
|
36
|
-
})
|
|
37
|
-
const res = await app.request('http://localhost/')
|
|
38
|
-
expect(res.status).toBe(200)
|
|
39
|
-
expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
|
|
40
|
-
expect(await res.text()).toBe('<h1>Hello</h1>')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('Should be able to be used with html middleware', async () => {
|
|
44
|
-
const Layout = (props: SiteData) =>
|
|
45
|
-
html`<!DOCTYPE html>
|
|
46
|
-
<html>
|
|
47
|
-
<head>
|
|
48
|
-
<title>${props.title}</title>
|
|
49
|
-
</head>
|
|
50
|
-
<body>
|
|
51
|
-
${props.children}
|
|
52
|
-
</body>
|
|
53
|
-
</html>`
|
|
54
|
-
|
|
55
|
-
const Content = (props: { siteData: SiteData; name: string }) => (
|
|
56
|
-
<Layout {...props.siteData}>
|
|
57
|
-
<h1>{props.name}</h1>
|
|
58
|
-
</Layout>
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
app.get('/', (c) => {
|
|
62
|
-
const props = {
|
|
63
|
-
name: 'JSX',
|
|
64
|
-
siteData: {
|
|
65
|
-
title: 'with html middleware',
|
|
66
|
-
},
|
|
67
|
-
}
|
|
68
|
-
return c.html(<Content {...props} />)
|
|
69
|
-
})
|
|
70
|
-
const res = await app.request('http://localhost/')
|
|
71
|
-
expect(res.status).toBe(200)
|
|
72
|
-
expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
|
|
73
|
-
expect(await res.text()).toBe(`<!DOCTYPE html>
|
|
74
|
-
<html>
|
|
75
|
-
<head>
|
|
76
|
-
<title>with html middleware</title>
|
|
77
|
-
</head>
|
|
78
|
-
<body>
|
|
79
|
-
<h1>JSX</h1>
|
|
80
|
-
</body>
|
|
81
|
-
</html>`)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('Should render async component', async () => {
|
|
85
|
-
const ChildAsyncComponent = async () => {
|
|
86
|
-
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
87
|
-
return <span>child async component</span>
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const AsyncComponent = async () => {
|
|
91
|
-
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
92
|
-
return (
|
|
93
|
-
<h1>
|
|
94
|
-
Hello from async component
|
|
95
|
-
<ChildAsyncComponent />
|
|
96
|
-
</h1>
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
app.get('/', (c) => {
|
|
101
|
-
return c.html(<AsyncComponent />)
|
|
102
|
-
})
|
|
103
|
-
const res = await app.request('http://localhost/')
|
|
104
|
-
expect(res.status).toBe(200)
|
|
105
|
-
expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
|
|
106
|
-
expect(await res.text()).toBe(
|
|
107
|
-
'<h1>Hello from async component<span>child async component</span></h1>'
|
|
108
|
-
)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('Should render async component with "html" tagged template strings', async () => {
|
|
112
|
-
const AsyncComponent = async () => {
|
|
113
|
-
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
114
|
-
return <h1>Hello from async component</h1>
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
app.get('/', (c) => {
|
|
118
|
-
// prettier-ignore
|
|
119
|
-
return c.html(
|
|
120
|
-
html`<html><body>${(<AsyncComponent />)}</body></html>`
|
|
121
|
-
)
|
|
122
|
-
})
|
|
123
|
-
const res = await app.request('http://localhost/')
|
|
124
|
-
expect(res.status).toBe(200)
|
|
125
|
-
expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
|
|
126
|
-
expect(await res.text()).toBe('<html><body><h1>Hello from async component</h1></body></html>')
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('Should handle async component error', async () => {
|
|
130
|
-
const componentError = new Error('Error from async error component')
|
|
131
|
-
|
|
132
|
-
const AsyncComponent = async () => {
|
|
133
|
-
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
134
|
-
return <h1>Hello from async component</h1>
|
|
135
|
-
}
|
|
136
|
-
const AsyncErrorComponent = async () => {
|
|
137
|
-
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
138
|
-
throw componentError
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
let raisedError: any
|
|
142
|
-
app.onError((e, c) => {
|
|
143
|
-
raisedError = e
|
|
144
|
-
return c.html('<html><body><h1>Error from onError</h1></body></html>', 500)
|
|
145
|
-
})
|
|
146
|
-
app.get('/', (c) => {
|
|
147
|
-
return c.html(
|
|
148
|
-
<>
|
|
149
|
-
<AsyncComponent />
|
|
150
|
-
<AsyncErrorComponent />
|
|
151
|
-
</>
|
|
152
|
-
)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
const res = await app.request('http://localhost/')
|
|
156
|
-
expect(res.status).toBe(500)
|
|
157
|
-
expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
|
|
158
|
-
expect(await res.text()).toBe('<html><body><h1>Error from onError</h1></body></html>')
|
|
159
|
-
expect(raisedError).toBe(componentError)
|
|
160
|
-
})
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
describe('render to string', () => {
|
|
164
|
-
it('Nested array', () => {
|
|
165
|
-
const template = (
|
|
166
|
-
<p>
|
|
167
|
-
{[[['a']], [['b']]].map((item1) =>
|
|
168
|
-
item1.map((item2) => item2.map((item3) => <span>{item3}</span>))
|
|
169
|
-
)}
|
|
170
|
-
</p>
|
|
171
|
-
)
|
|
172
|
-
expect(template.toString()).toBe('<p><span>a</span><span>b</span></p>')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('Component returning an array', () => {
|
|
176
|
-
const Item = ({ x }: { x: number }) => <span>{x}</span>
|
|
177
|
-
const Items = () => [0, 1].map((x) => <Item key={x} x={x} />)
|
|
178
|
-
const template = <Items />
|
|
179
|
-
expect(template.toString()).toBe('<span>0</span><span>1</span>')
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('Component returning a nested array', () => {
|
|
183
|
-
const Items = () => [['a', 'b'], [<span>c</span>], null]
|
|
184
|
-
const template = <Items />
|
|
185
|
-
expect(template.toString()).toBe('ab<span>c</span>')
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('Component returning an array preserves escaped string callbacks', () => {
|
|
189
|
-
const Items = () => [
|
|
190
|
-
raw('a', [
|
|
191
|
-
({ buffer }) => {
|
|
192
|
-
if (buffer) {
|
|
193
|
-
buffer[0] += 'b'
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
]),
|
|
197
|
-
]
|
|
198
|
-
expect((<Items />).toString()).toBe('ab')
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('Component returning an array escapes strings', async () => {
|
|
202
|
-
const SyncItems = () => ['<script>alert(1)</script>']
|
|
203
|
-
const AsyncItems = async () => ['<script>alert(1)</script>']
|
|
204
|
-
const expected = '<script>alert(1)</script>'
|
|
205
|
-
|
|
206
|
-
expect((<SyncItems />).toString()).toBe(expected)
|
|
207
|
-
expect((await (<AsyncItems />).toString()).toString()).toBe(expected)
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('Component returning an array including async components', async () => {
|
|
211
|
-
const AsyncItem = async ({ x }: { x: number }) => <span>{x}</span>
|
|
212
|
-
const Items = () => [0, 1].map((x) => <AsyncItem key={x} x={x} />)
|
|
213
|
-
const template = <Items />
|
|
214
|
-
expect((await template.toString()).toString()).toBe('<span>0</span><span>1</span>')
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
it('Component typed as FC returning an array', () => {
|
|
218
|
-
const Item: FC<{ x: number }> = ({ x }) => <span>{x}</span>
|
|
219
|
-
const Items: FC = () => [0, 1].map((x) => <Item key={x} x={x} />)
|
|
220
|
-
const template = <Items />
|
|
221
|
-
expect(template.toString()).toBe('<span>0</span><span>1</span>')
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('Async component returning an array', async () => {
|
|
225
|
-
const AsyncItems = async () => [<span>a</span>, <span>b</span>]
|
|
226
|
-
const template = <AsyncItems />
|
|
227
|
-
expect((await template.toString()).toString()).toBe('<span>a</span><span>b</span>')
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('Async component returning an array of async components', async () => {
|
|
231
|
-
const AsyncItem = async ({ x }: { x: number }) => <span>{x}</span>
|
|
232
|
-
const AsyncItems = async () => [<AsyncItem key={0} x={0} />, <AsyncItem key={1} x={1} />]
|
|
233
|
-
const template = <AsyncItems />
|
|
234
|
-
expect((await template.toString()).toString()).toBe('<span>0</span><span>1</span>')
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('Empty elements are rended without closing tag', () => {
|
|
238
|
-
const template = <input />
|
|
239
|
-
expect(template.toString()).toBe('<input/>')
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('Empty elements with children are rended with children and closing tag', () => {
|
|
243
|
-
const template = <link>https://example.com</link>
|
|
244
|
-
expect(template.toString()).toBe('<link>https://example.com</link>')
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
describe('programmatic tag names', () => {
|
|
248
|
-
it('Should throw when jsx() receives a tag name that can break out of the HTML context', () => {
|
|
249
|
-
const payloads = [
|
|
250
|
-
'><script>alert(1)</script><x',
|
|
251
|
-
'div onmouseover="alert(1)" x=',
|
|
252
|
-
'div></div><img src=x onerror=alert(1)><div',
|
|
253
|
-
'!--',
|
|
254
|
-
'!DOCTYPE',
|
|
255
|
-
'?xml',
|
|
256
|
-
]
|
|
257
|
-
|
|
258
|
-
for (const tag of payloads) {
|
|
259
|
-
expect(() => jsx(tag, {}, 'hello')).toThrow(`Invalid JSX tag name: ${tag}`)
|
|
260
|
-
}
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it('Should throw when createElement() receives a tag name that can break out of the HTML context', () => {
|
|
264
|
-
expect(() => createElement('div onmouseover="alert(1)" x=', {}, 'hello')).toThrow(
|
|
265
|
-
'Invalid JSX tag name: div onmouseover="alert(1)" x='
|
|
266
|
-
)
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
it('Should throw when jsx() receives an invalid non-primitive tag name', () => {
|
|
270
|
-
expect(() => jsx(new String('div') as never, {}, 'hello')).toThrow(
|
|
271
|
-
'Invalid JSX tag name: div'
|
|
272
|
-
)
|
|
273
|
-
expect(() =>
|
|
274
|
-
jsx({ toString: () => 'div onmouseover="alert(1)" x=' } as never, {}, 'hello')
|
|
275
|
-
).toThrow('Invalid JSX tag name: div onmouseover="alert(1)" x=')
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
it('Should allow compatible custom and namespaced tag names', () => {
|
|
279
|
-
expect(jsx('custom-element', {}, 'hello').toString()).toBe(
|
|
280
|
-
'<custom-element>hello</custom-element>'
|
|
281
|
-
)
|
|
282
|
-
expect(jsx('foo:bar', {}, 'hello').toString()).toBe('<foo:bar>hello</foo:bar>')
|
|
283
|
-
expect(jsx('x.foo', {}, 'hello').toString()).toBe('<x.foo>hello</x.foo>')
|
|
284
|
-
expect(jsx('x_bar', {}, 'hello').toString()).toBe('<x_bar>hello</x_bar>')
|
|
285
|
-
expect(jsx('é', {}, 'hello').toString()).toBe('<é>hello</é>')
|
|
286
|
-
})
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
it('Props value is null', () => {
|
|
290
|
-
const template = <span data-hello={null}>Hello</span>
|
|
291
|
-
expect(template.toString()).toBe('<span>Hello</span>')
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
it('Props value is undefined', () => {
|
|
295
|
-
const template = <span data-hello={undefined}>Hello</span>
|
|
296
|
-
expect(template.toString()).toBe('<span>Hello</span>')
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
describe('dangerouslySetInnerHTML', () => {
|
|
300
|
-
it('Should render dangerouslySetInnerHTML', () => {
|
|
301
|
-
const template = <span dangerouslySetInnerHTML={{ __html: '" is allowed here' }}></span>
|
|
302
|
-
expect(template.toString()).toBe('<span>" is allowed here</span>')
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
it('Should get an error if both dangerouslySetInnerHTML and children are specified', () => {
|
|
306
|
-
expect(() =>
|
|
307
|
-
(<span dangerouslySetInnerHTML={{ __html: '" is allowed here' }}>Hello</span>).toString()
|
|
308
|
-
).toThrow(Error)
|
|
309
|
-
})
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
// https://en.reactjs.org/docs/jsx-in-depth.html#booleans-null-and-undefined-are-ignored
|
|
313
|
-
describe('Booleans, Null, and Undefined Are Ignored', () => {
|
|
314
|
-
it.each([true, false, undefined, null])('%s', (item) => {
|
|
315
|
-
expect((<span>{item}</span>).toString()).toBe('<span></span>')
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
it('falsy value', () => {
|
|
319
|
-
const template = <span>{0}</span>
|
|
320
|
-
expect(template.toString()).toBe('<span>0</span>')
|
|
321
|
-
})
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
// https://en.reactjs.org/docs/jsx-in-depth.html#props-default-to-true
|
|
325
|
-
describe('Props Default to “True”', () => {
|
|
326
|
-
it('default prop value', () => {
|
|
327
|
-
const template = <span data-hello>Hello</span>
|
|
328
|
-
expect(template.toString()).toBe('<span data-hello="true">Hello</span>')
|
|
329
|
-
})
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
// https://html.spec.whatwg.org/#attributes-3
|
|
333
|
-
describe('Boolean attribute', () => {
|
|
334
|
-
it('default prop value for checked', () => {
|
|
335
|
-
const template = <input type='checkbox' checked />
|
|
336
|
-
expect(template.toString()).toBe('<input type="checkbox" checked=""/>')
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
it('default prop value for checked={true}', () => {
|
|
340
|
-
const template = <input type='checkbox' checked={true} />
|
|
341
|
-
expect(template.toString()).toBe('<input type="checkbox" checked=""/>')
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
it('no prop for checked={false}', () => {
|
|
345
|
-
const template = <input type='checkbox' checked={false} />
|
|
346
|
-
expect(template.toString()).toBe('<input type="checkbox"/>')
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
it('default prop value for disabled', () => {
|
|
350
|
-
const template = <input type='checkbox' disabled />
|
|
351
|
-
expect(template.toString()).toBe('<input type="checkbox" disabled=""/>')
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
it('default prop value for disabled={true}', () => {
|
|
355
|
-
const template = <input type='checkbox' disabled={true} />
|
|
356
|
-
expect(template.toString()).toBe('<input type="checkbox" disabled=""/>')
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
it('no prop for disabled={false}', () => {
|
|
360
|
-
const template = <input type='checkbox' disabled={false} />
|
|
361
|
-
expect(template.toString()).toBe('<input type="checkbox"/>')
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
it('default prop value for readonly', () => {
|
|
365
|
-
const template = <input type='checkbox' readonly />
|
|
366
|
-
expect(template.toString()).toBe('<input type="checkbox" readonly=""/>')
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
it('default prop value for readonly={true}', () => {
|
|
370
|
-
const template = <input type='checkbox' readonly={true} />
|
|
371
|
-
expect(template.toString()).toBe('<input type="checkbox" readonly=""/>')
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
it('no prop for readonly={false}', () => {
|
|
375
|
-
const template = <input type='checkbox' readonly={false} />
|
|
376
|
-
expect(template.toString()).toBe('<input type="checkbox"/>')
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
it('default prop value for selected', () => {
|
|
380
|
-
const template = (
|
|
381
|
-
<option value='test' selected>
|
|
382
|
-
Test
|
|
383
|
-
</option>
|
|
384
|
-
)
|
|
385
|
-
expect(template.toString()).toBe('<option value="test" selected="">Test</option>')
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
it('default prop value for selected={true}', () => {
|
|
389
|
-
const template = (
|
|
390
|
-
<option value='test' selected={true}>
|
|
391
|
-
Test
|
|
392
|
-
</option>
|
|
393
|
-
)
|
|
394
|
-
expect(template.toString()).toBe('<option value="test" selected="">Test</option>')
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
it('no prop for selected={false}', () => {
|
|
398
|
-
const template = (
|
|
399
|
-
<option value='test' selected={false}>
|
|
400
|
-
Test
|
|
401
|
-
</option>
|
|
402
|
-
)
|
|
403
|
-
expect(template.toString()).toBe('<option value="test">Test</option>')
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
it('default prop value for multiple select', () => {
|
|
407
|
-
const template = (
|
|
408
|
-
<select multiple>
|
|
409
|
-
<option>test</option>
|
|
410
|
-
</select>
|
|
411
|
-
)
|
|
412
|
-
expect(template.toString()).toBe('<select multiple=""><option>test</option></select>')
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
it('default prop value for select multiple={true}', () => {
|
|
416
|
-
const template = (
|
|
417
|
-
<select multiple={true}>
|
|
418
|
-
<option>test</option>
|
|
419
|
-
</select>
|
|
420
|
-
)
|
|
421
|
-
expect(template.toString()).toBe('<select multiple=""><option>test</option></select>')
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
it('no prop for select multiple={false}', () => {
|
|
425
|
-
const template = (
|
|
426
|
-
<select multiple={false}>
|
|
427
|
-
<option>test</option>
|
|
428
|
-
</select>
|
|
429
|
-
)
|
|
430
|
-
expect(template.toString()).toBe('<select><option>test</option></select>')
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
it('should render "false" value properly for other non-defined keys', () => {
|
|
434
|
-
const template = <input type='checkbox' testkey={false} />
|
|
435
|
-
expect(template.toString()).toBe('<input type="checkbox" testkey="false"/>')
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
it('should support attributes for elements other than input', () => {
|
|
439
|
-
const template = (
|
|
440
|
-
<video controls autoplay>
|
|
441
|
-
<source src='movie.mp4' type='video/mp4' />
|
|
442
|
-
</video>
|
|
443
|
-
)
|
|
444
|
-
expect(template.toString()).toBe(
|
|
445
|
-
'<video controls="" autoplay=""><source src="movie.mp4" type="video/mp4"/></video>'
|
|
446
|
-
)
|
|
447
|
-
})
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
describe('download attribute', () => {
|
|
451
|
-
it('<a download={true}></a> should be rendered as <a download=""></a>', () => {
|
|
452
|
-
const template = <a download={true}></a>
|
|
453
|
-
expect(template.toString()).toBe('<a download=""></a>')
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
it('<a download={false}></a> should be rendered as <a></a>', () => {
|
|
457
|
-
const template = <a download={false}></a>
|
|
458
|
-
expect(template.toString()).toBe('<a></a>')
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
it('<a download></a> should be rendered as <a download=""></a>', () => {
|
|
462
|
-
const template = <a download></a>
|
|
463
|
-
expect(template.toString()).toBe('<a download=""></a>')
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
it('<a download="test"></a> should be rendered as <a download="test"></a>', () => {
|
|
467
|
-
const template = <a download='test'></a>
|
|
468
|
-
expect(template.toString()).toBe('<a download="test"></a>')
|
|
469
|
-
})
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
describe('Function', () => {
|
|
473
|
-
it('should be ignored used in on* props', () => {
|
|
474
|
-
const onClick = () => {}
|
|
475
|
-
const template = <button onClick={onClick}>Click</button>
|
|
476
|
-
expect(template.toString()).toBe('<button>Click</button>')
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
it('should be ignored used in ref props', () => {
|
|
480
|
-
const ref = () => {}
|
|
481
|
-
const template = <div ref={ref}>Content</div>
|
|
482
|
-
expect(template.toString()).toBe('<div>Content</div>')
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
it('should raise an error if used in other props', () => {
|
|
486
|
-
const onClick = () => {}
|
|
487
|
-
const template = <button data-handler={onClick}>Click</button>
|
|
488
|
-
expect(() => template.toString()).toThrow(Error)
|
|
489
|
-
})
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
// https://en.reactjs.org/docs/jsx-in-depth.html#functions-as-children
|
|
493
|
-
describe('Functions as Children', () => {
|
|
494
|
-
it('Function', () => {
|
|
495
|
-
function Repeat(props: any) {
|
|
496
|
-
const items = []
|
|
497
|
-
for (let i = 0; i < props.numTimes; i++) {
|
|
498
|
-
items.push((props.children as Function)(i))
|
|
499
|
-
}
|
|
500
|
-
return <div>{items}</div>
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function ListOfTenThings() {
|
|
504
|
-
return (
|
|
505
|
-
<Repeat numTimes={10}>
|
|
506
|
-
{(index: string) => <div key={index}>This is item {index} in the list</div>}
|
|
507
|
-
</Repeat>
|
|
508
|
-
)
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const template = <ListOfTenThings />
|
|
512
|
-
expect(template.toString()).toBe(
|
|
513
|
-
'<div><div>This is item 0 in the list</div><div>This is item 1 in the list</div><div>This is item 2 in the list</div><div>This is item 3 in the list</div><div>This is item 4 in the list</div><div>This is item 5 in the list</div><div>This is item 6 in the list</div><div>This is item 7 in the list</div><div>This is item 8 in the list</div><div>This is item 9 in the list</div></div>'
|
|
514
|
-
)
|
|
515
|
-
})
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
describe('FC', () => {
|
|
519
|
-
it('Should define the type correctly', () => {
|
|
520
|
-
const Layout: FC<PropsWithChildren<{ title: string }>> = (props) => {
|
|
521
|
-
return (
|
|
522
|
-
<html>
|
|
523
|
-
<head>
|
|
524
|
-
<title>{props.title}</title>
|
|
525
|
-
</head>
|
|
526
|
-
<body>{props.children}</body>
|
|
527
|
-
</html>
|
|
528
|
-
)
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const Top = (
|
|
532
|
-
<Layout title='Home page'>
|
|
533
|
-
<h1>Acryl</h1>
|
|
534
|
-
<p>Acryl is great</p>
|
|
535
|
-
</Layout>
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
expect(Top.toString()).toBe(
|
|
539
|
-
'<html><head><title>Home page</title></head><body><h1>Acryl</h1><p>Acryl is great</p></body></html>'
|
|
540
|
-
)
|
|
541
|
-
})
|
|
542
|
-
|
|
543
|
-
describe('Booleans, Null, and Undefined Are Ignored', () => {
|
|
544
|
-
it.each([true, false, undefined, null])('%s', (item) => {
|
|
545
|
-
const Component: FC = (() => {
|
|
546
|
-
return item
|
|
547
|
-
}) as FC
|
|
548
|
-
const template = <Component />
|
|
549
|
-
expect(template.toString()).toBe('')
|
|
550
|
-
})
|
|
551
|
-
|
|
552
|
-
it('falsy value', () => {
|
|
553
|
-
const Component: FC = (() => {
|
|
554
|
-
return 0
|
|
555
|
-
}) as unknown as FC
|
|
556
|
-
const template = <Component />
|
|
557
|
-
expect(template.toString()).toBe('0')
|
|
558
|
-
})
|
|
559
|
-
})
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
describe('style attribute', () => {
|
|
563
|
-
it('should convert the object to strings', () => {
|
|
564
|
-
const template = (
|
|
565
|
-
<h1
|
|
566
|
-
style={{
|
|
567
|
-
color: 'red',
|
|
568
|
-
fontSize: 'small',
|
|
569
|
-
fontFamily: 'Menlo, Consolas, "DejaVu Sans Mono", monospace',
|
|
570
|
-
}}
|
|
571
|
-
>
|
|
572
|
-
Hello
|
|
573
|
-
</h1>
|
|
574
|
-
)
|
|
575
|
-
expect(template.toString()).toBe(
|
|
576
|
-
'<h1 style="color:red;font-size:small;font-family:Menlo, Consolas, "DejaVu Sans Mono", monospace">Hello</h1>'
|
|
577
|
-
)
|
|
578
|
-
})
|
|
579
|
-
it('should not convert the strings', () => {
|
|
580
|
-
const template = <h1 style='color:red;font-size:small'>Hello</h1>
|
|
581
|
-
expect(template.toString()).toBe('<h1 style="color:red;font-size:small">Hello</h1>')
|
|
582
|
-
})
|
|
583
|
-
it('should render variable without any name conversion', () => {
|
|
584
|
-
const template = <h1 style={{ '--myVar': 1 }}>Hello</h1>
|
|
585
|
-
expect(template.toString()).toBe('<h1 style="--myVar:1px">Hello</h1>')
|
|
586
|
-
})
|
|
587
|
-
|
|
588
|
-
describe('CSS injection prevention', () => {
|
|
589
|
-
it('should drop style values containing ";" to prevent CSS injection', () => {
|
|
590
|
-
const userInput =
|
|
591
|
-
'transparent;background:url(https://attacker.example/a.png);position:fixed;top:0'
|
|
592
|
-
const template = <div style={{ color: userInput }} />
|
|
593
|
-
const out = template.toString() as string
|
|
594
|
-
expect(out).not.toContain('background:url')
|
|
595
|
-
expect(out).not.toContain('position:fixed')
|
|
596
|
-
expect(out).not.toContain('color:')
|
|
597
|
-
expect(out).toBe('<div style=""></div>')
|
|
598
|
-
})
|
|
599
|
-
|
|
600
|
-
it('should drop only the unsafe property and keep other safe properties', () => {
|
|
601
|
-
const template = <div style={{ color: 'red;background:blue', backgroundColor: 'white' }} />
|
|
602
|
-
const out = template.toString() as string
|
|
603
|
-
expect(out).toBe('<div style="background-color:white"></div>')
|
|
604
|
-
expect(out).not.toContain('background:blue')
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
it('should drop style values that try to hide declaration separators in CSS comments', () => {
|
|
608
|
-
const template = (
|
|
609
|
-
<div
|
|
610
|
-
style={{
|
|
611
|
-
color: 'red/*(*/;background:blue;position:fixed;top:0',
|
|
612
|
-
backgroundColor: 'white',
|
|
613
|
-
}}
|
|
614
|
-
/>
|
|
615
|
-
)
|
|
616
|
-
const out = template.toString() as string
|
|
617
|
-
expect(out).toBe('<div style="background-color:white"></div>')
|
|
618
|
-
expect(out).not.toContain('position:fixed')
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
it('should drop style values that try to expose declarations through CSS curly blocks', () => {
|
|
622
|
-
const template = (
|
|
623
|
-
<div
|
|
624
|
-
style={{
|
|
625
|
-
color: 'red{;background:blue}',
|
|
626
|
-
backgroundColor: 'white',
|
|
627
|
-
}}
|
|
628
|
-
/>
|
|
629
|
-
)
|
|
630
|
-
const out = template.toString() as string
|
|
631
|
-
expect(out).toBe('<div style="background-color:white"></div>')
|
|
632
|
-
expect(out).not.toContain('background:blue')
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
it('should drop unterminated style values that can swallow following declarations', () => {
|
|
636
|
-
const template = <div style={{ color: 'red/*', display: 'none' }} />
|
|
637
|
-
const out = template.toString() as string
|
|
638
|
-
expect(out).toBe('<div style="display:none"></div>')
|
|
639
|
-
expect(out).not.toContain('color:')
|
|
640
|
-
})
|
|
641
|
-
|
|
642
|
-
it('should drop style property names that can inject declarations', () => {
|
|
643
|
-
const template = (
|
|
644
|
-
<div
|
|
645
|
-
style={{
|
|
646
|
-
'color;background-image': 'url(https://attacker.example/a.png)',
|
|
647
|
-
backgroundColor: 'white',
|
|
648
|
-
}}
|
|
649
|
-
/>
|
|
650
|
-
)
|
|
651
|
-
const out = template.toString() as string
|
|
652
|
-
expect(out).toBe('<div style="background-color:white"></div>')
|
|
653
|
-
expect(out).not.toContain('background-image')
|
|
654
|
-
})
|
|
655
|
-
|
|
656
|
-
it('should still HTML-escape safe style values', () => {
|
|
657
|
-
const template = <h1 style={{ fontFamily: '"DejaVu Sans Mono", monospace' }}>Hello</h1>
|
|
658
|
-
expect(template.toString()).toBe(
|
|
659
|
-
'<h1 style="font-family:"DejaVu Sans Mono", monospace">Hello</h1>'
|
|
660
|
-
)
|
|
661
|
-
})
|
|
662
|
-
|
|
663
|
-
it('should keep semicolons inside quoted style values', () => {
|
|
664
|
-
const template = <h1 style={{ fontFamily: '"a;b", sans-serif' }}>Hello</h1>
|
|
665
|
-
expect(template.toString()).toBe(
|
|
666
|
-
'<h1 style="font-family:"a;b", sans-serif">Hello</h1>'
|
|
667
|
-
)
|
|
668
|
-
})
|
|
669
|
-
|
|
670
|
-
it('should drop quoted style values that use newlines to expose declaration separators', () => {
|
|
671
|
-
const template = (
|
|
672
|
-
<div
|
|
673
|
-
style={{
|
|
674
|
-
fontFamily: '"\n;background:url(https://attacker.example/a.png)',
|
|
675
|
-
backgroundColor: 'white',
|
|
676
|
-
}}
|
|
677
|
-
/>
|
|
678
|
-
)
|
|
679
|
-
expect(template.toString()).toBe('<div style="background-color:white"></div>')
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
it('should keep numeric style values working', () => {
|
|
683
|
-
const template = <div style={{ fontSize: 10, lineHeight: 1.5 }} />
|
|
684
|
-
expect(template.toString()).toBe('<div style="font-size:10px;line-height:1.5"></div>')
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
it('should not throw when style values contain ";"', () => {
|
|
688
|
-
expect(() => (<div style={{ color: 'red;evil:1' }} />).toString()).not.toThrow()
|
|
689
|
-
})
|
|
690
|
-
})
|
|
691
|
-
})
|
|
692
|
-
|
|
693
|
-
describe('HtmlEscaped in props', () => {
|
|
694
|
-
it('should not be double-escaped', () => {
|
|
695
|
-
const escapedString = html`${'<html-escaped-string>'}`
|
|
696
|
-
const template = <span data-text={escapedString}>Hello</span>
|
|
697
|
-
expect(template.toString()).toBe('<span data-text="<html-escaped-string>">Hello</span>')
|
|
698
|
-
})
|
|
699
|
-
})
|
|
700
|
-
|
|
701
|
-
describe('XSS prevention for attribute keys', () => {
|
|
702
|
-
it('Should skip attribute keys containing double quotes', () => {
|
|
703
|
-
const props: Record<string, string> = { ['" onfocus="alert(1)']: 'x' }
|
|
704
|
-
const template = <div {...props}>Hello</div>
|
|
705
|
-
expect(template.toString()).toBe('<div>Hello</div>')
|
|
706
|
-
})
|
|
707
|
-
|
|
708
|
-
it('Should skip attribute keys containing >', () => {
|
|
709
|
-
const props: Record<string, string> = { ['"><script>alert(1)</script><x x="']: 'x' }
|
|
710
|
-
const template = <div {...props}>Hello</div>
|
|
711
|
-
expect(template.toString()).toBe('<div>Hello</div>')
|
|
712
|
-
})
|
|
713
|
-
|
|
714
|
-
it('Should skip attribute keys containing <', () => {
|
|
715
|
-
const props: Record<string, string> = { ['foo<bar']: 'x' }
|
|
716
|
-
const template = <div {...props}>Hello</div>
|
|
717
|
-
expect(template.toString()).toBe('<div>Hello</div>')
|
|
718
|
-
})
|
|
719
|
-
|
|
720
|
-
it('Should skip attribute keys containing backslashes', () => {
|
|
721
|
-
const props: Record<string, string> = { ['foo\\bar']: 'x' }
|
|
722
|
-
const template = <div {...props}>Hello</div>
|
|
723
|
-
expect(template.toString()).toBe('<div>Hello</div>')
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
it('Should skip attribute keys containing backticks', () => {
|
|
727
|
-
const props: Record<string, string> = { ['foo`bar']: 'x' }
|
|
728
|
-
const template = <div {...props}>Hello</div>
|
|
729
|
-
expect(template.toString()).toBe('<div>Hello</div>')
|
|
730
|
-
})
|
|
731
|
-
|
|
732
|
-
it('Should skip attribute keys containing spaces', () => {
|
|
733
|
-
const props: Record<string, string> = { ['foo bar']: 'x' }
|
|
734
|
-
const template = <div {...props}>Hello</div>
|
|
735
|
-
expect(template.toString()).toBe('<div>Hello</div>')
|
|
736
|
-
})
|
|
737
|
-
|
|
738
|
-
it('Should still render valid attributes alongside invalid ones', () => {
|
|
739
|
-
const props: Record<string, string> = {
|
|
740
|
-
id: 'safe',
|
|
741
|
-
['" onfocus="alert(1)']: 'x',
|
|
742
|
-
class: 'test',
|
|
743
|
-
}
|
|
744
|
-
const template = <div {...props}>Hello</div>
|
|
745
|
-
expect(template.toString()).toBe('<div id="safe" class="test">Hello</div>')
|
|
746
|
-
})
|
|
747
|
-
|
|
748
|
-
it('Should skip invalid attribute keys before style serialization', () => {
|
|
749
|
-
const template = <div {...{ ['" onfocus="alert(1)']: { fontSize: 10 } }}>Hello</div>
|
|
750
|
-
expect(template.toString()).toBe('<div>Hello</div>')
|
|
751
|
-
})
|
|
752
|
-
|
|
753
|
-
it('Should skip invalid attribute keys before function prop validation', () => {
|
|
754
|
-
const template = <div {...{ ['" onfocus="alert(1)']: () => 'x' }}>Hello</div>
|
|
755
|
-
expect(template.toString()).toBe('<div>Hello</div>')
|
|
756
|
-
})
|
|
757
|
-
|
|
758
|
-
it('Should skip invalid attribute keys before dangerouslySetInnerHTML handling', () => {
|
|
759
|
-
const template = (
|
|
760
|
-
<div {...{ ['" onfocus="alert(1)']: { __html: '<strong>Injected</strong>' } }}>Hello</div>
|
|
761
|
-
)
|
|
762
|
-
expect(template.toString()).toBe('<div>Hello</div>')
|
|
763
|
-
})
|
|
764
|
-
|
|
765
|
-
it('Should skip invalid attribute keys with Promise values', async () => {
|
|
766
|
-
const template = <div {...{ ['" onfocus="alert(1)']: Promise.resolve('x') }}>Hello</div>
|
|
767
|
-
expect(await template.toString()).toBe('<div>Hello</div>')
|
|
768
|
-
})
|
|
769
|
-
})
|
|
770
|
-
|
|
771
|
-
describe('head', () => {
|
|
772
|
-
it('Simple head elements should be rendered as is', () => {
|
|
773
|
-
const template = (
|
|
774
|
-
<head>
|
|
775
|
-
<title>Acryl!</title>
|
|
776
|
-
<meta name='description' content='A description' />
|
|
777
|
-
<script src='script.js'></script>
|
|
778
|
-
</head>
|
|
779
|
-
)
|
|
780
|
-
expect(template.toString()).toBe(
|
|
781
|
-
'<head><title>Acryl!</title><meta name="description" content="A description"/><script src="script.js"></script></head>'
|
|
782
|
-
)
|
|
783
|
-
})
|
|
784
|
-
})
|
|
785
|
-
})
|
|
786
|
-
|
|
787
|
-
describe('className', () => {
|
|
788
|
-
it('should convert to class attribute for intrinsic elements', () => {
|
|
789
|
-
const template = <h1 className='h1'>Hello</h1>
|
|
790
|
-
expect(template.toString()).toBe('<h1 class="h1">Hello</h1>')
|
|
791
|
-
})
|
|
792
|
-
|
|
793
|
-
it('should convert to class attribute for custom elements', () => {
|
|
794
|
-
const template = <custom-element className='h1'>Hello</custom-element>
|
|
795
|
-
expect(template.toString()).toBe('<custom-element class="h1">Hello</custom-element>')
|
|
796
|
-
})
|
|
797
|
-
|
|
798
|
-
it('should not convert to class attribute for custom components', () => {
|
|
799
|
-
const CustomComponent: FC<{ className: string }> = ({ className }) => (
|
|
800
|
-
<div data-class-name={className}>Hello</div>
|
|
801
|
-
)
|
|
802
|
-
const template = <CustomComponent className='h1' />
|
|
803
|
-
expect(template.toString()).toBe('<div data-class-name="h1">Hello</div>')
|
|
804
|
-
})
|
|
805
|
-
})
|
|
806
|
-
|
|
807
|
-
describe('memo', () => {
|
|
808
|
-
it('does not reuse the result of a previous render', () => {
|
|
809
|
-
let counter = 0
|
|
810
|
-
const Header = memo(() => <title>Test Site {counter}</title>)
|
|
811
|
-
const Body = () => <span>{counter}</span>
|
|
812
|
-
|
|
813
|
-
let template = (
|
|
814
|
-
<html>
|
|
815
|
-
<head>
|
|
816
|
-
<Header />
|
|
817
|
-
</head>
|
|
818
|
-
<body>
|
|
819
|
-
<Body />
|
|
820
|
-
</body>
|
|
821
|
-
</html>
|
|
822
|
-
)
|
|
823
|
-
expect(template.toString()).toBe(
|
|
824
|
-
'<html><head><title>Test Site 0</title></head><body><span>0</span></body></html>'
|
|
825
|
-
)
|
|
826
|
-
|
|
827
|
-
counter++
|
|
828
|
-
template = (
|
|
829
|
-
<html>
|
|
830
|
-
<head>
|
|
831
|
-
<Header />
|
|
832
|
-
</head>
|
|
833
|
-
<body>
|
|
834
|
-
<Body />
|
|
835
|
-
</body>
|
|
836
|
-
</html>
|
|
837
|
-
)
|
|
838
|
-
expect(template.toString()).toBe(
|
|
839
|
-
'<html><head><title>Test Site 1</title></head><body><span>1</span></body></html>'
|
|
840
|
-
)
|
|
841
|
-
})
|
|
842
|
-
|
|
843
|
-
it('does not carry a context value into a later render', () => {
|
|
844
|
-
const NameContext = createContext('anonymous')
|
|
845
|
-
const Panel = memo(() => <p>{useContext(NameContext)}</p>)
|
|
846
|
-
const render = (name: string) =>
|
|
847
|
-
(
|
|
848
|
-
<NameContext.Provider value={name}>
|
|
849
|
-
<Panel />
|
|
850
|
-
</NameContext.Provider>
|
|
851
|
-
).toString()
|
|
852
|
-
|
|
853
|
-
expect(render('alice')).toBe('<p>alice</p>')
|
|
854
|
-
expect(render('bob')).toBe('<p>bob</p>')
|
|
855
|
-
})
|
|
856
|
-
|
|
857
|
-
it('props are updated', () => {
|
|
858
|
-
const Body = memo(({ counter }: { counter: number }) => <span>{counter}</span>)
|
|
859
|
-
|
|
860
|
-
let template = <Body counter={0} />
|
|
861
|
-
expect(template.toString()).toBe('<span>0</span>')
|
|
862
|
-
|
|
863
|
-
template = <Body counter={1} />
|
|
864
|
-
expect(template.toString()).toBe('<span>1</span>')
|
|
865
|
-
})
|
|
866
|
-
|
|
867
|
-
it('custom propsAreEqual is handed to the DOM renderer', () => {
|
|
868
|
-
const propsAreEqual = (_: { counter: number }, nextProps: { counter: number }) =>
|
|
869
|
-
nextProps.counter === 0
|
|
870
|
-
const Body = memo(({ counter }: { counter: number }) => <span>{counter}</span>, propsAreEqual)
|
|
871
|
-
|
|
872
|
-
expect((Body as any)[DOM_MEMO]).toBe(propsAreEqual)
|
|
873
|
-
|
|
874
|
-
expect((<Body counter={0} />).toString()).toBe('<span>0</span>')
|
|
875
|
-
expect((<Body counter={1} />).toString()).toBe('<span>1</span>')
|
|
876
|
-
})
|
|
877
|
-
})
|
|
878
|
-
|
|
879
|
-
describe('Fragment', () => {
|
|
880
|
-
it('Should render children', () => {
|
|
881
|
-
const template = (
|
|
882
|
-
<>
|
|
883
|
-
<p>1</p>
|
|
884
|
-
<p>2</p>
|
|
885
|
-
</>
|
|
886
|
-
)
|
|
887
|
-
expect(template.toString()).toBe('<p>1</p><p>2</p>')
|
|
888
|
-
})
|
|
889
|
-
|
|
890
|
-
it('Should render children - with `Fragment`', () => {
|
|
891
|
-
const template = (
|
|
892
|
-
<Fragment>
|
|
893
|
-
<p>1</p>
|
|
894
|
-
<p>2</p>
|
|
895
|
-
</Fragment>
|
|
896
|
-
)
|
|
897
|
-
expect(template.toString()).toBe('<p>1</p><p>2</p>')
|
|
898
|
-
})
|
|
899
|
-
|
|
900
|
-
it('Should render a child', () => {
|
|
901
|
-
const template = (
|
|
902
|
-
<>
|
|
903
|
-
<p>1</p>
|
|
904
|
-
</>
|
|
905
|
-
)
|
|
906
|
-
expect(template.toString()).toBe('<p>1</p>')
|
|
907
|
-
})
|
|
908
|
-
|
|
909
|
-
it('Should render a child - with `Fragment`', () => {
|
|
910
|
-
const template = (
|
|
911
|
-
<Fragment>
|
|
912
|
-
<p>1</p>
|
|
913
|
-
</Fragment>
|
|
914
|
-
)
|
|
915
|
-
expect(template.toString()).toBe('<p>1</p>')
|
|
916
|
-
})
|
|
917
|
-
|
|
918
|
-
it('Should render nothing for empty Fragment', () => {
|
|
919
|
-
const template = <></>
|
|
920
|
-
expect(template.toString()).toBe('')
|
|
921
|
-
})
|
|
922
|
-
|
|
923
|
-
it('Should render nothing for undefined', () => {
|
|
924
|
-
const template = <>{undefined}</>
|
|
925
|
-
expect(template.toString()).toBe('')
|
|
926
|
-
})
|
|
927
|
-
})
|
|
928
|
-
|
|
929
|
-
describe('StrictMode', () => {
|
|
930
|
-
it('Should render children', () => {
|
|
931
|
-
const template = (
|
|
932
|
-
<StrictMode>
|
|
933
|
-
<p>1</p>
|
|
934
|
-
<p>2</p>
|
|
935
|
-
</StrictMode>
|
|
936
|
-
)
|
|
937
|
-
expect(template.toString()).toBe('<p>1</p><p>2</p>')
|
|
938
|
-
})
|
|
939
|
-
})
|
|
940
|
-
|
|
941
|
-
describe('SVG', () => {
|
|
942
|
-
it('simple', () => {
|
|
943
|
-
const template = (
|
|
944
|
-
<svg>
|
|
945
|
-
<circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red' />
|
|
946
|
-
</svg>
|
|
947
|
-
)
|
|
948
|
-
expect(template.toString()).toBe(
|
|
949
|
-
'<svg><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red"></circle></svg>'
|
|
950
|
-
)
|
|
951
|
-
})
|
|
952
|
-
|
|
953
|
-
it('title element', () => {
|
|
954
|
-
const template = (
|
|
955
|
-
<>
|
|
956
|
-
<head>
|
|
957
|
-
<title>Document Title</title>
|
|
958
|
-
</head>
|
|
959
|
-
<svg>
|
|
960
|
-
<title>SVG Title</title>
|
|
961
|
-
</svg>
|
|
962
|
-
</>
|
|
963
|
-
)
|
|
964
|
-
expect(template.toString()).toBe(
|
|
965
|
-
'<head><title>Document Title</title></head><svg><title>SVG Title</title></svg>'
|
|
966
|
-
)
|
|
967
|
-
})
|
|
968
|
-
|
|
969
|
-
it('should skip invalid attribute keys in SVG while preserving valid ones', () => {
|
|
970
|
-
const template = (
|
|
971
|
-
<svg>
|
|
972
|
-
<g {...{ ['" onload="alert(1)']: 'x', viewBox: '0 0 10 10' }} />
|
|
973
|
-
</svg>
|
|
974
|
-
)
|
|
975
|
-
expect(template.toString()).toBe('<svg><g viewBox="0 0 10 10"></g></svg>')
|
|
976
|
-
})
|
|
977
|
-
|
|
978
|
-
it('should normalize kebab-case attributes on the <svg> root element', () => {
|
|
979
|
-
const template = <svg {...{ strokeWidth: '1.5', strokeLinecap: 'round' }} viewBox='0 0 16 16' />
|
|
980
|
-
expect(template.toString()).toBe(
|
|
981
|
-
'<svg stroke-width="1.5" stroke-linecap="round" viewBox="0 0 16 16"></svg>'
|
|
982
|
-
)
|
|
983
|
-
})
|
|
984
|
-
|
|
985
|
-
describe('attribute', () => {
|
|
986
|
-
describe('camelCase', () => {
|
|
987
|
-
test.each`
|
|
988
|
-
key
|
|
989
|
-
${'attributeName'}
|
|
990
|
-
${'baseFrequency'}
|
|
991
|
-
${'calcMode'}
|
|
992
|
-
${'clipPathUnits'}
|
|
993
|
-
${'diffuseConstant'}
|
|
994
|
-
${'edgeMode'}
|
|
995
|
-
${'filterUnits'}
|
|
996
|
-
${'gradientTransform'}
|
|
997
|
-
${'gradientUnits'}
|
|
998
|
-
${'kernelMatrix'}
|
|
999
|
-
${'kernelUnitLength'}
|
|
1000
|
-
${'keyPoints'}
|
|
1001
|
-
${'keySplines'}
|
|
1002
|
-
${'keyTimes'}
|
|
1003
|
-
${'lengthAdjust'}
|
|
1004
|
-
${'limitingConeAngle'}
|
|
1005
|
-
${'markerHeight'}
|
|
1006
|
-
${'markerUnits'}
|
|
1007
|
-
${'markerWidth'}
|
|
1008
|
-
${'maskContentUnits'}
|
|
1009
|
-
${'maskUnits'}
|
|
1010
|
-
${'numOctaves'}
|
|
1011
|
-
${'pathLength'}
|
|
1012
|
-
${'patternContentUnits'}
|
|
1013
|
-
${'patternTransform'}
|
|
1014
|
-
${'patternUnits'}
|
|
1015
|
-
${'pointsAtX'}
|
|
1016
|
-
${'pointsAtY'}
|
|
1017
|
-
${'pointsAtZ'}
|
|
1018
|
-
${'preserveAlpha'}
|
|
1019
|
-
${'preserveAspectRatio'}
|
|
1020
|
-
${'primitiveUnits'}
|
|
1021
|
-
${'refX'}
|
|
1022
|
-
${'refY'}
|
|
1023
|
-
${'repeatCount'}
|
|
1024
|
-
${'repeatDur'}
|
|
1025
|
-
${'specularConstant'}
|
|
1026
|
-
${'specularExponent'}
|
|
1027
|
-
${'spreadMethod'}
|
|
1028
|
-
${'startOffset'}
|
|
1029
|
-
${'stdDeviation'}
|
|
1030
|
-
${'stitchTiles'}
|
|
1031
|
-
${'surfaceScale'}
|
|
1032
|
-
${'crossorigin'}
|
|
1033
|
-
${'systemLanguage'}
|
|
1034
|
-
${'tableValues'}
|
|
1035
|
-
${'targetX'}
|
|
1036
|
-
${'targetY'}
|
|
1037
|
-
${'textLength'}
|
|
1038
|
-
${'viewBox'}
|
|
1039
|
-
${'xChannelSelector'}
|
|
1040
|
-
${'yChannelSelector'}
|
|
1041
|
-
`('$key', ({ key }) => {
|
|
1042
|
-
const template = (
|
|
1043
|
-
<svg>
|
|
1044
|
-
<g {...{ [key]: 'test' }} />
|
|
1045
|
-
</svg>
|
|
1046
|
-
)
|
|
1047
|
-
expect(template.toString()).toBe(`<svg><g ${key}="test"></g></svg>`)
|
|
1048
|
-
})
|
|
1049
|
-
})
|
|
1050
|
-
|
|
1051
|
-
describe('kebab-case', () => {
|
|
1052
|
-
test.each`
|
|
1053
|
-
key
|
|
1054
|
-
${'alignmentBaseline'}
|
|
1055
|
-
${'baselineShift'}
|
|
1056
|
-
${'clipPath'}
|
|
1057
|
-
${'clipRule'}
|
|
1058
|
-
${'colorInterpolation'}
|
|
1059
|
-
${'colorInterpolationFilters'}
|
|
1060
|
-
${'dominantBaseline'}
|
|
1061
|
-
${'fillOpacity'}
|
|
1062
|
-
${'fillRule'}
|
|
1063
|
-
${'floodColor'}
|
|
1064
|
-
${'floodOpacity'}
|
|
1065
|
-
${'fontFamily'}
|
|
1066
|
-
${'fontSize'}
|
|
1067
|
-
${'fontSizeAdjust'}
|
|
1068
|
-
${'fontStretch'}
|
|
1069
|
-
${'fontStyle'}
|
|
1070
|
-
${'fontVariant'}
|
|
1071
|
-
${'fontWeight'}
|
|
1072
|
-
${'imageRendering'}
|
|
1073
|
-
${'letterSpacing'}
|
|
1074
|
-
${'lightingColor'}
|
|
1075
|
-
${'markerEnd'}
|
|
1076
|
-
${'markerMid'}
|
|
1077
|
-
${'markerStart'}
|
|
1078
|
-
${'overlinePosition'}
|
|
1079
|
-
${'overlineThickness'}
|
|
1080
|
-
${'paintOrder'}
|
|
1081
|
-
${'pointerEvents'}
|
|
1082
|
-
${'shapeRendering'}
|
|
1083
|
-
${'stopColor'}
|
|
1084
|
-
${'stopOpacity'}
|
|
1085
|
-
${'strikethroughPosition'}
|
|
1086
|
-
${'strikethroughThickness'}
|
|
1087
|
-
${'strokeDasharray'}
|
|
1088
|
-
${'strokeDashoffset'}
|
|
1089
|
-
${'strokeLinecap'}
|
|
1090
|
-
${'strokeLinejoin'}
|
|
1091
|
-
${'strokeMiterlimit'}
|
|
1092
|
-
${'strokeOpacity'}
|
|
1093
|
-
${'strokeWidth'}
|
|
1094
|
-
${'textAnchor'}
|
|
1095
|
-
${'textDecoration'}
|
|
1096
|
-
${'textRendering'}
|
|
1097
|
-
${'transformOrigin'}
|
|
1098
|
-
${'underlinePosition'}
|
|
1099
|
-
${'underlineThickness'}
|
|
1100
|
-
${'unicodeBidi'}
|
|
1101
|
-
${'vectorEffect'}
|
|
1102
|
-
${'wordSpacing'}
|
|
1103
|
-
${'writingMode'}
|
|
1104
|
-
`('$key', ({ key }) => {
|
|
1105
|
-
const template = (
|
|
1106
|
-
<svg>
|
|
1107
|
-
<g {...{ [key]: 'test' }} />
|
|
1108
|
-
</svg>
|
|
1109
|
-
)
|
|
1110
|
-
expect(template.toString()).toBe(
|
|
1111
|
-
`<svg><g ${key.replace(/([A-Z])/g, '-$1').toLowerCase()}="test"></g></svg>`
|
|
1112
|
-
)
|
|
1113
|
-
})
|
|
1114
|
-
})
|
|
1115
|
-
|
|
1116
|
-
describe('data-*', () => {
|
|
1117
|
-
test.each`
|
|
1118
|
-
key
|
|
1119
|
-
${'data-foo'}
|
|
1120
|
-
${'data-foo-bar'}
|
|
1121
|
-
${'data-fooBar'}
|
|
1122
|
-
`('$key', ({ key }) => {
|
|
1123
|
-
const template = (
|
|
1124
|
-
<svg>
|
|
1125
|
-
<g {...{ [key]: 'test' }} />
|
|
1126
|
-
</svg>
|
|
1127
|
-
)
|
|
1128
|
-
expect(template.toString()).toBe(`<svg><g ${key}="test"></g></svg>`)
|
|
1129
|
-
})
|
|
1130
|
-
})
|
|
1131
|
-
})
|
|
1132
|
-
})
|
|
1133
|
-
|
|
1134
|
-
describe('Context', () => {
|
|
1135
|
-
let ThemeContext: Context<string>
|
|
1136
|
-
let Consumer: FC
|
|
1137
|
-
let ErrorConsumer: FC
|
|
1138
|
-
let AsyncConsumer: FC
|
|
1139
|
-
let AsyncErrorConsumer: FC
|
|
1140
|
-
beforeAll(() => {
|
|
1141
|
-
ThemeContext = createContext('light')
|
|
1142
|
-
Consumer = () => {
|
|
1143
|
-
const theme = useContext(ThemeContext)
|
|
1144
|
-
return <span>{theme}</span>
|
|
1145
|
-
}
|
|
1146
|
-
ErrorConsumer = () => {
|
|
1147
|
-
throw new Error('ErrorConsumer')
|
|
1148
|
-
}
|
|
1149
|
-
AsyncConsumer = async () => {
|
|
1150
|
-
const theme = useContext(ThemeContext)
|
|
1151
|
-
return <span>{theme}</span>
|
|
1152
|
-
}
|
|
1153
|
-
AsyncErrorConsumer = async () => {
|
|
1154
|
-
throw new Error('AsyncErrorConsumer')
|
|
1155
|
-
}
|
|
1156
|
-
})
|
|
1157
|
-
|
|
1158
|
-
describe('with .Provider', () => {
|
|
1159
|
-
it('has a child', () => {
|
|
1160
|
-
const template = (
|
|
1161
|
-
<ThemeContext.Provider value='dark'>
|
|
1162
|
-
<Consumer />
|
|
1163
|
-
</ThemeContext.Provider>
|
|
1164
|
-
)
|
|
1165
|
-
expect(template.toString()).toBe('<span>dark</span>')
|
|
1166
|
-
})
|
|
1167
|
-
|
|
1168
|
-
it('has children', () => {
|
|
1169
|
-
const template = (
|
|
1170
|
-
<ThemeContext.Provider value='dark'>
|
|
1171
|
-
<div>
|
|
1172
|
-
<Consumer />!
|
|
1173
|
-
</div>
|
|
1174
|
-
<div>
|
|
1175
|
-
<Consumer />!
|
|
1176
|
-
</div>
|
|
1177
|
-
</ThemeContext.Provider>
|
|
1178
|
-
)
|
|
1179
|
-
expect(template.toString()).toBe('<div><span>dark</span>!</div><div><span>dark</span>!</div>')
|
|
1180
|
-
})
|
|
1181
|
-
|
|
1182
|
-
it('escapes a single string child', async () => {
|
|
1183
|
-
const template = (
|
|
1184
|
-
<ThemeContext.Provider value='dark'>{'<img src=x onerror=alert(1)>'}</ThemeContext.Provider>
|
|
1185
|
-
)
|
|
1186
|
-
|
|
1187
|
-
expect(String(await template.toString())).toBe('<img src=x onerror=alert(1)>')
|
|
1188
|
-
})
|
|
1189
|
-
|
|
1190
|
-
it('escapes a string returned by an asynchronous child', async () => {
|
|
1191
|
-
const Async = async () => '<img src=x onerror=alert(1)>' as any
|
|
1192
|
-
const template = (
|
|
1193
|
-
<ThemeContext.Provider value='dark'>
|
|
1194
|
-
<Async />
|
|
1195
|
-
</ThemeContext.Provider>
|
|
1196
|
-
)
|
|
1197
|
-
|
|
1198
|
-
expect(String(await template.toString())).toBe('<img src=x onerror=alert(1)>')
|
|
1199
|
-
})
|
|
1200
|
-
|
|
1201
|
-
it('preserves explicitly trusted content', async () => {
|
|
1202
|
-
const template = (
|
|
1203
|
-
<ThemeContext.Provider value='dark'>
|
|
1204
|
-
{raw('<strong>trusted</strong>')}
|
|
1205
|
-
</ThemeContext.Provider>
|
|
1206
|
-
)
|
|
1207
|
-
|
|
1208
|
-
expect(await template.toString()).toBe('<strong>trusted</strong>')
|
|
1209
|
-
})
|
|
1210
|
-
|
|
1211
|
-
it('escapes untrusted object stringification', async () => {
|
|
1212
|
-
const object = { toString: () => '<img src=x onerror=alert(1)>' }
|
|
1213
|
-
const asyncObject = { toString: async () => '<img src=x onerror=alert(1)>' }
|
|
1214
|
-
const rawObject = { toString: () => raw('<img src=x onerror=alert(1)>') }
|
|
1215
|
-
|
|
1216
|
-
for (const child of [object, asyncObject, rawObject]) {
|
|
1217
|
-
const template = (
|
|
1218
|
-
<ThemeContext.Provider value='dark'>{child as never}</ThemeContext.Provider>
|
|
1219
|
-
)
|
|
1220
|
-
expect(String(await template.toString())).toBe('<img src=x onerror=alert(1)>')
|
|
1221
|
-
}
|
|
1222
|
-
})
|
|
1223
|
-
|
|
1224
|
-
it('continues to omit unsupported objects in child arrays', async () => {
|
|
1225
|
-
const object = { toString: () => '<img src=x onerror=alert(1)>' }
|
|
1226
|
-
const template = (
|
|
1227
|
-
<ThemeContext.Provider value='dark'>{[object as never, 'safe']}</ThemeContext.Provider>
|
|
1228
|
-
)
|
|
1229
|
-
|
|
1230
|
-
expect(await template.toString()).toBe('safe')
|
|
1231
|
-
})
|
|
1232
|
-
|
|
1233
|
-
it('keeps a direct Promise child unchanged', async () => {
|
|
1234
|
-
const template = (
|
|
1235
|
-
<ThemeContext.Provider value='dark'>{Promise.resolve('resolved')}</ThemeContext.Provider>
|
|
1236
|
-
)
|
|
1237
|
-
|
|
1238
|
-
expect(await template.toString()).toBe('[object Promise]')
|
|
1239
|
-
})
|
|
1240
|
-
|
|
1241
|
-
it('nested', () => {
|
|
1242
|
-
const template = (
|
|
1243
|
-
<ThemeContext.Provider value='dark'>
|
|
1244
|
-
<Consumer />
|
|
1245
|
-
<ThemeContext.Provider value='black'>
|
|
1246
|
-
<Consumer />
|
|
1247
|
-
</ThemeContext.Provider>
|
|
1248
|
-
<Consumer />
|
|
1249
|
-
</ThemeContext.Provider>
|
|
1250
|
-
)
|
|
1251
|
-
expect(template.toString()).toBe('<span>dark</span><span>black</span><span>dark</span>')
|
|
1252
|
-
})
|
|
1253
|
-
|
|
1254
|
-
it('should reset context by error', () => {
|
|
1255
|
-
const template = (
|
|
1256
|
-
<ThemeContext.Provider value='dark'>
|
|
1257
|
-
<ErrorConsumer />
|
|
1258
|
-
</ThemeContext.Provider>
|
|
1259
|
-
)
|
|
1260
|
-
expect(() => template.toString()).toThrow()
|
|
1261
|
-
|
|
1262
|
-
const nextRequest = <Consumer />
|
|
1263
|
-
expect(nextRequest.toString()).toBe('<span>light</span>')
|
|
1264
|
-
})
|
|
1265
|
-
})
|
|
1266
|
-
|
|
1267
|
-
describe('<Context> as a provider ', () => {
|
|
1268
|
-
it('has a child', () => {
|
|
1269
|
-
const template = (
|
|
1270
|
-
<ThemeContext value='dark'>
|
|
1271
|
-
<Consumer />
|
|
1272
|
-
</ThemeContext>
|
|
1273
|
-
)
|
|
1274
|
-
expect(template.toString()).toBe('<span>dark</span>')
|
|
1275
|
-
})
|
|
1276
|
-
})
|
|
1277
|
-
|
|
1278
|
-
it('default value', () => {
|
|
1279
|
-
const template = <Consumer />
|
|
1280
|
-
expect(template.toString()).toBe('<span>light</span>')
|
|
1281
|
-
})
|
|
1282
|
-
|
|
1283
|
-
describe('with Suspence', () => {
|
|
1284
|
-
const RedTheme = () => (
|
|
1285
|
-
<ThemeContext.Provider value='red'>
|
|
1286
|
-
<Consumer />
|
|
1287
|
-
</ThemeContext.Provider>
|
|
1288
|
-
)
|
|
1289
|
-
|
|
1290
|
-
it('Should preserve context in sync component', async () => {
|
|
1291
|
-
const template = (
|
|
1292
|
-
<ThemeContext.Provider value='dark'>
|
|
1293
|
-
<Suspense fallback={<RedTheme />}>
|
|
1294
|
-
<Consumer />
|
|
1295
|
-
<ThemeContext.Provider value='black'>
|
|
1296
|
-
<Consumer />
|
|
1297
|
-
</ThemeContext.Provider>
|
|
1298
|
-
</Suspense>
|
|
1299
|
-
</ThemeContext.Provider>
|
|
1300
|
-
)
|
|
1301
|
-
const stream = renderToReadableStream(template)
|
|
1302
|
-
|
|
1303
|
-
const chunks = []
|
|
1304
|
-
const textDecoder = new TextDecoder()
|
|
1305
|
-
for await (const chunk of stream as any) {
|
|
1306
|
-
chunks.push(textDecoder.decode(chunk))
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
expect(chunks).toEqual(['<span>dark</span><span>black</span>'])
|
|
1310
|
-
})
|
|
1311
|
-
|
|
1312
|
-
it('Should preserve context in async component', async () => {
|
|
1313
|
-
const template = (
|
|
1314
|
-
<ThemeContext.Provider value='dark'>
|
|
1315
|
-
<Suspense fallback={<RedTheme />}>
|
|
1316
|
-
<Consumer />
|
|
1317
|
-
<ThemeContext.Provider value='black'>
|
|
1318
|
-
<AsyncConsumer />
|
|
1319
|
-
</ThemeContext.Provider>
|
|
1320
|
-
</Suspense>
|
|
1321
|
-
</ThemeContext.Provider>
|
|
1322
|
-
)
|
|
1323
|
-
const stream = renderToReadableStream(template)
|
|
1324
|
-
|
|
1325
|
-
const chunks = []
|
|
1326
|
-
const textDecoder = new TextDecoder()
|
|
1327
|
-
for await (const chunk of stream as any) {
|
|
1328
|
-
chunks.push(textDecoder.decode(chunk))
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
expect(chunks).toEqual([
|
|
1332
|
-
'<template id="H:0"></template><span>red</span><!--/$-->',
|
|
1333
|
-
`<template data-acryl-target="H:0"><span>dark</span><span>black</span></template><script>
|
|
1334
|
-
((d,c,n) => {
|
|
1335
|
-
c=d.currentScript.previousSibling
|
|
1336
|
-
d=d.getElementById('H:0')
|
|
1337
|
-
if(!d)return
|
|
1338
|
-
do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
|
|
1339
|
-
d.replaceWith(c.content)
|
|
1340
|
-
})(document)
|
|
1341
|
-
</script>`,
|
|
1342
|
-
])
|
|
1343
|
-
})
|
|
1344
|
-
})
|
|
1345
|
-
|
|
1346
|
-
describe('async component', () => {
|
|
1347
|
-
const ParentAsyncConsumer = async () => {
|
|
1348
|
-
const theme = useContext(ThemeContext)
|
|
1349
|
-
return (
|
|
1350
|
-
<div>
|
|
1351
|
-
<span>{theme}</span>
|
|
1352
|
-
<AsyncConsumer />
|
|
1353
|
-
</div>
|
|
1354
|
-
)
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
const ParentAsyncErrorConsumer = async () => {
|
|
1358
|
-
const theme = useContext(ThemeContext)
|
|
1359
|
-
return (
|
|
1360
|
-
<div>
|
|
1361
|
-
<span>{theme}</span>
|
|
1362
|
-
<AsyncErrorConsumer />
|
|
1363
|
-
</div>
|
|
1364
|
-
)
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
it('simple', async () => {
|
|
1368
|
-
const template = (
|
|
1369
|
-
<ThemeContext.Provider value='dark'>
|
|
1370
|
-
<AsyncConsumer />
|
|
1371
|
-
</ThemeContext.Provider>
|
|
1372
|
-
)
|
|
1373
|
-
expect((await template.toString()).toString()).toBe('<span>dark</span>')
|
|
1374
|
-
})
|
|
1375
|
-
|
|
1376
|
-
it('returning an array', async () => {
|
|
1377
|
-
const ArrayConsumer = async () => {
|
|
1378
|
-
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
1379
|
-
return [<span>{useContext(ThemeContext)}</span>, <span>x</span>]
|
|
1380
|
-
}
|
|
1381
|
-
const template = (
|
|
1382
|
-
<ThemeContext.Provider value='dark'>
|
|
1383
|
-
<ArrayConsumer />
|
|
1384
|
-
</ThemeContext.Provider>
|
|
1385
|
-
)
|
|
1386
|
-
expect((await template.toString()).toString()).toBe('<span>dark</span><span>x</span>')
|
|
1387
|
-
})
|
|
1388
|
-
|
|
1389
|
-
it('isolates a shared async result between providers', async () => {
|
|
1390
|
-
const sharedResult = Promise.resolve(<Consumer />)
|
|
1391
|
-
const SharedConsumer = () => sharedResult
|
|
1392
|
-
const [dark, black] = await Promise.all([
|
|
1393
|
-
(
|
|
1394
|
-
<ThemeContext.Provider value='dark'>
|
|
1395
|
-
<SharedConsumer />
|
|
1396
|
-
</ThemeContext.Provider>
|
|
1397
|
-
).toString(),
|
|
1398
|
-
(
|
|
1399
|
-
<ThemeContext.Provider value='black'>
|
|
1400
|
-
<SharedConsumer />
|
|
1401
|
-
</ThemeContext.Provider>
|
|
1402
|
-
).toString(),
|
|
1403
|
-
])
|
|
1404
|
-
expect(dark.toString()).toBe('<span>dark</span>')
|
|
1405
|
-
expect(black.toString()).toBe('<span>black</span>')
|
|
1406
|
-
})
|
|
1407
|
-
|
|
1408
|
-
it('nested', async () => {
|
|
1409
|
-
const template = (
|
|
1410
|
-
<ThemeContext.Provider value='dark'>
|
|
1411
|
-
<ParentAsyncConsumer />
|
|
1412
|
-
</ThemeContext.Provider>
|
|
1413
|
-
)
|
|
1414
|
-
expect((await template.toString()).toString()).toBe(
|
|
1415
|
-
'<div><span>dark</span><span>dark</span></div>'
|
|
1416
|
-
)
|
|
1417
|
-
})
|
|
1418
|
-
|
|
1419
|
-
it('should reset context by error', async () => {
|
|
1420
|
-
const template = (
|
|
1421
|
-
<ThemeContext.Provider value='dark'>
|
|
1422
|
-
<ParentAsyncErrorConsumer />
|
|
1423
|
-
</ThemeContext.Provider>
|
|
1424
|
-
)
|
|
1425
|
-
await expect(async () => (await template.toString()).toString()).rejects.toThrow()
|
|
1426
|
-
|
|
1427
|
-
const nextRequest = <Consumer />
|
|
1428
|
-
expect(nextRequest.toString()).toBe('<span>light</span>')
|
|
1429
|
-
})
|
|
1430
|
-
|
|
1431
|
-
it('should keep captured context until a resumed async callback settles', async () => {
|
|
1432
|
-
let resolveReader!: () => void
|
|
1433
|
-
const readerWait = new Promise<void>((resolve) => {
|
|
1434
|
-
resolveReader = resolve
|
|
1435
|
-
})
|
|
1436
|
-
let markReaderEntered!: () => void
|
|
1437
|
-
const readerEntered = new Promise<void>((resolve) => {
|
|
1438
|
-
markReaderEntered = resolve
|
|
1439
|
-
})
|
|
1440
|
-
|
|
1441
|
-
const ResumedReader = () => {
|
|
1442
|
-
const resume = captureRenderContext()
|
|
1443
|
-
return resume(async () => {
|
|
1444
|
-
markReaderEntered()
|
|
1445
|
-
await readerWait
|
|
1446
|
-
return <>{useContext(ThemeContext)}</>
|
|
1447
|
-
})
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
const htmlPromise = (
|
|
1451
|
-
<ThemeContext.Provider value='dark'>
|
|
1452
|
-
<ResumedReader />
|
|
1453
|
-
</ThemeContext.Provider>
|
|
1454
|
-
).toString()
|
|
1455
|
-
|
|
1456
|
-
await readerEntered
|
|
1457
|
-
resolveReader()
|
|
1458
|
-
|
|
1459
|
-
expect(`${await htmlPromise}`).toBe('dark')
|
|
1460
|
-
})
|
|
1461
|
-
|
|
1462
|
-
it('should pop captured context when a resumed callback throws', async () => {
|
|
1463
|
-
const ThrowThenRead = ({ resume }: { resume: ReturnType<typeof captureRenderContext> }) => {
|
|
1464
|
-
try {
|
|
1465
|
-
resume(() => {
|
|
1466
|
-
throw new Error('boom')
|
|
1467
|
-
})
|
|
1468
|
-
} catch {}
|
|
1469
|
-
return <>{useContext(ThemeContext)}</>
|
|
1470
|
-
}
|
|
1471
|
-
const CaptureOuter = () => {
|
|
1472
|
-
const resume = captureRenderContext()
|
|
1473
|
-
return (
|
|
1474
|
-
<ThemeContext.Provider value='inner'>
|
|
1475
|
-
<ThrowThenRead resume={resume} />
|
|
1476
|
-
</ThemeContext.Provider>
|
|
1477
|
-
)
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
expect(
|
|
1481
|
-
`${await (
|
|
1482
|
-
<ThemeContext.Provider value='outer'>
|
|
1483
|
-
<CaptureOuter />
|
|
1484
|
-
</ThemeContext.Provider>
|
|
1485
|
-
).toString()}`
|
|
1486
|
-
).toBe('inner')
|
|
1487
|
-
})
|
|
1488
|
-
|
|
1489
|
-
it('should pop captured context when a resumed async callback rejects', async () => {
|
|
1490
|
-
const RejectThenRead = async ({
|
|
1491
|
-
resume,
|
|
1492
|
-
}: {
|
|
1493
|
-
resume: ReturnType<typeof captureRenderContext>
|
|
1494
|
-
}) => {
|
|
1495
|
-
try {
|
|
1496
|
-
await resume(async () => {
|
|
1497
|
-
await Promise.resolve()
|
|
1498
|
-
throw new Error('boom')
|
|
1499
|
-
})
|
|
1500
|
-
} catch {}
|
|
1501
|
-
return <>{useContext(ThemeContext)}</>
|
|
1502
|
-
}
|
|
1503
|
-
const CaptureOuter = () => {
|
|
1504
|
-
const resume = captureRenderContext()
|
|
1505
|
-
return (
|
|
1506
|
-
<ThemeContext.Provider value='inner'>
|
|
1507
|
-
<RejectThenRead resume={resume} />
|
|
1508
|
-
</ThemeContext.Provider>
|
|
1509
|
-
)
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
expect(
|
|
1513
|
-
`${await (
|
|
1514
|
-
<ThemeContext.Provider value='outer'>
|
|
1515
|
-
<CaptureOuter />
|
|
1516
|
-
</ThemeContext.Provider>
|
|
1517
|
-
).toString()}`
|
|
1518
|
-
).toBe('inner')
|
|
1519
|
-
})
|
|
1520
|
-
|
|
1521
|
-
it('should isolate async component context between concurrent requests', async () => {
|
|
1522
|
-
const app = new Acryl()
|
|
1523
|
-
const SessionContext = createContext({ username: 'guest', role: 'guest' })
|
|
1524
|
-
const waits = new Map<string, Promise<void>>()
|
|
1525
|
-
const entered = new Map<string, () => void>()
|
|
1526
|
-
|
|
1527
|
-
const AdminDashboard = async ({ wait }: { wait: Promise<void> }) => {
|
|
1528
|
-
entered.get(useContext(SessionContext).username)?.()
|
|
1529
|
-
await wait
|
|
1530
|
-
const session = useContext(SessionContext)
|
|
1531
|
-
|
|
1532
|
-
if (session.role !== 'admin') {
|
|
1533
|
-
return <div>Access Denied for {session.username}</div>
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
return <div>Welcome Admin {session.username}. Secret Data: 42</div>
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
app.get('/', (c) => {
|
|
1540
|
-
const session = {
|
|
1541
|
-
username: c.req.query('user') || 'guest',
|
|
1542
|
-
role: c.req.query('role') || 'guest',
|
|
1543
|
-
}
|
|
1544
|
-
const wait = waits.get(session.username) || Promise.resolve()
|
|
1545
|
-
|
|
1546
|
-
return c.html(
|
|
1547
|
-
<SessionContext.Provider value={session}>
|
|
1548
|
-
<AdminDashboard wait={wait} />
|
|
1549
|
-
</SessionContext.Provider>
|
|
1550
|
-
)
|
|
1551
|
-
})
|
|
1552
|
-
|
|
1553
|
-
let resolveAdmin!: () => void
|
|
1554
|
-
let resolveAttacker!: () => void
|
|
1555
|
-
waits.set(
|
|
1556
|
-
'admin',
|
|
1557
|
-
new Promise<void>((resolve) => {
|
|
1558
|
-
resolveAdmin = resolve
|
|
1559
|
-
})
|
|
1560
|
-
)
|
|
1561
|
-
waits.set(
|
|
1562
|
-
'attacker',
|
|
1563
|
-
new Promise<void>((resolve) => {
|
|
1564
|
-
resolveAttacker = resolve
|
|
1565
|
-
})
|
|
1566
|
-
)
|
|
1567
|
-
const adminEntered = new Promise<void>((resolve) => {
|
|
1568
|
-
entered.set('admin', resolve)
|
|
1569
|
-
})
|
|
1570
|
-
const attackerEntered = new Promise<void>((resolve) => {
|
|
1571
|
-
entered.set('attacker', resolve)
|
|
1572
|
-
})
|
|
1573
|
-
|
|
1574
|
-
const adminReq = app.fetch(new Request('http://localhost/?user=admin&role=admin'))
|
|
1575
|
-
const attackerReq = app.fetch(new Request('http://localhost/?user=attacker&role=guest'))
|
|
1576
|
-
|
|
1577
|
-
await Promise.all([adminEntered, attackerEntered])
|
|
1578
|
-
resolveAttacker()
|
|
1579
|
-
await Promise.resolve()
|
|
1580
|
-
resolveAdmin()
|
|
1581
|
-
|
|
1582
|
-
const [adminRes, attackerRes] = await Promise.all([adminReq, attackerReq])
|
|
1583
|
-
const adminHtml = await adminRes.text()
|
|
1584
|
-
const attackerHtml = await attackerRes.text()
|
|
1585
|
-
|
|
1586
|
-
// Each request must render with exactly its own provided value: no
|
|
1587
|
-
// cross-request leak, and no degradation to the context default.
|
|
1588
|
-
expect(adminHtml).toBe('<div>Welcome Admin admin. Secret Data: 42</div>')
|
|
1589
|
-
expect(attackerHtml).toBe('<div>Access Denied for attacker</div>')
|
|
1590
|
-
})
|
|
1591
|
-
})
|
|
1592
|
-
|
|
1593
|
-
describe('async with html helper', () => {
|
|
1594
|
-
it('should preserve context when using await before html helper', async () => {
|
|
1595
|
-
// Context was being popped before async children resolved
|
|
1596
|
-
const AsyncParentWithHtml = async (props: { children?: any }) => {
|
|
1597
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
1598
|
-
return html`<div>${props.children}</div>`
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
const template = (
|
|
1602
|
-
<ThemeContext.Provider value='dark'>
|
|
1603
|
-
<AsyncParentWithHtml>
|
|
1604
|
-
<Consumer />
|
|
1605
|
-
</AsyncParentWithHtml>
|
|
1606
|
-
</ThemeContext.Provider>
|
|
1607
|
-
)
|
|
1608
|
-
expect((await template.toString()).toString()).toBe('<div><span>dark</span></div>')
|
|
1609
|
-
})
|
|
1610
|
-
|
|
1611
|
-
it('should preserve nested context when using await before html helper', async () => {
|
|
1612
|
-
const AsyncParentWithHtml = async (props: { children?: any }) => {
|
|
1613
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
1614
|
-
return html`<div>${props.children}</div>`
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
const template = (
|
|
1618
|
-
<ThemeContext.Provider value='dark'>
|
|
1619
|
-
<AsyncParentWithHtml>
|
|
1620
|
-
<ThemeContext.Provider value='black'>
|
|
1621
|
-
<Consumer />
|
|
1622
|
-
</ThemeContext.Provider>
|
|
1623
|
-
</AsyncParentWithHtml>
|
|
1624
|
-
</ThemeContext.Provider>
|
|
1625
|
-
)
|
|
1626
|
-
expect((await template.toString()).toString()).toBe('<div><span>black</span></div>')
|
|
1627
|
-
})
|
|
1628
|
-
})
|
|
1629
|
-
})
|
|
1630
|
-
|
|
1631
|
-
describe('ErrorBoundary', () => {
|
|
1632
|
-
it('awaits thenable fallback values', async () => {
|
|
1633
|
-
const Broken = () => {
|
|
1634
|
-
throw new Error('boom')
|
|
1635
|
-
}
|
|
1636
|
-
const fallback = {
|
|
1637
|
-
then(resolve: (value: unknown) => void) {
|
|
1638
|
-
resolve(<span>Recovered</span>)
|
|
1639
|
-
},
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
expect(
|
|
1643
|
-
`${await (
|
|
1644
|
-
<ErrorBoundary fallback={fallback as any}>
|
|
1645
|
-
<Broken />
|
|
1646
|
-
</ErrorBoundary>
|
|
1647
|
-
).toString()}`
|
|
1648
|
-
).toBe('<span>Recovered</span>')
|
|
1649
|
-
})
|
|
1650
|
-
|
|
1651
|
-
it('prefers an empty fallback over fallbackRender', async () => {
|
|
1652
|
-
const Broken = () => {
|
|
1653
|
-
throw new Error('boom')
|
|
1654
|
-
}
|
|
1655
|
-
let fallbackRenderCalled = false
|
|
1656
|
-
|
|
1657
|
-
expect(
|
|
1658
|
-
`${await (
|
|
1659
|
-
<ErrorBoundary
|
|
1660
|
-
fallback=''
|
|
1661
|
-
fallbackRender={() => {
|
|
1662
|
-
fallbackRenderCalled = true
|
|
1663
|
-
return <span>fallbackRender</span>
|
|
1664
|
-
}}
|
|
1665
|
-
>
|
|
1666
|
-
<Broken />
|
|
1667
|
-
</ErrorBoundary>
|
|
1668
|
-
).toString()}`
|
|
1669
|
-
).toBe('')
|
|
1670
|
-
expect(fallbackRenderCalled).toBe(false)
|
|
1671
|
-
})
|
|
1672
|
-
|
|
1673
|
-
it('renders async rejection fallback with the captured context while streaming', async () => {
|
|
1674
|
-
const ThemeContext = createContext('default')
|
|
1675
|
-
const Fallback = () => <span>{useContext(ThemeContext)}</span>
|
|
1676
|
-
const Broken = async () => {
|
|
1677
|
-
await Promise.resolve()
|
|
1678
|
-
throw new Error('boom')
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
const stream = renderToReadableStream(
|
|
1682
|
-
<ThemeContext.Provider value='outer'>
|
|
1683
|
-
<ErrorBoundary fallbackRender={() => <Fallback />}>
|
|
1684
|
-
<Broken />
|
|
1685
|
-
</ErrorBoundary>
|
|
1686
|
-
</ThemeContext.Provider>
|
|
1687
|
-
)
|
|
1688
|
-
const decoder = new TextDecoder()
|
|
1689
|
-
let html = ''
|
|
1690
|
-
for await (const chunk of stream) {
|
|
1691
|
-
html += decoder.decode(chunk)
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
expect(html).toContain('<span>outer</span>')
|
|
1695
|
-
expect(html).not.toContain('<span>default</span>')
|
|
1696
|
-
})
|
|
1697
|
-
})
|
|
1698
|
-
|
|
1699
|
-
describe('version', () => {
|
|
1700
|
-
it('should be defined with semantic versioning format', () => {
|
|
1701
|
-
expect(version).toMatch(/^\d+\.\d+\.\d+-acryl-jsx$/)
|
|
1702
|
-
})
|
|
1703
|
-
})
|
|
1704
|
-
|
|
1705
|
-
describe('default export', () => {
|
|
1706
|
-
;[
|
|
1707
|
-
'version',
|
|
1708
|
-
'memo',
|
|
1709
|
-
'Fragment',
|
|
1710
|
-
'isValidElement',
|
|
1711
|
-
'createElement',
|
|
1712
|
-
'cloneElement',
|
|
1713
|
-
'ErrorBoundary',
|
|
1714
|
-
'createContext',
|
|
1715
|
-
'useContext',
|
|
1716
|
-
'useState',
|
|
1717
|
-
'useEffect',
|
|
1718
|
-
'useRef',
|
|
1719
|
-
'useCallback',
|
|
1720
|
-
'useReducer',
|
|
1721
|
-
'useDebugValue',
|
|
1722
|
-
'createRef',
|
|
1723
|
-
'forwardRef',
|
|
1724
|
-
'useImperativeHandle',
|
|
1725
|
-
'useSyncExternalStore',
|
|
1726
|
-
'use',
|
|
1727
|
-
'startTransition',
|
|
1728
|
-
'useTransition',
|
|
1729
|
-
'useDeferredValue',
|
|
1730
|
-
'startViewTransition',
|
|
1731
|
-
'useViewTransition',
|
|
1732
|
-
'useMemo',
|
|
1733
|
-
'useLayoutEffect',
|
|
1734
|
-
'useInsertionEffect',
|
|
1735
|
-
'useActionState',
|
|
1736
|
-
'useOptimistic',
|
|
1737
|
-
'Suspense',
|
|
1738
|
-
'StrictMode',
|
|
1739
|
-
].forEach((key) => {
|
|
1740
|
-
it(key, () => {
|
|
1741
|
-
expect((DefaultExport as any)[key]).toBeDefined()
|
|
1742
|
-
})
|
|
1743
|
-
})
|
|
1744
|
-
})
|