quickwin 1.0.1 → 1.1.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.
Files changed (79) hide show
  1. package/README.en.md +204 -204
  2. package/README.md +203 -203
  3. package/examples/PdfCanvas.tsx +83 -83
  4. package/examples/exec_server.js +25 -3
  5. package/examples/exec_server.ts +35 -17
  6. package/examples/pdf_preview2.ts +375 -375
  7. package/examples/pdf_viewer.tsx +279 -279
  8. package/examples/test_gallery.tsx +285 -285
  9. package/examples/test_react_lazy.tsx +63 -63
  10. package/examples/test_react_listbox.tsx +71 -71
  11. package/examples/test_root_window.tsx +41 -41
  12. package/examples/test_tab.tsx +75 -75
  13. package/examples/tray_demo.tsx +81 -81
  14. package/lib/assert.ts +6 -6
  15. package/lib/cache-utils.ts +40 -40
  16. package/lib/dumpRects.ts +25 -25
  17. package/lib/fetch.ts +502 -502
  18. package/lib/http-common.ts +399 -399
  19. package/lib/http-server.ts +424 -424
  20. package/lib/polyfill.ts +171 -171
  21. package/lib/react-qw/components/index.ts +17 -17
  22. package/lib/react-qw/index.ts +90 -90
  23. package/lib/react-qw/jsx.d.ts +34 -34
  24. package/lib/react-qw/layout.ts +111 -111
  25. package/lib/react-qw/props.ts +36 -36
  26. package/lib/react-qw/reconciler.ts +373 -373
  27. package/lib/stream.ts +21 -21
  28. package/lib/text-codec.ts +171 -171
  29. package/lib/text-measure.ts +54 -54
  30. package/lib/url.ts +201 -201
  31. package/lib/vendor/web-streams/ponyfill.d.mts +829 -829
  32. package/lib/vendor/web-streams/ponyfill.mjs +8 -8
  33. package/lib/websocket.js +8 -0
  34. package/lib/websocket.ts +563 -555
  35. package/package.json +50 -44
  36. package/qwin-nowasm-x86.exe +0 -0
  37. package/qwin-nowasm.exe +0 -0
  38. package/qwin-x86.exe +0 -0
  39. package/qwin.exe +0 -0
  40. package/test/run.ts +96 -96
  41. package/test/test_basic.ts +9 -9
  42. package/test/test_basic_fetch.ts +33 -33
  43. package/test/test_brotli.ts +52 -52
  44. package/test/test_fetch_cache.ts +187 -187
  45. package/test/test_ffi.ts +174 -174
  46. package/test/test_frame_encoding.ts +132 -132
  47. package/test/test_helper.ts +80 -80
  48. package/test/test_http_import.ts +74 -74
  49. package/test/test_http_server.ts +136 -136
  50. package/test/test_import_dir.ts +5 -5
  51. package/test/test_ipv6.ts +129 -129
  52. package/test/test_meta_url.ts +4 -4
  53. package/test/test_mupdf_render.ts +74 -74
  54. package/test/test_mupdf_twice.ts +81 -81
  55. package/test/test_mupdf_wasm.ts +30 -30
  56. package/test/test_net_event.ts +55 -55
  57. package/test/test_net_fetch.ts +238 -238
  58. package/test/test_net_websocket.ts +164 -164
  59. package/test/test_polyfill.ts +159 -159
  60. package/test/test_url.ts +183 -183
  61. package/test/test_wasm_basic.ts +73 -73
  62. package/test/test_wasm_import_global.ts +45 -45
  63. package/test/test_wasm_sjlj.ts +125 -125
  64. package/test/test_wasm_types.ts +124 -124
  65. package/test/test_worker.ts +24 -24
  66. package/test/test_worker_fetch.ts +27 -27
  67. package/test/test_worker_net.ts +54 -54
  68. package/test/test_worker_wasm_concurrent.ts +45 -45
  69. package/test/worker_fetch.ts +13 -13
  70. package/test/worker_helper.ts +1 -1
  71. package/test/worker_http.ts +20 -20
  72. package/test/worker_import_lib.ts +13 -13
  73. package/test/worker_module.ts +17 -17
  74. package/test/worker_wasm_concurrent.ts +22 -22
  75. package/tsconfig.json +27 -27
  76. package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -571
  77. package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -2749
  78. package/vendor/mupdf-wasm/mupdf.d.ts +939 -939
  79. package/vendor/mupdf-wasm/mupdf.js +3317 -3317
