quickwin 1.0.2 → 1.1.1
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.en.md +204 -204
- package/README.md +203 -203
- package/examples/PdfCanvas.tsx +83 -83
- package/examples/exec_server.ts +35 -35
- package/examples/pdf_preview2.ts +375 -375
- package/examples/pdf_viewer.tsx +279 -279
- package/examples/test_gallery.tsx +285 -285
- package/examples/test_react_lazy.tsx +63 -63
- package/examples/test_react_listbox.tsx +71 -71
- package/examples/test_root_window.tsx +41 -41
- package/examples/test_tab.tsx +75 -75
- package/examples/tray_demo.tsx +81 -81
- package/lib/assert.ts +6 -6
- package/lib/cache-utils.ts +40 -40
- package/lib/dumpRects.ts +25 -25
- package/lib/fetch.ts +502 -502
- package/lib/http-common.ts +399 -399
- package/lib/http-server.ts +424 -424
- package/lib/polyfill.ts +171 -171
- package/lib/react-qw/components/index.ts +17 -17
- package/lib/react-qw/index.ts +90 -90
- package/lib/react-qw/jsx.d.ts +34 -34
- package/lib/react-qw/layout.ts +111 -111
- package/lib/react-qw/props.ts +36 -36
- package/lib/react-qw/reconciler.ts +373 -373
- package/lib/stream.ts +21 -21
- package/lib/text-codec.ts +171 -171
- package/lib/text-measure.ts +54 -54
- package/lib/url.ts +201 -201
- package/lib/vendor/web-streams/ponyfill.d.mts +829 -829
- package/lib/vendor/web-streams/ponyfill.mjs +8 -8
- package/lib/websocket.js +8 -0
- package/lib/websocket.ts +563 -555
- package/package.json +50 -44
- package/qwin-nowasm-x86.exe +0 -0
- package/qwin-nowasm.exe +0 -0
- package/qwin-x86.exe +0 -0
- package/qwin.exe +0 -0
- package/test/run.ts +96 -96
- package/test/test_basic.ts +9 -9
- package/test/test_basic_fetch.ts +33 -33
- package/test/test_brotli.ts +52 -52
- package/test/test_fetch_cache.ts +187 -187
- package/test/test_ffi.ts +174 -174
- package/test/test_frame_encoding.ts +132 -132
- package/test/test_helper.ts +80 -80
- package/test/test_http_import.ts +74 -74
- package/test/test_http_server.ts +136 -136
- package/test/test_import_dir.ts +5 -5
- package/test/test_ipv6.ts +129 -129
- package/test/test_meta_url.ts +4 -4
- package/test/test_mupdf_render.ts +74 -74
- package/test/test_mupdf_twice.ts +81 -81
- package/test/test_mupdf_wasm.ts +30 -30
- package/test/test_net_event.ts +55 -55
- package/test/test_net_fetch.ts +238 -238
- package/test/test_net_websocket.ts +164 -164
- package/test/test_polyfill.ts +159 -159
- package/test/test_url.ts +183 -183
- package/test/test_wasm_basic.ts +73 -73
- package/test/test_wasm_import_global.ts +45 -45
- package/test/test_wasm_sjlj.ts +125 -125
- package/test/test_wasm_types.ts +124 -124
- package/test/test_worker.ts +24 -24
- package/test/test_worker_fetch.ts +27 -27
- package/test/test_worker_net.ts +54 -54
- package/test/test_worker_wasm_concurrent.ts +45 -45
- package/test/worker_fetch.ts +13 -13
- package/test/worker_helper.ts +1 -1
- package/test/worker_http.ts +20 -20
- package/test/worker_import_lib.ts +13 -13
- package/test/worker_module.ts +17 -17
- package/test/worker_wasm_concurrent.ts +22 -22
- package/tsconfig.json +27 -27
- package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -571
- package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -2749
- package/vendor/mupdf-wasm/mupdf.d.ts +939 -939
- package/vendor/mupdf-wasm/mupdf.js +3317 -3317
package/test/test_net_event.ts
CHANGED
|
@@ -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
|
+
}
|
package/test/test_net_fetch.ts
CHANGED
|
@@ -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
|
+
}
|