quickwin 1.2.1 → 2.0.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.en.md +3 -3
- package/README.md +21 -24
- package/examples/PdfCanvas.js +8 -37
- package/examples/PdfCanvas.tsx +11 -41
- package/examples/exec_server.js +20054 -1423
- package/examples/exec_server.tsx +489 -0
- package/examples/exec_server_worker.js +383 -33
- package/examples/exec_server_worker.ts +408 -30
- package/examples/pdf_preview2.js +31 -73
- package/examples/pdf_preview2.ts +33 -76
- package/examples/pdf_viewer.js +1496 -616
- package/examples/pdf_viewer.tsx +23 -64
- package/examples/setres.js +22 -47
- package/examples/setres.ts +23 -57
- package/examples/test_gallery.js +1491 -525
- package/examples/test_react_lazy.js +1455 -500
- package/examples/test_react_listbox.js +1454 -499
- package/examples/test_root_window.js +1450 -495
- package/examples/test_tab.js +1455 -500
- package/lib/abort.js +70 -0
- package/lib/abort.ts +69 -0
- package/lib/certs.js +214 -0
- package/lib/certs.ts +224 -0
- package/lib/exec-log.js +15 -0
- package/lib/exec-log.ts +30 -0
- package/lib/fetch.js +37 -4
- package/lib/fetch.ts +35 -4
- package/lib/ffi/bind.js +352 -0
- package/lib/ffi/bind.ts +442 -0
- package/lib/ffi/ctype.js +168 -0
- package/lib/ffi/ctype.ts +331 -0
- package/lib/ffi/index.js +6 -0
- package/lib/ffi/index.ts +6 -0
- package/lib/ffi/struct.js +412 -0
- package/lib/ffi/struct.ts +664 -0
- package/lib/http-common.js +43 -5
- package/lib/http-common.ts +48 -6
- package/lib/http-server.js +187 -29
- package/lib/http-server.ts +177 -26
- package/lib/react-qw/components/DateTimePicker.js +20 -33
- package/lib/react-qw/components/Input.js +3 -10
- package/lib/react-qw/components/Link.js +7 -22
- package/lib/react-qw/components/ListView.js +80 -141
- package/lib/react-qw/components/PathPicker.js +36 -80
- package/lib/react-qw/components/ScrollView.js +15 -8
- package/lib/react-qw/components/Slider.js +2 -1
- package/lib/react-qw/components/Tab.js +15 -20
- package/lib/react-qw/components/Tooltip.js +21 -20
- package/lib/react-qw/components/TreeView.js +14 -47
- package/lib/react-qw/index.js +4 -2
- package/lib/react-qw/index.ts +4 -2
- package/lib/react-qw/nmhdr.js +5 -0
- package/lib/react-qw/nmhdr.ts +7 -0
- package/lib/react-qw/props.js +7 -2
- package/lib/react-qw/props.ts +7 -3
- package/lib/react-qw/reconciler.js +23 -13
- package/lib/react-qw/reconciler.ts +22 -13
- package/lib/text-measure.js +18 -35
- package/lib/text-measure.ts +20 -37
- package/lib/websocket.js +12 -2
- package/lib/websocket.ts +15 -2
- package/lib/windows/comctl32.js +18 -0
- package/lib/windows/comctl32.ts +20 -0
- package/lib/windows/comdlg32.js +7 -0
- package/lib/windows/comdlg32.ts +9 -0
- package/lib/windows/gdi32.js +90 -0
- package/lib/windows/gdi32.ts +97 -0
- package/lib/windows/kernel32.js +66 -0
- package/lib/windows/kernel32.ts +76 -0
- package/lib/windows/ntdll.js +8 -0
- package/lib/windows/ntdll.ts +10 -0
- package/lib/windows/ole32.js +7 -0
- package/lib/windows/ole32.ts +9 -0
- package/lib/windows/shell32.js +11 -0
- package/lib/windows/shell32.ts +14 -0
- package/lib/windows/structs.js +384 -0
- package/lib/windows/structs.ts +422 -0
- package/lib/windows/user32.js +469 -0
- package/lib/windows/user32.ts +502 -0
- package/package.json +1 -1
- package/quickwin.d.ts +22 -33
- package/quickwin_const.d.ts +1 -0
- 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/bench_ffi_callraw.js +236 -0
- package/test/bench_ffi_callraw.ts +236 -0
- package/test/bench_ffi_overhead.js +139 -0
- package/test/bench_ffi_overhead.ts +141 -0
- package/test/bench_sendmessage.js +104 -0
- package/test/bench_sendmessage.ts +100 -0
- package/test/bench_sendmessage_nonnull.js +187 -0
- package/test/bench_sendmessage_nonnull.ts +188 -0
- package/test/run.js +14 -4
- package/test/run.ts +15 -5
- package/test/test_basic_fetch.js +8 -0
- package/test/test_basic_fetch.ts +9 -0
- package/test/test_ffi.js +33 -47
- package/test/test_ffi.ts +41 -60
- package/test/test_ffi_abi.js +235 -0
- package/test/test_ffi_abi.ts +218 -0
- package/test/test_ffi_bind.js +572 -0
- package/test/test_ffi_bind.ts +597 -0
- package/test/test_ffi_struct.js +745 -0
- package/test/test_ffi_struct.ts +798 -0
- package/test/test_ffi_struct_layout.js +495 -0
- package/test/test_ffi_struct_layout.ts +543 -0
- package/test/test_ffi_user32.js +103 -0
- package/test/test_ffi_user32.ts +113 -0
- package/test/test_ffi_windows.js +101 -0
- package/test/test_ffi_windows.ts +113 -0
- package/test/test_helper.js +8 -1
- package/test/test_helper.ts +9 -1
- package/test/test_http_import.js +18 -0
- package/test/test_http_import.ts +19 -0
- package/test/test_http_server.js +73 -1
- package/test/test_http_server.ts +75 -1
- package/test/test_ipv6.js +2 -2
- package/test/test_ipv6.ts +2 -2
- package/test/test_net_fetch.js +70 -8
- package/test/test_net_fetch.ts +69 -7
- package/test/test_net_websocket.js +1 -1
- package/test/test_net_websocket.ts +1 -1
- package/test/test_react_complex.js +1465 -510
- package/test/test_react_counter.js +1455 -500
- package/test/test_react_flex.js +1475 -513
- package/test/test_react_hidden.js +1458 -503
- package/test/test_react_listview_link.js +1455 -500
- package/test/test_react_notify.js +18534 -0
- package/test/test_react_render.js +1455 -500
- package/test/test_wolfssl_read.js +3 -2
- package/test/test_wolfssl_read.ts +3 -1
- package/test/test_worker_net.js +35 -17
- package/test/test_worker_net.ts +27 -15
- package/tsconfig.json +1 -1
- package/examples/exec_server.ts +0 -80
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Tester } from './test_helper.js'
|
|
2
|
+
import { closure, WCHAR } from '../lib/ffi/bind.js'
|
|
3
|
+
import { NULL, PtrArrayBuffer } from '../lib/ffi/ctype.js'
|
|
4
|
+
import {
|
|
5
|
+
GetDesktopWindow, IsWindow, IsWindowVisible, GetSystemMetrics, GetClientRect,
|
|
6
|
+
GetWindowTextLength, GetClassName, FindWindow, SetTimer, KillTimer,
|
|
7
|
+
GetKeyState, LoadCursor, SetCursor, GetMenu, DestroyMenu, GetForegroundWindow,
|
|
8
|
+
GetDC, ReleaseDC, EnumWindows, FindWindowEx,
|
|
9
|
+
} from '../lib/windows/user32.js'
|
|
10
|
+
import { RECT } from '../lib/windows/structs.js'
|
|
11
|
+
|
|
12
|
+
export const suite = {
|
|
13
|
+
run(t: Tester) {
|
|
14
|
+
t.section('user32: 窗口 / 度量')
|
|
15
|
+
const desk = GetDesktopWindow()
|
|
16
|
+
t.checkTrue('GetDesktopWindow 非 0', desk !== 0)
|
|
17
|
+
if (desk === 0) return
|
|
18
|
+
t.check('IsWindow(desktop)', 1, IsWindow(desk))
|
|
19
|
+
t.check('IsWindowVisible(desktop)', 1, IsWindowVisible(desk))
|
|
20
|
+
t.checkTrue('GetSystemMetrics(SM_CXSCREEN) > 0', GetSystemMetrics(0) > 0)
|
|
21
|
+
t.checkTrue('GetForegroundWindow 形态 number',
|
|
22
|
+
typeof GetForegroundWindow() === 'number')
|
|
23
|
+
|
|
24
|
+
t.section('user32: RECT 出参(encode → call → decode 字段直读)')
|
|
25
|
+
const rectOut = RECT.alloc()
|
|
26
|
+
t.check('GetClientRect(desktop)', 1, GetClientRect(desk, rectOut.ptr))
|
|
27
|
+
const rect = RECT.decode(rectOut)
|
|
28
|
+
t.check('客户区宽 = SM_CXSCREEN', GetSystemMetrics(0), rect.right - rect.left)
|
|
29
|
+
t.check('客户区高 = SM_CYSCREEN', GetSystemMetrics(1), rect.bottom - rect.top)
|
|
30
|
+
|
|
31
|
+
t.section('user32: 字符串出参(宽字符 buffer 读回)')
|
|
32
|
+
t.checkTrue('GetWindowTextLength ≥ 0', GetWindowTextLength(desk) >= 0)
|
|
33
|
+
const cls = new PtrArrayBuffer(64)
|
|
34
|
+
const n = GetClassName(desk, cls, 32)
|
|
35
|
+
t.checkTrue('GetClassName 写入长度 > 0', n > 0)
|
|
36
|
+
const cdv = new DataView(cls)
|
|
37
|
+
let clsName = ''
|
|
38
|
+
for (let i = 0; i < n; i++) clsName += String.fromCharCode(cdv.getUint16(i * 2, true))
|
|
39
|
+
t.checkTrue('类名非空', clsName.length > 0)
|
|
40
|
+
// JSDoc 文档化流(L4):WCHAR.alloc(n) 当 out buffer、WCHAR.decode(buf) 读回
|
|
41
|
+
const clsH = WCHAR.alloc(32)
|
|
42
|
+
t.checkTrue('WCHAR.alloc out 写入长度 > 0', GetClassName(desk, clsH, 32) > 0)
|
|
43
|
+
t.check('WCHAR.decode(buf) 读回类名', clsName, WCHAR.decode(clsH))
|
|
44
|
+
|
|
45
|
+
t.section('user32: FindWindow(<WCHAR>ptr 的 NULL 与 string 双形态)')
|
|
46
|
+
const fw = FindWindow(NULL, NULL)
|
|
47
|
+
t.checkTrue('FindWindow(NULL, NULL) 形态 number(NULL 实参不 throw)',
|
|
48
|
+
typeof fw === 'number')
|
|
49
|
+
t.check('FindWindow(不存在类名, NULL)', 0, FindWindow('qw_no_such_class_xyz', NULL))
|
|
50
|
+
|
|
51
|
+
t.section('user32: 计时器')
|
|
52
|
+
const tid = SetTimer(NULL, 0, 50, NULL)
|
|
53
|
+
t.checkTrue('SetTimer 返回非 0 id', tid !== 0)
|
|
54
|
+
if (tid !== 0) t.checkTrue('KillTimer 成功', KillTimer(NULL, tid) !== 0)
|
|
55
|
+
|
|
56
|
+
t.section('user32: 输入 / 光标 / 菜单')
|
|
57
|
+
t.checkTrue('GetKeyState 返回整数', Number.isInteger(GetKeyState(0x1b)))
|
|
58
|
+
t.checkTrue('LoadCursor(NULL, IDC_ARROW=32512) 非 0', LoadCursor(NULL, 32512) !== 0)
|
|
59
|
+
// 品牌流动(正例):LoadCursor 返回 MaybePtr<'HCURSOR'> 直接喂 SetCursor 形参(先收窄空位)
|
|
60
|
+
const arrow = LoadCursor(NULL, 32512)
|
|
61
|
+
const prevCur = arrow !== 0 ? SetCursor(arrow) : null
|
|
62
|
+
t.checkTrue('SetCursor(LoadCursor(...)) 返回句柄形态',
|
|
63
|
+
arrow !== 0 && (prevCur === null || typeof prevCur === 'number'))
|
|
64
|
+
t.check('GetMenu(desktop) 无菜单 → 0', 0, GetMenu(desk))
|
|
65
|
+
|
|
66
|
+
t.section('user32: GetDC / ReleaseDC 配对')
|
|
67
|
+
const hdc = GetDC(NULL)
|
|
68
|
+
t.checkTrue('GetDC(NULL) 非 0', hdc !== 0)
|
|
69
|
+
t.checkTrue('ReleaseDC(NULL, hdc) 成功', hdc !== 0 && ReleaseDC(NULL, hdc) !== 0)
|
|
70
|
+
|
|
71
|
+
// 仅类型层反例(不执行):异种句柄互传、<>ptr 误传品牌位都应编译不过
|
|
72
|
+
const _typeOnly = () => {
|
|
73
|
+
// @ts-expect-error Ptr<>(number)不是 Ptr<'HDC'>,误传桌面句柄应编译不过
|
|
74
|
+
ReleaseDC(NULL, GetDesktopWindow())
|
|
75
|
+
// @ts-expect-error Ptr<'HWND'> 不是 Ptr<'HCURSOR'>,窗口句柄喂 SetCursor 应编译不过
|
|
76
|
+
SetCursor(GetDesktopWindow())
|
|
77
|
+
// @ts-expect-error Ptr<'HWND'> 不是 Ptr<'HMENU'>,窗口句柄喂 DestroyMenu 应编译不过
|
|
78
|
+
DestroyMenu(GetDesktopWindow())
|
|
79
|
+
// @ts-expect-error Ptr<'HMENU'> 不是 Ptr<'HWND'>,菜单句柄喂 IsWindow 应编译不过
|
|
80
|
+
IsWindow(GetMenu(desk))
|
|
81
|
+
// @ts-expect-error 出参位未注册 encoder:<RECT>ptr 只收 MaybePtr<'RECT'>(NULL|Ptr),裸 ArrayBuffer 编译不过
|
|
82
|
+
GetClientRect(desk, new ArrayBuffer(16))
|
|
83
|
+
// @ts-expect-error 裸 number 缺 Ptr<'RECT'> 品牌
|
|
84
|
+
GetClientRect(desk, 123)
|
|
85
|
+
// 正例:同种品牌流动(GetMenu → DestroyMenu)通过——上面反例的成立依赖这层区分
|
|
86
|
+
const m = GetMenu(desk)
|
|
87
|
+
if (m !== 0) DestroyMenu(m)
|
|
88
|
+
}
|
|
89
|
+
void _typeOnly
|
|
90
|
+
|
|
91
|
+
t.section('user32: EnumWindows(<>ptr 回调端到端)')
|
|
92
|
+
let count = 0
|
|
93
|
+
const enumClos = closure('<>ptr <>ptr -> i32', () => { count++; return 1 })
|
|
94
|
+
t.check('EnumWindows 返回 1(枚举完成)', 1, EnumWindows(enumClos.ptr, NULL))
|
|
95
|
+
t.checkTrue('回调计数 ≥ 1', count >= 1)
|
|
96
|
+
enumClos.dispose()
|
|
97
|
+
|
|
98
|
+
// EnumWindows(快照)与 FindWindowEx(NULL, prev) 链式遍历应得到同一组顶层窗口(实测两者顺序不同,
|
|
99
|
+
// 所以只比集合,不比顺序)。
|
|
100
|
+
// 回调只收集:回调内抛异常会被吞成返回 0(枚举静默提前结束),所以不在回调里做别的。
|
|
101
|
+
t.section('user32: EnumWindows ≡ FindWindowEx 链遍历(顶层窗口集合)')
|
|
102
|
+
const viaEnum: number[] = []
|
|
103
|
+
const collect = closure('<HWND>ptr <>ptr -> i32', (h) => { viaEnum.push(h); return 1 })
|
|
104
|
+
try { EnumWindows(collect.ptr, NULL) } finally { collect.dispose() }
|
|
105
|
+
const viaChain: number[] = []
|
|
106
|
+
for (let h = FindWindowEx(NULL, NULL, NULL, NULL); h && viaChain.length < 100000;
|
|
107
|
+
h = FindWindowEx(NULL, h, NULL, NULL)) viaChain.push(h)
|
|
108
|
+
t.checkTrue('两种遍历都非空', viaEnum.length > 0 && viaChain.length > 0)
|
|
109
|
+
t.check('窗口数量一致', viaEnum.length, viaChain.length)
|
|
110
|
+
const sorted = (a: number[]): string => a.slice().sort((x, y) => x - y).join(',')
|
|
111
|
+
t.check('窗口集合一致(忽略顺序)', sorted(viaEnum), sorted(viaChain))
|
|
112
|
+
},
|
|
113
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NULL } from '../lib/ffi/ctype.js';
|
|
2
|
+
import { GetDesktopWindow, CreatePopupMenu, DestroyMenu, GetDC, ReleaseDC, LoadImage, LoadImageOrdinal, SetScrollInfo, GetScrollInfo, } from '../lib/windows/user32.js';
|
|
3
|
+
import { GetDeviceCaps, CreateSolidBrush, DeleteObject, CreateFontIndirect, GetObject, SelectObject, CreateCompatibleDC, DeleteDC, } from '../lib/windows/gdi32.js';
|
|
4
|
+
import { ImageListCreate, ImageListDestroy } from '../lib/windows/comctl32.js';
|
|
5
|
+
import { LOGFONTW, SCROLLINFO } from '../lib/windows/structs.js';
|
|
6
|
+
export const suite = {
|
|
7
|
+
run(t) {
|
|
8
|
+
// 本文件 import 三个 dll 模块即在加载时执行全部 bind:proc 名错误 → GetProcAddress 失败 →
|
|
9
|
+
// 模块加载 throw → run.ts 静默跳过本 suite → 总计数低于基线(判据链对比计数即暴露)
|
|
10
|
+
t.section('gdi32: 画刷 / 字体 创建-删除往返(LOGFONTW 对象直传)');
|
|
11
|
+
const brush = CreateSolidBrush(0x3366ff);
|
|
12
|
+
t.checkTrue('CreateSolidBrush 非 0', brush !== 0);
|
|
13
|
+
if (brush !== 0)
|
|
14
|
+
t.checkTrue('DeleteObject(brush) 成功', DeleteObject(brush) !== 0);
|
|
15
|
+
// DeepPartial 缺省字段跳过:lfHeight + lfFaceName 必给,其余标量保持 0(FW_DONTCARE 等合法零值)
|
|
16
|
+
const font = CreateFontIndirect({ lfHeight: -16, lfWeight: 400, lfCharSet: 1, lfFaceName: 'Arial' });
|
|
17
|
+
t.checkTrue('CreateFontIndirect(DeepPartial LOGFONTW) 非 0', font !== 0);
|
|
18
|
+
if (font !== 0)
|
|
19
|
+
t.checkTrue('DeleteObject(font) 成功', DeleteObject(font) !== 0);
|
|
20
|
+
// react-qw ListView.getCellFont 的流程:GetObject 读出现有字体 → decode → 叠加粗/斜/下划线 → 重建 → 回读
|
|
21
|
+
t.section('gdi32: 字体读出-改写-重建往返(GetObject → decode → CreateFontIndirect → GetObject)');
|
|
22
|
+
const base = CreateFontIndirect({ lfHeight: -18, lfWeight: 400, lfCharSet: 1, lfFaceName: 'Arial' });
|
|
23
|
+
if (base !== 0) {
|
|
24
|
+
const lf0 = LOGFONTW.alloc();
|
|
25
|
+
t.checkTrue('GetObject(font) 写入 LOGFONTW.size 字节', GetObject(base, LOGFONTW.size, lf0) === LOGFONTW.size);
|
|
26
|
+
const b0 = LOGFONTW.decode(lf0);
|
|
27
|
+
t.check('读出 lfHeight', -18, b0.lfHeight);
|
|
28
|
+
t.check('读出 lfWeight', 400, b0.lfWeight);
|
|
29
|
+
t.check('读出 lfItalic(未设)', 0, b0.lfItalic);
|
|
30
|
+
const styled = CreateFontIndirect({ ...b0, lfWeight: 700, lfItalic: 1, lfUnderline: 1 });
|
|
31
|
+
t.checkTrue('叠加样式后重建非 0', styled !== 0);
|
|
32
|
+
if (styled !== 0) {
|
|
33
|
+
const lf1 = LOGFONTW.alloc();
|
|
34
|
+
GetObject(styled, LOGFONTW.size, lf1);
|
|
35
|
+
const b1 = LOGFONTW.decode(lf1);
|
|
36
|
+
t.check('重建后 lfWeight', 700, b1.lfWeight);
|
|
37
|
+
t.check('重建后 lfItalic', 1, b1.lfItalic);
|
|
38
|
+
t.check('重建后 lfUnderline', 1, b1.lfUnderline);
|
|
39
|
+
t.check('底字体的 lfHeight 保留', -18, b1.lfHeight);
|
|
40
|
+
t.check('底字体的 lfCharSet 保留', 1, b1.lfCharSet);
|
|
41
|
+
t.check('底字体的字体名保留', b0.lfFaceName, b1.lfFaceName);
|
|
42
|
+
t.checkTrue('DeleteObject(styled) 成功', DeleteObject(styled) !== 0);
|
|
43
|
+
}
|
|
44
|
+
t.checkTrue('DeleteObject(base) 成功', DeleteObject(base) !== 0);
|
|
45
|
+
}
|
|
46
|
+
// 内存 DC 选入字体:SelectObject 返回被替换的旧对象,选回后可删除新对象(截屏与 ListView 自绘共用)
|
|
47
|
+
t.section('gdi32: CreateCompatibleDC / SelectObject 往返');
|
|
48
|
+
const memDc = CreateCompatibleDC(NULL);
|
|
49
|
+
t.checkTrue('CreateCompatibleDC(NULL) 非 0', memDc !== 0);
|
|
50
|
+
if (memDc !== 0) {
|
|
51
|
+
const f2 = CreateFontIndirect({ lfHeight: -12, lfFaceName: 'Arial' });
|
|
52
|
+
if (f2 !== 0) {
|
|
53
|
+
const old = SelectObject(memDc, f2);
|
|
54
|
+
t.checkTrue('SelectObject 返回旧对象非 0', old !== 0);
|
|
55
|
+
t.check('再选回旧对象,返回刚选入的字体', f2, SelectObject(memDc, old));
|
|
56
|
+
t.checkTrue('DeleteObject(f2) 成功', DeleteObject(f2) !== 0);
|
|
57
|
+
}
|
|
58
|
+
t.checkTrue('DeleteDC 成功', DeleteDC(memDc) !== 0);
|
|
59
|
+
}
|
|
60
|
+
t.section('gdi32: GetDeviceCaps(<HDC>ptr 跨 user32 品牌互认)');
|
|
61
|
+
const hdc = GetDC(NULL);
|
|
62
|
+
t.checkTrue('GetDC(NULL) 非 0', hdc !== 0);
|
|
63
|
+
if (hdc !== 0)
|
|
64
|
+
t.checkTrue('GetDeviceCaps(hdc, LOGPIXELSX) > 0', GetDeviceCaps(hdc, 88) > 0);
|
|
65
|
+
t.checkTrue('ReleaseDC(NULL, hdc) 成功', hdc !== 0 && ReleaseDC(NULL, hdc) !== 0);
|
|
66
|
+
t.section('user32: 弹出菜单往返');
|
|
67
|
+
const pm = CreatePopupMenu();
|
|
68
|
+
t.checkTrue('CreatePopupMenu 非 0', pm !== 0);
|
|
69
|
+
if (pm !== 0)
|
|
70
|
+
t.checkTrue('DestroyMenu(pm) 成功', DestroyMenu(pm) !== 0);
|
|
71
|
+
t.section('comctl32: ImageList 创建-销毁往返');
|
|
72
|
+
const himl = ImageListCreate(16, 16, 4, 0, 0); // ILC_COLOR32=4;initial/grow=0 默认
|
|
73
|
+
t.checkTrue('ImageListCreate(16, 16, ILC_COLOR32) 非 0', himl !== 0);
|
|
74
|
+
if (himl !== 0)
|
|
75
|
+
t.checkTrue('ImageListDestroy(himl) 成功', ImageListDestroy(himl) !== 0);
|
|
76
|
+
t.section('user32: LoadImage 双变体(string 自动编码 / ordinal MAKEINTRESOURCE)');
|
|
77
|
+
// Win32 IMAGE_* 真值:BITMAP=0 CURSOR=1 ICON=2;LR_SHARED=0x8000(注意 gui.ImageType d.ts 当前值与此不一致)
|
|
78
|
+
t.checkTrue('LoadImageOrdinal(NULL, IDI_APPLICATION, IMAGE_ICON, LR_SHARED) 非 0', LoadImageOrdinal(NULL, 32512, 2, 0, 0, 0x8000) !== 0);
|
|
79
|
+
t.check('LoadImage(LOADFROMFILE, 不存在文件) → 0', 0, LoadImage(NULL, 'qw_no_such_icon_xyz.ico', 2, 0, 0, 0x10));
|
|
80
|
+
t.section('user32: 滚动条(入参对象直传 / 出参 encode → call → decode)');
|
|
81
|
+
const desk = GetDesktopWindow();
|
|
82
|
+
t.checkTrue('GetDesktopWindow 非 0', desk !== 0);
|
|
83
|
+
if (desk === 0)
|
|
84
|
+
return;
|
|
85
|
+
// 入参位(注册 encoder):DeepPartial 对象直传;cbSize 必须显式 = 结构自描述要求。
|
|
86
|
+
// 桌面窗口无 WS_VSCROLL 样式 → Set/GetScrollInfo 返回 0 是 Win32 合法行为(.NET 的
|
|
87
|
+
// WindowsScroll 包装同样先查样式再调用),故断言形态而非成功。
|
|
88
|
+
t.checkTrue('SetScrollInfo 对象直传 形态 number', typeof SetScrollInfo(desk, 1, { cbSize: SCROLLINFO.size, fMask: 0x0001, nMin: 0, nMax: 100 }, 0) === 'number');
|
|
89
|
+
// 出参位(不注册 encoder):encode() 新建 + 初值一步 → 传 .ptr → decode 字段直读。
|
|
90
|
+
// GetScrollInfo 失败(无滚动条)不写 buffer → decode 回读 = encode 初值;成功时 Win32
|
|
91
|
+
// 覆写 cbSize/fMask 为同值 —— 两条路径下回环断言都稳定。
|
|
92
|
+
const si = SCROLLINFO.encode({ cbSize: SCROLLINFO.size, fMask: 0x0001 });
|
|
93
|
+
t.checkTrue('GetScrollInfo 调用形态 number', typeof GetScrollInfo(desk, 1, si.ptr) === 'number');
|
|
94
|
+
const info = SCROLLINFO.decode(si);
|
|
95
|
+
t.check('decode 回读 cbSize = encode 初值', SCROLLINFO.size, info.cbSize);
|
|
96
|
+
t.check('decode 回读 fMask = encode 初值', 0x0001, info.fMask);
|
|
97
|
+
t.checkTrue('读回字段为 number 形态', typeof info.nMin === 'number' && typeof info.nMax === 'number');
|
|
98
|
+
// CallWindowProc / ShowScrollBar / ImageListAdd 仅由 import 的模块加载覆盖 proc 名:
|
|
99
|
+
// CallWindowProc 跨线程窗口过程不安全、ShowScrollBar 有副作用、ImageListAdd 需位图源
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Tester } from './test_helper.js'
|
|
2
|
+
import { NULL } from '../lib/ffi/ctype.js'
|
|
3
|
+
import {
|
|
4
|
+
GetDesktopWindow, CreatePopupMenu, DestroyMenu, GetDC, ReleaseDC,
|
|
5
|
+
LoadImage, LoadImageOrdinal, SetScrollInfo, GetScrollInfo,
|
|
6
|
+
} from '../lib/windows/user32.js'
|
|
7
|
+
import {
|
|
8
|
+
GetDeviceCaps, CreateSolidBrush, DeleteObject, CreateFontIndirect, GetObject, SelectObject, CreateCompatibleDC, DeleteDC,
|
|
9
|
+
} from '../lib/windows/gdi32.js'
|
|
10
|
+
import { ImageListCreate, ImageListDestroy } from '../lib/windows/comctl32.js'
|
|
11
|
+
import { LOGFONTW, SCROLLINFO } from '../lib/windows/structs.js'
|
|
12
|
+
|
|
13
|
+
export const suite = {
|
|
14
|
+
run(t: Tester) {
|
|
15
|
+
// 本文件 import 三个 dll 模块即在加载时执行全部 bind:proc 名错误 → GetProcAddress 失败 →
|
|
16
|
+
// 模块加载 throw → run.ts 静默跳过本 suite → 总计数低于基线(判据链对比计数即暴露)
|
|
17
|
+
t.section('gdi32: 画刷 / 字体 创建-删除往返(LOGFONTW 对象直传)')
|
|
18
|
+
const brush = CreateSolidBrush(0x3366ff)
|
|
19
|
+
t.checkTrue('CreateSolidBrush 非 0', brush !== 0)
|
|
20
|
+
if (brush !== 0) t.checkTrue('DeleteObject(brush) 成功', DeleteObject(brush) !== 0)
|
|
21
|
+
|
|
22
|
+
// DeepPartial 缺省字段跳过:lfHeight + lfFaceName 必给,其余标量保持 0(FW_DONTCARE 等合法零值)
|
|
23
|
+
const font = CreateFontIndirect({ lfHeight: -16, lfWeight: 400, lfCharSet: 1, lfFaceName: 'Arial' })
|
|
24
|
+
t.checkTrue('CreateFontIndirect(DeepPartial LOGFONTW) 非 0', font !== 0)
|
|
25
|
+
if (font !== 0) t.checkTrue('DeleteObject(font) 成功', DeleteObject(font) !== 0)
|
|
26
|
+
|
|
27
|
+
// react-qw ListView.getCellFont 的流程:GetObject 读出现有字体 → decode → 叠加粗/斜/下划线 → 重建 → 回读
|
|
28
|
+
t.section('gdi32: 字体读出-改写-重建往返(GetObject → decode → CreateFontIndirect → GetObject)')
|
|
29
|
+
const base = CreateFontIndirect({ lfHeight: -18, lfWeight: 400, lfCharSet: 1, lfFaceName: 'Arial' })
|
|
30
|
+
if (base !== 0) {
|
|
31
|
+
const lf0 = LOGFONTW.alloc()
|
|
32
|
+
t.checkTrue('GetObject(font) 写入 LOGFONTW.size 字节', GetObject(base, LOGFONTW.size, lf0) === LOGFONTW.size)
|
|
33
|
+
const b0 = LOGFONTW.decode(lf0)
|
|
34
|
+
t.check('读出 lfHeight', -18, b0.lfHeight)
|
|
35
|
+
t.check('读出 lfWeight', 400, b0.lfWeight)
|
|
36
|
+
t.check('读出 lfItalic(未设)', 0, b0.lfItalic)
|
|
37
|
+
const styled = CreateFontIndirect({ ...b0, lfWeight: 700, lfItalic: 1, lfUnderline: 1 })
|
|
38
|
+
t.checkTrue('叠加样式后重建非 0', styled !== 0)
|
|
39
|
+
if (styled !== 0) {
|
|
40
|
+
const lf1 = LOGFONTW.alloc()
|
|
41
|
+
GetObject(styled, LOGFONTW.size, lf1)
|
|
42
|
+
const b1 = LOGFONTW.decode(lf1)
|
|
43
|
+
t.check('重建后 lfWeight', 700, b1.lfWeight)
|
|
44
|
+
t.check('重建后 lfItalic', 1, b1.lfItalic)
|
|
45
|
+
t.check('重建后 lfUnderline', 1, b1.lfUnderline)
|
|
46
|
+
t.check('底字体的 lfHeight 保留', -18, b1.lfHeight)
|
|
47
|
+
t.check('底字体的 lfCharSet 保留', 1, b1.lfCharSet)
|
|
48
|
+
t.check('底字体的字体名保留', b0.lfFaceName, b1.lfFaceName)
|
|
49
|
+
t.checkTrue('DeleteObject(styled) 成功', DeleteObject(styled) !== 0)
|
|
50
|
+
}
|
|
51
|
+
t.checkTrue('DeleteObject(base) 成功', DeleteObject(base) !== 0)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 内存 DC 选入字体:SelectObject 返回被替换的旧对象,选回后可删除新对象(截屏与 ListView 自绘共用)
|
|
55
|
+
t.section('gdi32: CreateCompatibleDC / SelectObject 往返')
|
|
56
|
+
const memDc = CreateCompatibleDC(NULL)
|
|
57
|
+
t.checkTrue('CreateCompatibleDC(NULL) 非 0', memDc !== 0)
|
|
58
|
+
if (memDc !== 0) {
|
|
59
|
+
const f2 = CreateFontIndirect({ lfHeight: -12, lfFaceName: 'Arial' })
|
|
60
|
+
if (f2 !== 0) {
|
|
61
|
+
const old = SelectObject(memDc, f2)
|
|
62
|
+
t.checkTrue('SelectObject 返回旧对象非 0', old !== 0)
|
|
63
|
+
t.check('再选回旧对象,返回刚选入的字体', f2, SelectObject(memDc, old))
|
|
64
|
+
t.checkTrue('DeleteObject(f2) 成功', DeleteObject(f2) !== 0)
|
|
65
|
+
}
|
|
66
|
+
t.checkTrue('DeleteDC 成功', DeleteDC(memDc) !== 0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
t.section('gdi32: GetDeviceCaps(<HDC>ptr 跨 user32 品牌互认)')
|
|
70
|
+
const hdc = GetDC(NULL)
|
|
71
|
+
t.checkTrue('GetDC(NULL) 非 0', hdc !== 0)
|
|
72
|
+
if (hdc !== 0) t.checkTrue('GetDeviceCaps(hdc, LOGPIXELSX) > 0', GetDeviceCaps(hdc, 88) > 0)
|
|
73
|
+
t.checkTrue('ReleaseDC(NULL, hdc) 成功', hdc !== 0 && ReleaseDC(NULL, hdc) !== 0)
|
|
74
|
+
|
|
75
|
+
t.section('user32: 弹出菜单往返')
|
|
76
|
+
const pm = CreatePopupMenu()
|
|
77
|
+
t.checkTrue('CreatePopupMenu 非 0', pm !== 0)
|
|
78
|
+
if (pm !== 0) t.checkTrue('DestroyMenu(pm) 成功', DestroyMenu(pm) !== 0)
|
|
79
|
+
|
|
80
|
+
t.section('comctl32: ImageList 创建-销毁往返')
|
|
81
|
+
const himl = ImageListCreate(16, 16, 4, 0, 0) // ILC_COLOR32=4;initial/grow=0 默认
|
|
82
|
+
t.checkTrue('ImageListCreate(16, 16, ILC_COLOR32) 非 0', himl !== 0)
|
|
83
|
+
if (himl !== 0) t.checkTrue('ImageListDestroy(himl) 成功', ImageListDestroy(himl) !== 0)
|
|
84
|
+
|
|
85
|
+
t.section('user32: LoadImage 双变体(string 自动编码 / ordinal MAKEINTRESOURCE)')
|
|
86
|
+
// Win32 IMAGE_* 真值:BITMAP=0 CURSOR=1 ICON=2;LR_SHARED=0x8000(注意 gui.ImageType d.ts 当前值与此不一致)
|
|
87
|
+
t.checkTrue('LoadImageOrdinal(NULL, IDI_APPLICATION, IMAGE_ICON, LR_SHARED) 非 0',
|
|
88
|
+
LoadImageOrdinal(NULL, 32512, 2, 0, 0, 0x8000) !== 0)
|
|
89
|
+
t.check('LoadImage(LOADFROMFILE, 不存在文件) → 0', 0,
|
|
90
|
+
LoadImage(NULL, 'qw_no_such_icon_xyz.ico', 2, 0, 0, 0x10))
|
|
91
|
+
|
|
92
|
+
t.section('user32: 滚动条(入参对象直传 / 出参 encode → call → decode)')
|
|
93
|
+
const desk = GetDesktopWindow()
|
|
94
|
+
t.checkTrue('GetDesktopWindow 非 0', desk !== 0)
|
|
95
|
+
if (desk === 0) return
|
|
96
|
+
// 入参位(注册 encoder):DeepPartial 对象直传;cbSize 必须显式 = 结构自描述要求。
|
|
97
|
+
// 桌面窗口无 WS_VSCROLL 样式 → Set/GetScrollInfo 返回 0 是 Win32 合法行为(.NET 的
|
|
98
|
+
// WindowsScroll 包装同样先查样式再调用),故断言形态而非成功。
|
|
99
|
+
t.checkTrue('SetScrollInfo 对象直传 形态 number',
|
|
100
|
+
typeof SetScrollInfo(desk, 1, { cbSize: SCROLLINFO.size, fMask: 0x0001, nMin: 0, nMax: 100 }, 0) === 'number')
|
|
101
|
+
// 出参位(不注册 encoder):encode() 新建 + 初值一步 → 传 .ptr → decode 字段直读。
|
|
102
|
+
// GetScrollInfo 失败(无滚动条)不写 buffer → decode 回读 = encode 初值;成功时 Win32
|
|
103
|
+
// 覆写 cbSize/fMask 为同值 —— 两条路径下回环断言都稳定。
|
|
104
|
+
const si = SCROLLINFO.encode({ cbSize: SCROLLINFO.size, fMask: 0x0001 })
|
|
105
|
+
t.checkTrue('GetScrollInfo 调用形态 number', typeof GetScrollInfo(desk, 1, si.ptr) === 'number')
|
|
106
|
+
const info = SCROLLINFO.decode(si)
|
|
107
|
+
t.check('decode 回读 cbSize = encode 初值', SCROLLINFO.size, info.cbSize)
|
|
108
|
+
t.check('decode 回读 fMask = encode 初值', 0x0001, info.fMask)
|
|
109
|
+
t.checkTrue('读回字段为 number 形态', typeof info.nMin === 'number' && typeof info.nMax === 'number')
|
|
110
|
+
// CallWindowProc / ShowScrollBar / ImageListAdd 仅由 import 的模块加载覆盖 proc 名:
|
|
111
|
+
// CallWindowProc 跨线程窗口过程不安全、ShowScrollBar 有副作用、ImageListAdd 需位图源
|
|
112
|
+
},
|
|
113
|
+
}
|
package/test/test_helper.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as std from 'std';
|
|
2
2
|
const GREEN = '\x1b[32m';
|
|
3
3
|
const RED = '\x1b[31m';
|
|
4
|
+
const YELLOW = '\x1b[33m';
|
|
4
5
|
const BOLD = '\x1b[1m';
|
|
5
6
|
const RESET = '\x1b[0m';
|
|
6
7
|
function formatDuration(ms) {
|
|
@@ -35,6 +36,7 @@ export function readWasmFile(path) {
|
|
|
35
36
|
export class Tester {
|
|
36
37
|
ok = 0;
|
|
37
38
|
fail = 0;
|
|
39
|
+
skip = 0;
|
|
38
40
|
startTime = Date.now();
|
|
39
41
|
sectionStart = this.startTime;
|
|
40
42
|
lastSection = '';
|
|
@@ -71,6 +73,10 @@ export class Tester {
|
|
|
71
73
|
checkTrue(name, actual) {
|
|
72
74
|
this.check(name, true, actual);
|
|
73
75
|
}
|
|
76
|
+
skipCase(name) {
|
|
77
|
+
this.skip++;
|
|
78
|
+
std.printf(' %sSKIP:%s %s\n', YELLOW, RESET, name);
|
|
79
|
+
}
|
|
74
80
|
summary() {
|
|
75
81
|
const now = Date.now();
|
|
76
82
|
if (this.lastSection) {
|
|
@@ -79,6 +85,7 @@ export class Tester {
|
|
|
79
85
|
}
|
|
80
86
|
const total = now - this.startTime;
|
|
81
87
|
const color = this.fail > 0 ? RED : GREEN;
|
|
82
|
-
|
|
88
|
+
const skipped = this.skip > 0 ? ` (${this.skip} skipped)` : '';
|
|
89
|
+
std.printf('\n%s%d/%d passed%s (%s)%s\n', color, this.ok, this.ok + this.fail, skipped, formatDuration(total), RESET);
|
|
83
90
|
}
|
|
84
91
|
}
|
package/test/test_helper.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as std from 'std'
|
|
|
2
2
|
|
|
3
3
|
const GREEN = '\x1b[32m'
|
|
4
4
|
const RED = '\x1b[31m'
|
|
5
|
+
const YELLOW = '\x1b[33m'
|
|
5
6
|
const BOLD = '\x1b[1m'
|
|
6
7
|
const RESET = '\x1b[0m'
|
|
7
8
|
|
|
@@ -34,6 +35,7 @@ export function readWasmFile(path: string): ArrayBuffer | null {
|
|
|
34
35
|
export class Tester {
|
|
35
36
|
ok = 0
|
|
36
37
|
fail = 0
|
|
38
|
+
skip = 0
|
|
37
39
|
private startTime = Date.now()
|
|
38
40
|
private sectionStart = this.startTime
|
|
39
41
|
private lastSection = ''
|
|
@@ -67,6 +69,11 @@ export class Tester {
|
|
|
67
69
|
this.check(name, true, actual)
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
skipCase(name: string): void {
|
|
73
|
+
this.skip++
|
|
74
|
+
std.printf(' %sSKIP:%s %s\n', YELLOW, RESET, name)
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
summary(): void {
|
|
71
78
|
const now = Date.now()
|
|
72
79
|
if (this.lastSection) {
|
|
@@ -75,6 +82,7 @@ export class Tester {
|
|
|
75
82
|
}
|
|
76
83
|
const total = now - this.startTime
|
|
77
84
|
const color = this.fail > 0 ? RED : GREEN
|
|
78
|
-
|
|
85
|
+
const skipped = this.skip > 0 ? ` (${this.skip} skipped)` : ''
|
|
86
|
+
std.printf('\n%s%d/%d passed%s (%s)%s\n', color, this.ok, this.ok + this.fail, skipped, formatDuration(total), RESET)
|
|
79
87
|
}
|
|
80
88
|
}
|
package/test/test_http_import.js
CHANGED
|
@@ -60,6 +60,24 @@ export const suite = {
|
|
|
60
60
|
return;
|
|
61
61
|
assert('cache has body files', after.some(function (f) { return f.endsWith('.body'); }));
|
|
62
62
|
assert('cache has meta files', after.some(function (f) { return f.endsWith('.meta'); }));
|
|
63
|
+
t.section('repeat https import (fresh url, forces TLS)');
|
|
64
|
+
// http_get_sync() short-circuits on a warm disk cache, so a second
|
|
65
|
+
// import of the same URL would never build a TLS connection. Use a
|
|
66
|
+
// distinct query string to force a real handshake into the
|
|
67
|
+
// process-wide CA store, then import once more to confirm repeated
|
|
68
|
+
// loadTrustedCerts() calls keep verifying.
|
|
69
|
+
const url2 = 'https://esm.sh/left-pad@1.3.0?twice=1';
|
|
70
|
+
let mod2;
|
|
71
|
+
try {
|
|
72
|
+
mod2 = await import(url2);
|
|
73
|
+
assert('repeat import succeeds', true);
|
|
74
|
+
assert('repeat default is function', typeof mod2.default === 'function');
|
|
75
|
+
assert('repeat left-pad works', mod2.default('hello', 8) === ' hello');
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
assert('repeat import succeeds', false);
|
|
79
|
+
std.printf(' ERROR: %s\n', String(e));
|
|
80
|
+
}
|
|
63
81
|
for (const f of after) {
|
|
64
82
|
if (f.endsWith('.meta') && (err !== 0 || before.indexOf(f) < 0)) {
|
|
65
83
|
std.printf(' meta: %s\n', f);
|
package/test/test_http_import.ts
CHANGED
|
@@ -56,6 +56,25 @@ export const suite = {
|
|
|
56
56
|
if (err2 !== 0) return
|
|
57
57
|
assert('cache has body files', after.some(function(f: string) { return f.endsWith('.body') }))
|
|
58
58
|
assert('cache has meta files', after.some(function(f: string) { return f.endsWith('.meta') }))
|
|
59
|
+
|
|
60
|
+
t.section('repeat https import (fresh url, forces TLS)')
|
|
61
|
+
// http_get_sync() short-circuits on a warm disk cache, so a second
|
|
62
|
+
// import of the same URL would never build a TLS connection. Use a
|
|
63
|
+
// distinct query string to force a real handshake into the
|
|
64
|
+
// process-wide CA store, then import once more to confirm repeated
|
|
65
|
+
// loadTrustedCerts() calls keep verifying.
|
|
66
|
+
const url2 = 'https://esm.sh/left-pad@1.3.0?twice=1'
|
|
67
|
+
let mod2: any
|
|
68
|
+
try {
|
|
69
|
+
mod2 = await import(url2)
|
|
70
|
+
assert('repeat import succeeds', true)
|
|
71
|
+
assert('repeat default is function', typeof mod2.default === 'function')
|
|
72
|
+
assert('repeat left-pad works', mod2.default('hello', 8) === ' hello')
|
|
73
|
+
} catch (e) {
|
|
74
|
+
assert('repeat import succeeds', false)
|
|
75
|
+
std.printf(' ERROR: %s\n', String(e))
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
for (const f of after) {
|
|
60
79
|
if (f.endsWith('.meta') && (err !== 0 || before.indexOf(f) < 0)) {
|
|
61
80
|
std.printf(' meta: %s\n', f)
|
package/test/test_http_server.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as std from 'std';
|
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as sock from 'sock';
|
|
5
5
|
import { createServer } from '../lib/http-server.js';
|
|
6
|
+
import '../lib/fetch.js';
|
|
6
7
|
// ── Minimal raw HTTP client for testing ──
|
|
7
8
|
function httpSend(host, port, raw, nResponses = 1) {
|
|
8
9
|
return new Promise((resolve, reject) => {
|
|
@@ -82,6 +83,40 @@ export const suite = {
|
|
|
82
83
|
else if (path === '/json') {
|
|
83
84
|
return new Response(JSON.stringify({ method: req.method, q: Object.fromEntries(url.searchParams) }), { headers: { 'Content-Type': 'application/json' } });
|
|
84
85
|
}
|
|
86
|
+
else if (path === '/stream') {
|
|
87
|
+
// length unknown: server must send chunked frames
|
|
88
|
+
return new Response(new ReadableStream({
|
|
89
|
+
start(control) {
|
|
90
|
+
control.enqueue(new TextEncoder().encode('chunkA/'));
|
|
91
|
+
control.enqueue(new TextEncoder().encode('chunkB'));
|
|
92
|
+
control.close();
|
|
93
|
+
}
|
|
94
|
+
}), { headers: { 'Content-Type': 'text/plain' } });
|
|
95
|
+
}
|
|
96
|
+
else if (path === '/file') {
|
|
97
|
+
// length known: server streams raw bytes with Content-Length
|
|
98
|
+
return new Response(new ReadableStream({
|
|
99
|
+
start(control) {
|
|
100
|
+
control.enqueue(new TextEncoder().encode('chunkA/'));
|
|
101
|
+
control.enqueue(new TextEncoder().encode('chunkB'));
|
|
102
|
+
control.close();
|
|
103
|
+
}
|
|
104
|
+
}), { headers: { 'Content-Type': 'text/plain', 'Content-Length': '13' } });
|
|
105
|
+
}
|
|
106
|
+
else if (path === '/stream-trailer') {
|
|
107
|
+
// chunked stream with trailing headers (Node addTrailers
|
|
108
|
+
// semantics: `Trailer` declaration + frames, then the footer)
|
|
109
|
+
return new Response(new ReadableStream({
|
|
110
|
+
start(control) {
|
|
111
|
+
control.enqueue(new TextEncoder().encode('chunkA/'));
|
|
112
|
+
control.enqueue(new TextEncoder().encode('chunkB'));
|
|
113
|
+
control.close();
|
|
114
|
+
}
|
|
115
|
+
}), {
|
|
116
|
+
headers: { 'Content-Type': 'text/plain', 'Trailer': 'X-Exit-Code' },
|
|
117
|
+
trailers: Promise.resolve({ 'X-Exit-Code': '3' })
|
|
118
|
+
});
|
|
119
|
+
}
|
|
85
120
|
else {
|
|
86
121
|
return new Response('not found', { status: 404 });
|
|
87
122
|
}
|
|
@@ -101,6 +136,43 @@ export const suite = {
|
|
|
101
136
|
t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0);
|
|
102
137
|
t.checkTrue('body = hello world', r.endsWith('hello world'));
|
|
103
138
|
t.checkTrue('content-type text', r.toLowerCase().indexOf('content-type: text/plain') >= 0);
|
|
139
|
+
t.checkTrue('static body gets content-length', r.toLowerCase().indexOf('content-length: 11') >= 0);
|
|
140
|
+
// ── streaming chunked response (length unknown) ──
|
|
141
|
+
t.section('streaming chunked response');
|
|
142
|
+
r = await httpSend('127.0.0.1', port, 'GET /stream HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n');
|
|
143
|
+
t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0);
|
|
144
|
+
t.checkTrue('transfer-encoding chunked', r.toLowerCase().indexOf('transfer-encoding: chunked') >= 0);
|
|
145
|
+
t.checkTrue('no content-length', r.toLowerCase().indexOf('content-length:') < 0);
|
|
146
|
+
t.checkTrue('chunk frames + terminator', r.indexOf('7\r\nchunkA/\r\n6\r\nchunkB\r\n0\r\n\r\n') >= 0);
|
|
147
|
+
// ── streaming raw response with known length ──
|
|
148
|
+
t.section('streaming known-length response');
|
|
149
|
+
r = await httpSend('127.0.0.1', port, 'GET /file HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n');
|
|
150
|
+
t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0);
|
|
151
|
+
t.checkTrue('content-length 13', r.toLowerCase().indexOf('content-length: 13') >= 0);
|
|
152
|
+
t.checkTrue('no chunked encoding', r.toLowerCase().indexOf('transfer-encoding: chunked') < 0);
|
|
153
|
+
t.checkTrue('streamed raw body', r.includes('chunkA/chunkB'));
|
|
154
|
+
t.checkTrue('no chunk frame headers', r.indexOf('7\r\nchunkA/') < 0);
|
|
155
|
+
// ── HEAD of a streamed body: headers only, empty body ──
|
|
156
|
+
t.section('HEAD streaming response');
|
|
157
|
+
r = await httpSend('127.0.0.1', port, 'HEAD /stream HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n');
|
|
158
|
+
t.checkTrue('HEAD 200', r.indexOf('HTTP/1.1 200') === 0);
|
|
159
|
+
t.checkTrue('HEAD content-length 13', r.toLowerCase().indexOf('content-length: 13') >= 0);
|
|
160
|
+
const headBodyIdx = r.indexOf('\r\n\r\n');
|
|
161
|
+
t.checkTrue('HEAD empty body', r.slice(headBodyIdx + 4).length === 0);
|
|
162
|
+
// ── streaming chunked response with trailing headers ──
|
|
163
|
+
t.section('streaming trailer response');
|
|
164
|
+
r = await httpSend('127.0.0.1', port, 'GET /stream-trailer HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n');
|
|
165
|
+
t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0);
|
|
166
|
+
t.checkTrue('trailer declared in head', r.toLowerCase().indexOf('trailer: x-exit-code') >= 0);
|
|
167
|
+
t.checkTrue('terminator carries trailer', r.indexOf('0\r\nX-Exit-Code: 3\r\n\r\n') >= 0);
|
|
168
|
+
t.checkTrue('trailer footer after body', r.indexOf('7\r\nchunkA/\r\n6\r\nchunkB\r\n0\r\nX-Exit-Code: 3\r\n\r\n') >= 0);
|
|
169
|
+
// ── fetch client consumes chunked response with trailer footer ──
|
|
170
|
+
t.section('fetch consumes trailer response');
|
|
171
|
+
{
|
|
172
|
+
const rr = await fetch(`http://127.0.0.1:${port}/stream-trailer`);
|
|
173
|
+
t.checkTrue('status 200', rr.status === 200);
|
|
174
|
+
t.checkTrue('body decoded without trailer', await rr.text() === 'chunkA/chunkB');
|
|
175
|
+
}
|
|
104
176
|
// ── POST with body ──
|
|
105
177
|
t.section('POST with body');
|
|
106
178
|
r = await httpSend('127.0.0.1', port, 'POST /echo HTTP/1.1\r\nHost: x\r\nContent-Length: 7\r\nConnection: close\r\n\r\npayload');
|
|
@@ -132,7 +204,7 @@ export const suite = {
|
|
|
132
204
|
t.checkTrue('connection close on last', r.toLowerCase().indexOf('connection: close') >= 0);
|
|
133
205
|
// ── server received expected requests ──
|
|
134
206
|
t.section('request log');
|
|
135
|
-
t.check('total requests',
|
|
207
|
+
t.check('total requests', 12, requests.length);
|
|
136
208
|
t.checkTrue('saw GET /', requests.indexOf('GET /') >= 0);
|
|
137
209
|
t.checkTrue('saw POST /echo', requests.indexOf('POST /echo') >= 0);
|
|
138
210
|
server.close();
|