@@ -1,55 +1,55 @@
1
- import * as sock from "sock"
2
- import * as os from "os"
3
- import { Tester } from './test_helper.js'
4
-
5
- interface WSAEvent {
6
- lNetworkEvents: number;
7
- iErrorCode: number[];
8
- }
9
-
10
- export const suite = {
11
- name: 'net-event',
12
- run: async (t: Tester): Promise<void> => {
13
- await new Promise<void>((resolve, reject) => {
14
- const s = sock.socket()
15
- if (s === null || s < 0) { t.fail++; reject(new Error('socket() failed')); return }
16
-
17
- var connected = false, gotData = false
18
-
19
- const timeoutId = os.setTimeout(() => {
20
- if (!connected) {
21
- sock.closesocket(s)
22
- reject(new Error('connect timeout'))
23
- }
24
- }, 5000)
25
-
26
- sock.set_on_event(s, (event: WSAEvent) => {
27
- if (event.lNetworkEvents & sock.FdEvent.FD_CONNECT) {
28
- if (event.iErrorCode[0] === 0) {
29
- connected = true; t.ok++
30
- os.clearTimeout(timeoutId)
31
- const req = "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
32
- const buf = new ArrayBuffer(req.length)
33
- const v = new Uint8Array(buf)
34
- for (let i = 0; i < req.length; i++) v[i] = req.charCodeAt(i)
35
- sock.send(s, buf)
36
- } else {
37
- sock.closesocket(s)
38
- reject(new Error('connect error: ' + event.iErrorCode[0]))
39
- }
40
- }
41
- if (event.lNetworkEvents & sock.FdEvent.FD_READ && connected && !gotData) {
42
- const data = sock.recv(s, 4096)
43
- if (data && data.byteLength > 0) { gotData = true; t.ok++ }
44
- }
45
- if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
46
- os.clearTimeout(timeoutId)
47
- sock.closesocket(s)
48
- if (gotData && connected) resolve()
49
- else reject(new Error('test incomplete'))
50
- }
51
- })
52
- sock.connect(s, '127.0.0.1', 18923)
53
- })
54
- }
55
- }
1
+ import * as sock from "sock"
2
+ import * as os from "os"
3
+ import { Tester } from './test_helper.js'
4
+
5
+ interface WSAEvent {
6
+ lNetworkEvents: number;
7
+ iErrorCode: number[];
8
+ }
9
+
10
+ export const suite = {
11
+ name: 'net-event',
12
+ run: async (t: Tester): Promise<void> => {
13
+ await new Promise<void>((resolve, reject) => {
14
+ const s = sock.socket()
15
+ if (s === null || s < 0) { t.fail++; reject(new Error('socket() failed')); return }
16
+
17
+ var connected = false, gotData = false
18
+
19
+ const timeoutId = os.setTimeout(() => {
20
+ if (!connected) {
21
+ sock.closesocket(s)
22
+ reject(new Error('connect timeout'))
23
+ }
24
+ }, 5000)
25
+
26
+ sock.set_on_event(s, (event: WSAEvent) => {
27
+ if (event.lNetworkEvents & sock.FdEvent.FD_CONNECT) {
28
+ if (event.iErrorCode[0] === 0) {
29
+ connected = true; t.ok++
30
+ os.clearTimeout(timeoutId)
31
+ const req = "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
32
+ const buf = new ArrayBuffer(req.length)
33
+ const v = new Uint8Array(buf)
34
+ for (let i = 0; i < req.length; i++) v[i] = req.charCodeAt(i)
35
+ sock.send(s, buf)
36
+ } else {
37
+ sock.closesocket(s)
38
+ reject(new Error('connect error: ' + event.iErrorCode[0]))
39
+ }
40
+ }
41
+ if (event.lNetworkEvents & sock.FdEvent.FD_READ && connected && !gotData) {
42
+ const data = sock.recv(s, 4096)
43
+ if (data && data.byteLength > 0) { gotData = true; t.ok++ }
44
+ }
45
+ if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
46
+ os.clearTimeout(timeoutId)
47
+ sock.closesocket(s)
48
+ if (gotData && connected) resolve()
49
+ else reject(new Error('test incomplete'))
50
+ }
51
+ })
52
+ sock.connect(s, '127.0.0.1', 18923)
53
+ })
54
+ }
55
+ }
@@ -1,238 +1,238 @@
1
- import '../lib/fetch.js'
2
- import * as std from 'std'
3
- import { Tester } from './test_helper.js'
4
-
5
- export const suite = {
6
- name: 'net-fetch',
7
- run: async (t: Tester) => {
8
- function assert(name: string, ok: boolean): void {
9
- if (ok) { t.ok++; std.printf(' PASS: %s\n', name) }
10
- else { t.fail++; std.printf(' FAIL: %s\n', name) }
11
- }
12
-
13
- async function safeFetch(url: any, init?: RequestInit) {
14
- try {
15
- return await fetch(url, init);
16
- } catch {
17
- return null;
18
- }
19
- }
20
- const BASE = 'http://localhost:18923'
21
-
22
- // ── Plain HTTP GET (regression: slot 0 + !s bug) ──
23
- t.section('plain HTTP')
24
- const r0 = await safeFetch(BASE + '/', { timeout: 5000 })
25
- if (r0) {
26
- assert('plain HTTP status 200', r0.status === 200)
27
- const body = await r0.text()
28
- assert('plain HTTP body non-empty', body.length > 0)
29
- assert('plain HTTP body matches', body === 'hello from test server')
30
- } else {
31
- assert('plain HTTP endpoint reachable', false)
32
- }
33
-
34
- // ── Basic HTTP GET ──
35
- t.section('HTTP GET')
36
- const r1 = await safeFetch(BASE + '/any')
37
- if (r1) {
38
- assert('status received', r1.status > 0)
39
- const body = await r1.text()
40
- assert('body received', body.length > 0)
41
- } else {
42
- assert('local server reachable', false)
43
- }
44
-
45
- // ── Query string ──
46
- t.section('query string')
47
- const rq = await safeFetch(BASE + '/any?foo=bar&baz=42')
48
- if (rq) {
49
- const body = JSON.parse(await rq.text())
50
- assert('query args received', body.args !== undefined)
51
- assert('?foo=bar preserved', body.args.foo === 'bar')
52
- assert('?baz=42 preserved', body.args.baz === '42')
53
- } else {
54
- assert('query string endpoint reachable', false)
55
- }
56
-
57
- // ── Request constructor ──
58
- t.section('Request constructor')
59
- {
60
- const r = new Request('https://httpbun.com/any?x=1&y=2', { method: 'POST', headers: { 'X-Test': 'val' }, body: 'hello' })
61
- assert('Request.url', r.url === 'https://httpbun.com/any?x=1&y=2')
62
- assert('Request.method', r.method === 'POST')
63
- assert('Request.headers get', r.headers.get('x-test') === 'val')
64
- assert('Request.body is ReadableStream', r.body !== null && typeof r.body.getReader === 'function')
65
- }
66
- {
67
- const r1 = new Request('https://example.com/path')
68
- assert('Request default method GET', r1.method === 'GET')
69
- assert('Request default redirect follow', r1.redirect === 'follow')
70
- }
71
- {
72
- const r2 = new Request('https://httpbun.com/any')
73
- const r3 = new Request(r2, { method: 'PUT' })
74
- assert('Request clone same url', r3.url === r2.url)
75
- assert('Request clone override method', r3.method === 'PUT')
76
- }
77
- {
78
- const r = await safeFetch(new Request(BASE + '/any', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: 'req-class' }))
79
- if (r) {
80
- const body = JSON.parse(await r.text())
81
- assert('fetch(Request) status', r.status > 0)
82
- assert('fetch(Request) method POST', body.method === 'POST')
83
- assert('fetch(Request) body received', body.data === 'req-class')
84
- assert('fetch(Request) content-type', body.headers['content-type'] === 'text/plain')
85
- } else {
86
- assert('fetch(Request) endpoint reachable', false)
87
- }
88
- }
89
-
90
- // ── body / bodyUsed / stream ──
91
- t.section('body / bodyUsed / stream')
92
- const r2 = await safeFetch(BASE + '/any')
93
- if (r2) {
94
- assert('r.body exists', typeof r2.body === 'object' && r2.body !== null)
95
- assert('bodyUsed false before read', r2.bodyUsed === false)
96
-
97
- const reader = r2.body!.getReader()
98
- let total = 0
99
- while (true) {
100
- const { done, value } = await reader.read()
101
- if (done) break
102
- total += value.length
103
- }
104
- assert('reader streams all bytes', total > 0)
105
- assert('bodyUsed true after stream', r2.bodyUsed === true)
106
- } else {
107
- assert('body test endpoint reachable', false)
108
- }
109
-
110
- // ── text/json/arrayBuffer ──
111
- t.section('text / json / arrayBuffer')
112
- const r3 = await safeFetch(BASE + '/any')
113
- if (r3) {
114
- const text = await r3.text()
115
- assert('text() returns string', typeof text === 'string' && text.length > 0)
116
- assert('bodyUsed true after text()', r3.bodyUsed === true)
117
-
118
- const r4 = await safeFetch(BASE + '/any')
119
- if (r4) {
120
- const buf = await r4.arrayBuffer()
121
- assert('arrayBuffer() returns bytes', buf.byteLength > 0)
122
- }
123
- } else {
124
- assert('text/json/ab endpoint reachable', false)
125
- }
126
-
127
- // double text() throws
128
- t.section('bodyUsed throws')
129
- const r5 = await safeFetch(BASE + '/any')
130
- if (r5) {
131
- await r5.text()
132
- try { await r5.text(); assert('double text() throws', false) }
133
- catch (e: unknown) { assert('double text() throws TypeError', (e as Error).message === 'Body already used') }
134
- }
135
-
136
- // ── POST ──
137
- t.section('POST')
138
- const r6 = await safeFetch(BASE + '/any', {
139
- method: 'POST',
140
- headers: { 'Content-Type': 'application/json' },
141
- body: JSON.stringify({ msg: 'hello' })
142
- })
143
- if (r6) {
144
- const body = await r6.text()
145
- assert('POST body received', body.length > 0)
146
- } else {
147
- assert('POST endpoint reachable', false)
148
- }
149
-
150
- // ── Headers (local, no network) ──
151
- t.section('Headers (local)')
152
- const h = new Headers({ 'Content-Type': 'text/html', 'X-Custom': 'hello', 'Accept': '*/*' })
153
- let c = 0
154
- for (const [k, v] of h) { c++; if (typeof k !== 'string' || typeof v !== 'string') assert('for...of type', false) }
155
- assert('for...of yields entries', c === 3)
156
- assert('get()', h.get('content-type') === 'text/html')
157
- assert('get() case-insensitive', h.get('Content-Type') === 'text/html')
158
- assert('has()', h.has('x-custom'))
159
- assert('set() overwrites', (h.set('accept', 'application/json'), h.get('accept') === 'application/json'))
160
- assert('append() adds', (h.append('accept', 'text/plain'), h.get('accept') === 'application/json, text/plain'))
161
- assert('delete()', (h.delete('accept'), h.has('accept') === false))
162
-
163
- const h2 = new Headers({ 'a': '1', 'b': '2' })
164
- let ec = 0; for (const _ of h2.entries()) { ec++ }
165
- assert('entries()', ec === 2)
166
- let kc = 0; for (const _ of h2.keys()) { kc++ }
167
- assert('keys()', kc === 2)
168
- let vc = 0; for (const _ of h2.values()) { vc++ }
169
- assert('values()', vc === 2)
170
-
171
- // ── Chunked Transfer-Encoding ──
172
- t.section('chunked transfer encoding')
173
- const r8 = await safeFetch(BASE + '/anything')
174
- if (r8) {
175
- assert('chunked status 200', r8.status === 200)
176
- const body = await r8.json<{ url: string }>()
177
- assert('chunked json has url', typeof body.url === 'string' && body.url.length > 0)
178
- } else {
179
- assert('local server reachable', false)
180
- }
181
-
182
- // ── stream cancel ──
183
- t.section('stream cancel')
184
- const r7 = await safeFetch(BASE + '/any')
185
- if (r7) {
186
- r7.body!.cancel()
187
- assert('cancel() succeeds', true)
188
- // After cancel: buffered chunks still readable, then done
189
- const reader = r7.body!.getReader()
190
- let finalDone = false
191
- while (true) {
192
- const { done } = await reader.read()
193
- if (done) { finalDone = true; break }
194
- }
195
- assert('reader done after cancel', finalDone === true)
196
- } else {
197
- assert('cancel test endpoint reachable', false)
198
- }
199
-
200
- // ── UTF-8 Chinese text body ──
201
- t.section('utf-8 chinese text')
202
- const r9 = await safeFetch(BASE + '/base64/5Lit5paH')
203
- if (r9) {
204
- assert('chinese status 200', r9.status === 200)
205
- const body = await r9.text()
206
- assert('chinese body is 中文', body === '\u4e2d\u6587')
207
- assert('chinese body length=2', body.length === 2)
208
- } else {
209
- assert('local server reachable', false)
210
- }
211
-
212
- // ── POST with Chinese body ──
213
- t.section('post with chinese body')
214
- const r10 = await safeFetch(BASE + '/post', {
215
- method: 'POST',
216
- body: '\u4e2d\u6587',
217
- headers: { 'Content-Type': 'text/plain; charset=utf-8' }
218
- })
219
- if (r10) {
220
- assert('post status 200', r10.status === 200)
221
- const body = await r10.text()
222
- assert('post body contains 中文', body.includes('\u4e2d\u6587'))
223
- } else {
224
- assert('local server reachable', false)
225
- }
226
-
227
- // ── HTTPS fetch ──
228
- t.section('HTTPS fetch')
229
- const r11 = await safeFetch('https://localhost:18924/', { timeout: 5000 })
230
- if (r11) {
231
- assert('https status 200', r11.status === 200)
232
- const body = await r11.text()
233
- assert('https body matches', body === 'hello from test server')
234
- } else {
235
- assert('https endpoint reachable', false)
236
- }
237
- }
238
- }
1
+ import '../lib/fetch.js'
2
+ import * as std from 'std'
3
+ import { Tester } from './test_helper.js'
4
+
5
+ export const suite = {
6
+ name: 'net-fetch',
7
+ run: async (t: Tester) => {
8
+ function assert(name: string, ok: boolean): void {
9
+ if (ok) { t.ok++; std.printf(' PASS: %s\n', name) }
10
+ else { t.fail++; std.printf(' FAIL: %s\n', name) }
11
+ }
12
+
13
+ async function safeFetch(url: any, init?: RequestInit) {
14
+ try {
15
+ return await fetch(url, init);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ const BASE = 'http://localhost:18923'
21
+
22
+ // ── Plain HTTP GET (regression: slot 0 + !s bug) ──
23
+ t.section('plain HTTP')
24
+ const r0 = await safeFetch(BASE + '/', { timeout: 5000 })
25
+ if (r0) {
26
+ assert('plain HTTP status 200', r0.status === 200)
27
+ const body = await r0.text()
28
+ assert('plain HTTP body non-empty', body.length > 0)
29
+ assert('plain HTTP body matches', body === 'hello from test server')
30
+ } else {
31
+ assert('plain HTTP endpoint reachable', false)
32
+ }
33
+
34
+ // ── Basic HTTP GET ──
35
+ t.section('HTTP GET')
36
+ const r1 = await safeFetch(BASE + '/any')
37
+ if (r1) {
38
+ assert('status received', r1.status > 0)
39
+ const body = await r1.text()
40
+ assert('body received', body.length > 0)
41
+ } else {
42
+ assert('local server reachable', false)
43
+ }
44
+
45
+ // ── Query string ──
46
+ t.section('query string')
47
+ const rq = await safeFetch(BASE + '/any?foo=bar&baz=42')
48
+ if (rq) {
49
+ const body = JSON.parse(await rq.text())
50
+ assert('query args received', body.args !== undefined)
51
+ assert('?foo=bar preserved', body.args.foo === 'bar')
52
+ assert('?baz=42 preserved', body.args.baz === '42')
53
+ } else {
54
+ assert('query string endpoint reachable', false)
55
+ }
56
+
57
+ // ── Request constructor ──
58
+ t.section('Request constructor')
59
+ {
60
+ const r = new Request('https://httpbun.com/any?x=1&y=2', { method: 'POST', headers: { 'X-Test': 'val' }, body: 'hello' })
61
+ assert('Request.url', r.url === 'https://httpbun.com/any?x=1&y=2')
62
+ assert('Request.method', r.method === 'POST')
63
+ assert('Request.headers get', r.headers.get('x-test') === 'val')
64
+ assert('Request.body is ReadableStream', r.body !== null && typeof r.body.getReader === 'function')
65
+ }
66
+ {
67
+ const r1 = new Request('https://example.com/path')
68
+ assert('Request default method GET', r1.method === 'GET')
69
+ assert('Request default redirect follow', r1.redirect === 'follow')
70
+ }
71
+ {
72
+ const r2 = new Request('https://httpbun.com/any')
73
+ const r3 = new Request(r2, { method: 'PUT' })
74
+ assert('Request clone same url', r3.url === r2.url)
75
+ assert('Request clone override method', r3.method === 'PUT')
76
+ }
77
+ {
78
+ const r = await safeFetch(new Request(BASE + '/any', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: 'req-class' }))
79
+ if (r) {
80
+ const body = JSON.parse(await r.text())
81
+ assert('fetch(Request) status', r.status > 0)
82
+ assert('fetch(Request) method POST', body.method === 'POST')
83
+ assert('fetch(Request) body received', body.data === 'req-class')
84
+ assert('fetch(Request) content-type', body.headers['content-type'] === 'text/plain')
85
+ } else {
86
+ assert('fetch(Request) endpoint reachable', false)
87
+ }
88
+ }
89
+
90
+ // ── body / bodyUsed / stream ──
91
+ t.section('body / bodyUsed / stream')
92
+ const r2 = await safeFetch(BASE + '/any')
93
+ if (r2) {
94
+ assert('r.body exists', typeof r2.body === 'object' && r2.body !== null)
95
+ assert('bodyUsed false before read', r2.bodyUsed === false)
96
+
97
+ const reader = r2.body!.getReader()
98
+ let total = 0
99
+ while (true) {
100
+ const { done, value } = await reader.read()
101
+ if (done) break
102
+ total += value.length
103
+ }
104
+ assert('reader streams all bytes', total > 0)
105
+ assert('bodyUsed true after stream', r2.bodyUsed === true)
106
+ } else {
107
+ assert('body test endpoint reachable', false)
108
+ }
109
+
110
+ // ── text/json/arrayBuffer ──
111
+ t.section('text / json / arrayBuffer')
112
+ const r3 = await safeFetch(BASE + '/any')
113
+ if (r3) {
114
+ const text = await r3.text()
115
+ assert('text() returns string', typeof text === 'string' && text.length > 0)
116
+ assert('bodyUsed true after text()', r3.bodyUsed === true)
117
+
118
+ const r4 = await safeFetch(BASE + '/any')
119
+ if (r4) {
120
+ const buf = await r4.arrayBuffer()
121
+ assert('arrayBuffer() returns bytes', buf.byteLength > 0)
122
+ }
123
+ } else {
124
+ assert('text/json/ab endpoint reachable', false)
125
+ }
126
+
127
+ // double text() throws
128
+ t.section('bodyUsed throws')
129
+ const r5 = await safeFetch(BASE + '/any')
130
+ if (r5) {
131
+ await r5.text()
132
+ try { await r5.text(); assert('double text() throws', false) }
133
+ catch (e: unknown) { assert('double text() throws TypeError', (e as Error).message === 'Body already used') }
134
+ }
135
+
136
+ // ── POST ──
137
+ t.section('POST')
138
+ const r6 = await safeFetch(BASE + '/any', {
139
+ method: 'POST',
140
+ headers: { 'Content-Type': 'application/json' },
141
+ body: JSON.stringify({ msg: 'hello' })
142
+ })
143
+ if (r6) {
144
+ const body = await r6.text()
145
+ assert('POST body received', body.length > 0)
146
+ } else {
147
+ assert('POST endpoint reachable', false)
148
+ }
149
+
150
+ // ── Headers (local, no network) ──
151
+ t.section('Headers (local)')
152
+ const h = new Headers({ 'Content-Type': 'text/html', 'X-Custom': 'hello', 'Accept': '*/*' })
153
+ let c = 0
154
+ for (const [k, v] of h) { c++; if (typeof k !== 'string' || typeof v !== 'string') assert('for...of type', false) }
155
+ assert('for...of yields entries', c === 3)
156
+ assert('get()', h.get('content-type') === 'text/html')
157
+ assert('get() case-insensitive', h.get('Content-Type') === 'text/html')
158
+ assert('has()', h.has('x-custom'))
159
+ assert('set() overwrites', (h.set('accept', 'application/json'), h.get('accept') === 'application/json'))
160
+ assert('append() adds', (h.append('accept', 'text/plain'), h.get('accept') === 'application/json, text/plain'))
161
+ assert('delete()', (h.delete('accept'), h.has('accept') === false))
162
+
163
+ const h2 = new Headers({ 'a': '1', 'b': '2' })
164
+ let ec = 0; for (const _ of h2.entries()) { ec++ }
165
+ assert('entries()', ec === 2)
166
+ let kc = 0; for (const _ of h2.keys()) { kc++ }
167
+ assert('keys()', kc === 2)
168
+ let vc = 0; for (const _ of h2.values()) { vc++ }
169
+ assert('values()', vc === 2)
170
+
171
+ // ── Chunked Transfer-Encoding ──
172
+ t.section('chunked transfer encoding')
173
+ const r8 = await safeFetch(BASE + '/anything')
174
+ if (r8) {
175
+ assert('chunked status 200', r8.status === 200)
176
+ const body = await r8.json<{ url: string }>()
177
+ assert('chunked json has url', typeof body.url === 'string' && body.url.length > 0)
178
+ } else {
179
+ assert('local server reachable', false)
180
+ }
181
+
182
+ // ── stream cancel ──
183
+ t.section('stream cancel')
184
+ const r7 = await safeFetch(BASE + '/any')
185
+ if (r7) {
186
+ r7.body!.cancel()
187
+ assert('cancel() succeeds', true)
188
+ // After cancel: buffered chunks still readable, then done
189
+ const reader = r7.body!.getReader()
190
+ let finalDone = false
191
+ while (true) {
192
+ const { done } = await reader.read()
193
+ if (done) { finalDone = true; break }
194
+ }
195
+ assert('reader done after cancel', finalDone === true)
196
+ } else {
197
+ assert('cancel test endpoint reachable', false)
198
+ }
199
+
200
+ // ── UTF-8 Chinese text body ──
201
+ t.section('utf-8 chinese text')
202
+ const r9 = await safeFetch(BASE + '/base64/5Lit5paH')
203
+ if (r9) {
204
+ assert('chinese status 200', r9.status === 200)
205
+ const body = await r9.text()
206
+ assert('chinese body is 中文', body === '\u4e2d\u6587')
207
+ assert('chinese body length=2', body.length === 2)
208
+ } else {
209
+ assert('local server reachable', false)
210
+ }
211
+
212
+ // ── POST with Chinese body ──
213
+ t.section('post with chinese body')
214
+ const r10 = await safeFetch(BASE + '/post', {
215
+ method: 'POST',
216
+ body: '\u4e2d\u6587',
217
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' }
218
+ })
219
+ if (r10) {
220
+ assert('post status 200', r10.status === 200)
221
+ const body = await r10.text()
222
+ assert('post body contains 中文', body.includes('\u4e2d\u6587'))
223
+ } else {
224
+ assert('local server reachable', false)
225
+ }
226
+
227
+ // ── HTTPS fetch ──
228
+ t.section('HTTPS fetch')
229
+ const r11 = await safeFetch('https://localhost:18924/', { timeout: 5000 })
230
+ if (r11) {
231
+ assert('https status 200', r11.status === 200)
232
+ const body = await r11.text()
233
+ assert('https body matches', body === 'hello from test server')
234
+ } else {
235
+ assert('https endpoint reachable', false)
236
+ }
237
+ }
238
+ }