quickwin 1.3.0 → 2.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 (167) hide show
  1. package/README.en.md +18 -24
  2. package/README.md +17 -23
  3. package/examples/PdfCanvas.js +8 -37
  4. package/examples/PdfCanvas.tsx +11 -41
  5. package/examples/exec_server.js +20155 -1420
  6. package/examples/exec_server.tsx +489 -0
  7. package/examples/exec_server_worker.js +383 -33
  8. package/examples/exec_server_worker.ts +408 -30
  9. package/examples/pdf_preview2.js +25 -82
  10. package/examples/pdf_preview2.ts +27 -84
  11. package/examples/pdf_viewer.js +1424 -1180
  12. package/examples/pdf_viewer.tsx +16 -67
  13. package/examples/setres.js +22 -47
  14. package/examples/setres.ts +23 -57
  15. package/examples/test_gallery.js +1418 -1075
  16. package/examples/test_react_lazy.js +1391 -1059
  17. package/examples/test_react_listbox.js +1390 -1058
  18. package/examples/test_root_window.js +1384 -1052
  19. package/examples/test_tab.js +1391 -1059
  20. package/lib/abort.js +70 -0
  21. package/lib/abort.ts +69 -0
  22. package/lib/certs.js +219 -0
  23. package/lib/certs.ts +229 -0
  24. package/lib/exec-log.js +15 -0
  25. package/lib/exec-log.ts +30 -0
  26. package/lib/fetch.js +88 -7
  27. package/lib/fetch.ts +73 -7
  28. package/lib/ffi/bind.js +441 -0
  29. package/lib/ffi/bind.ts +539 -0
  30. package/lib/ffi/ctype.js +194 -0
  31. package/lib/ffi/ctype.ts +369 -0
  32. package/lib/ffi/index.js +6 -0
  33. package/lib/ffi/index.ts +6 -0
  34. package/lib/ffi/struct.js +490 -0
  35. package/lib/ffi/struct.ts +753 -0
  36. package/lib/http-common.js +43 -5
  37. package/lib/http-common.ts +48 -6
  38. package/lib/http-server.js +187 -29
  39. package/lib/http-server.ts +177 -26
  40. package/lib/react-qw/components/DateTimePicker.js +18 -37
  41. package/lib/react-qw/components/Input.js +3 -10
  42. package/lib/react-qw/components/Link.js +6 -19
  43. package/lib/react-qw/components/ListView.js +50 -154
  44. package/lib/react-qw/components/PathPicker.js +28 -88
  45. package/lib/react-qw/components/ScrollView.js +15 -8
  46. package/lib/react-qw/components/Slider.js +2 -1
  47. package/lib/react-qw/components/Tab.js +6 -22
  48. package/lib/react-qw/components/Tooltip.js +10 -21
  49. package/lib/react-qw/components/TreeView.js +8 -31
  50. package/lib/react-qw/index.js +5 -3
  51. package/lib/react-qw/index.ts +8 -3
  52. package/lib/react-qw/nmhdr.js +3 -24
  53. package/lib/react-qw/nmhdr.ts +5 -30
  54. package/lib/react-qw/props.js +7 -2
  55. package/lib/react-qw/props.ts +7 -3
  56. package/lib/react-qw/reconciler.js +23 -13
  57. package/lib/react-qw/reconciler.ts +22 -13
  58. package/lib/text-measure.js +20 -23
  59. package/lib/text-measure.ts +22 -26
  60. package/lib/websocket.js +44 -4
  61. package/lib/websocket.ts +40 -4
  62. package/lib/windows/advapi32.js +53 -0
  63. package/lib/windows/advapi32.ts +64 -0
  64. package/lib/windows/comctl32.js +18 -0
  65. package/lib/windows/comctl32.ts +19 -0
  66. package/lib/windows/comdlg32.js +8 -0
  67. package/lib/windows/comdlg32.ts +10 -0
  68. package/lib/windows/gdi32.js +392 -0
  69. package/lib/windows/gdi32.ts +420 -0
  70. package/lib/windows/kernel32.js +262 -0
  71. package/lib/windows/kernel32.ts +291 -0
  72. package/lib/windows/msimg32.js +38 -0
  73. package/lib/windows/msimg32.ts +45 -0
  74. package/lib/windows/ntdll.js +9 -0
  75. package/lib/windows/ntdll.ts +11 -0
  76. package/lib/windows/ole32.js +7 -0
  77. package/lib/windows/ole32.ts +9 -0
  78. package/lib/windows/psapi.js +32 -0
  79. package/lib/windows/psapi.ts +37 -0
  80. package/lib/windows/shell32.js +13 -0
  81. package/lib/windows/shell32.ts +16 -0
  82. package/lib/windows/structs.js +519 -0
  83. package/lib/windows/structs.ts +566 -0
  84. package/lib/windows/user32.js +572 -0
  85. package/lib/windows/user32.ts +607 -0
  86. package/lib/windows/version.js +29 -0
  87. package/lib/windows/version.ts +35 -0
  88. package/lib/windows/winspool.js +71 -0
  89. package/lib/windows/winspool.ts +80 -0
  90. package/package.json +1 -1
  91. package/quickwin.d.ts +31 -40
  92. package/quickwin_const.d.ts +137 -0
  93. package/qwin-nowasm-x86.exe +0 -0
  94. package/qwin-nowasm.exe +0 -0
  95. package/qwin-x86.exe +0 -0
  96. package/qwin.exe +0 -0
  97. package/test/bench_ffi_callraw.js +236 -0
  98. package/test/bench_ffi_callraw.ts +236 -0
  99. package/test/bench_ffi_overhead.js +139 -0
  100. package/test/bench_ffi_overhead.ts +141 -0
  101. package/test/bench_sendmessage.js +104 -0
  102. package/test/bench_sendmessage.ts +100 -0
  103. package/test/bench_sendmessage_nonnull.js +187 -0
  104. package/test/bench_sendmessage_nonnull.ts +188 -0
  105. package/test/run.js +19 -4
  106. package/test/run.ts +20 -5
  107. package/test/test_basic_fetch.js +29 -0
  108. package/test/test_basic_fetch.ts +26 -0
  109. package/test/test_ffi.js +27 -45
  110. package/test/test_ffi.ts +35 -54
  111. package/test/test_ffi_abi.js +290 -0
  112. package/test/test_ffi_abi.ts +274 -0
  113. package/test/test_ffi_array.js +116 -0
  114. package/test/test_ffi_array.ts +141 -0
  115. package/test/test_ffi_bind.js +620 -8
  116. package/test/test_ffi_bind.ts +630 -8
  117. package/test/test_ffi_struct.js +632 -88
  118. package/test/test_ffi_struct.ts +666 -91
  119. package/test/test_ffi_struct_layout.js +495 -0
  120. package/test/test_ffi_struct_layout.ts +543 -0
  121. package/test/test_ffi_user32.js +103 -0
  122. package/test/test_ffi_user32.ts +113 -0
  123. package/test/test_ffi_windows.js +101 -0
  124. package/test/test_ffi_windows.ts +113 -0
  125. package/test/test_files.js +102 -0
  126. package/test/test_files.ts +121 -0
  127. package/test/test_gdi_draw.js +681 -0
  128. package/test/test_gdi_draw.ts +725 -0
  129. package/test/test_helper.js +8 -1
  130. package/test/test_helper.ts +9 -1
  131. package/test/test_http_import.js +18 -0
  132. package/test/test_http_import.ts +19 -0
  133. package/test/test_http_server.js +73 -1
  134. package/test/test_http_server.ts +75 -1
  135. package/test/test_ipv6.js +2 -2
  136. package/test/test_ipv6.ts +2 -2
  137. package/test/test_net_fetch.js +74 -11
  138. package/test/test_net_fetch.ts +73 -10
  139. package/test/test_net_websocket.js +1 -1
  140. package/test/test_net_websocket.ts +1 -1
  141. package/test/test_printer.js +137 -0
  142. package/test/test_printer.ts +148 -0
  143. package/test/test_proc.js +141 -0
  144. package/test/test_proc.ts +163 -0
  145. package/test/test_react_complex.js +1401 -1069
  146. package/test/test_react_counter.js +1391 -1059
  147. package/test/test_react_flex.js +1411 -1072
  148. package/test/test_react_hidden.js +1394 -1062
  149. package/test/test_react_listview_link.js +1391 -1059
  150. package/test/test_react_notify.js +18638 -0
  151. package/test/test_react_render.js +1391 -1059
  152. package/test/test_registry.js +74 -0
  153. package/test/test_registry.ts +95 -0
  154. package/test/test_sysinfo.js +122 -0
  155. package/test/test_sysinfo.ts +141 -0
  156. package/test/test_wolfssl_read.js +45 -43
  157. package/test/test_wolfssl_read.ts +42 -40
  158. package/test/test_worker_net.js +35 -17
  159. package/test/test_worker_net.ts +27 -15
  160. package/tsconfig.json +1 -1
  161. package/examples/exec_server.ts +0 -80
  162. package/lib/ffi-bind.js +0 -85
  163. package/lib/ffi-bind.ts +0 -170
  164. package/lib/ffi-struct.js +0 -224
  165. package/lib/ffi-struct.ts +0 -239
  166. package/lib/win-common-structs.js +0 -39
  167. package/lib/win-common-structs.ts +0 -41
@@ -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
+ }
@@ -0,0 +1,102 @@
1
+ import { WCHAR } from '../lib/ffi/bind.js';
2
+ import { NULL, PtrArrayBuffer } from '../lib/ffi/ctype.js';
3
+ import { CloseHandle, CopyFile, CreateDirectory, CreateFile, DeleteFile, FindClose, FindFirstFile, FindNextFile, GetFileAttributes, GetFileSize, GetTempPath, MoveFileEx, ReadFile, RemoveDirectory, SetFilePointer, WriteFile, } from '../lib/windows/kernel32.js';
4
+ import { WIN32_FIND_DATAW } from '../lib/windows/structs.js';
5
+ const GENERIC_READ = 0x80000000;
6
+ const GENERIC_WRITE = 0x40000000;
7
+ const FILE_ATTRIBUTE_DIRECTORY = 0x10;
8
+ const INVALID32 = 0xffffffff;
9
+ // ASCII payload ↔ UTF-16LE 缓冲互转(REG_SZ 同型,测试内联)
10
+ function toWide(s) {
11
+ const p = new PtrArrayBuffer(s.length * 2);
12
+ const dv = new DataView(p);
13
+ for (let i = 0; i < s.length; i++)
14
+ dv.setUint16(i * 2, s.charCodeAt(i), true);
15
+ return p;
16
+ }
17
+ function fromWide(p, chars) {
18
+ const dv = new DataView(p);
19
+ let out = '';
20
+ for (let i = 0; i < chars; i++)
21
+ out += String.fromCharCode(dv.getUint16(i * 2, true));
22
+ return out;
23
+ }
24
+ export const suite = {
25
+ name: 'files',
26
+ run: (t) => {
27
+ t.section('GetTempPath + CreateDirectory');
28
+ const tmpW = WCHAR.alloc(260);
29
+ const tmpLen = GetTempPath(260, tmpW);
30
+ t.checkTrue(`temp path non-empty (${tmpLen})`, tmpLen > 0);
31
+ const dir = `${WCHAR.decode(tmpW)}QuickWinTest`; // GetTempPath 尾带反斜杠
32
+ // 目录可能残留自上次中断的 run:已存在(且是目录)则静默复用
33
+ if (CreateDirectory(dir, NULL) === 0 &&
34
+ (GetFileAttributes(dir) & FILE_ATTRIBUTE_DIRECTORY) === 0) {
35
+ t.checkTrue('CreateDirectory', false);
36
+ return;
37
+ }
38
+ const pathA = `${dir}\\a.txt`;
39
+ const pathB = `${dir}\\b.txt`;
40
+ const pathC = `${dir}\\c.txt`;
41
+ const pathCn = `${dir}\\中文文件.txt`;
42
+ t.section('CreateFile + WriteFile: write payload');
43
+ const hW = CreateFile(pathA, GENERIC_WRITE, 0, NULL, 2 /* gui.FileCreation.CREATE_ALWAYS */, 0x80 /* FILE_ATTRIBUTE_NORMAL */, NULL);
44
+ t.checkTrue(`write handle valid (${hW})`, hW !== -1); // 失败 = INVALID_HANDLE_VALUE(-1)
45
+ const payload = 'hello files';
46
+ const wData = toWide(payload);
47
+ const pWr = new PtrArrayBuffer(4);
48
+ t.checkTrue('WriteFile ok', WriteFile(hW, wData, payload.length * 2, pWr, NULL) !== 0);
49
+ t.check('bytes written', payload.length * 2, new DataView(pWr).getUint32(0, true));
50
+ t.checkTrue('CloseHandle (writer)', CloseHandle(hW) !== 0);
51
+ t.section('CreateFile(READ) + GetFileSize + SetFilePointer + ReadFile');
52
+ const hR = CreateFile(pathA, GENERIC_READ, 0, NULL, 3 /* gui.FileCreation.OPEN_EXISTING */, 0x80, NULL);
53
+ t.checkTrue(`read handle valid (${hR})`, hR !== -1);
54
+ t.check('GetFileSize', payload.length * 2, GetFileSize(hR, NULL));
55
+ t.check('SetFilePointer BEGIN → 0', 0, SetFilePointer(hR, 0, NULL, 0 /* gui.FileSeekFrom.BEGIN */));
56
+ const rData = new PtrArrayBuffer(payload.length * 2);
57
+ const pRd = new PtrArrayBuffer(4);
58
+ t.checkTrue('ReadFile ok', ReadFile(hR, rData, payload.length * 2, pRd, NULL) !== 0);
59
+ t.check('read roundtrip', payload, fromWide(rData, payload.length));
60
+ // seek 到 END-4 验证负偏移 + 读尾 4 字节
61
+ t.check('SetFilePointer END-4', payload.length * 2 - 4, SetFilePointer(hR, -4, NULL, 2 /* gui.FileSeekFrom.END */));
62
+ const rTail = new PtrArrayBuffer(8);
63
+ ReadFile(hR, rTail, 4, pRd, NULL);
64
+ t.check('seeked tail', payload.slice(-2), fromWide(rTail, 2));
65
+ CloseHandle(hR);
66
+ t.section('CopyFile + MoveFileEx + GetFileAttributes');
67
+ const hB = CreateFile(pathB, GENERIC_WRITE, 0, NULL, 2 /* gui.FileCreation.CREATE_ALWAYS */, 0x80, NULL);
68
+ const bData = toWide('bb');
69
+ WriteFile(hB, bData, 4, pWr, NULL);
70
+ CloseHandle(hB);
71
+ t.checkTrue('CopyFile ok', CopyFile(pathB, pathC, 0) !== 0);
72
+ const hC = CreateFile(pathC, GENERIC_READ, 0, NULL, 3 /* gui.FileCreation.OPEN_EXISTING */, 0x80, NULL);
73
+ t.check('copied size', 4, GetFileSize(hC, NULL));
74
+ t.checkTrue('CloseHandle (copy reader)', CloseHandle(hC) !== 0);
75
+ t.checkTrue('MoveFileEx replace ok', MoveFileEx(pathB, pathC, 0x1 /* MOVEFILE_REPLACE_EXISTING */) !== 0);
76
+ t.check('moved: source gone', INVALID32, GetFileAttributes(pathB));
77
+ t.checkTrue('dest is file (not dir)', (GetFileAttributes(pathC) & FILE_ATTRIBUTE_DIRECTORY) === 0);
78
+ t.section('FindFirstFile/FindNextFile: enumerate *.txt incl. CJK name');
79
+ const hCn = CreateFile(pathCn, GENERIC_WRITE, 0, NULL, 2 /* gui.FileCreation.CREATE_ALWAYS */, 0x80, NULL);
80
+ CloseHandle(hCn); // 0 字节中文名文件
81
+ const fd = WIN32_FIND_DATAW.alloc();
82
+ const hFind = FindFirstFile(`${dir}\\a.txt`, fd.ptr);
83
+ t.checkTrue(`FindFirstFile ok (${hFind})`, hFind !== -1);
84
+ t.check('found name', 'a.txt', WIN32_FIND_DATAW.decode(fd).cFileName);
85
+ t.checkTrue('FindNextFile exhausted = 0', FindNextFile(hFind, fd.ptr) === 0);
86
+ t.checkTrue('FindClose', FindClose(hFind) !== 0);
87
+ let count = 0;
88
+ const hEnum = FindFirstFile(`${dir}\\*.txt`, fd.ptr);
89
+ if (hEnum !== -1) {
90
+ count = 1;
91
+ while (FindNextFile(hEnum, fd.ptr) !== 0)
92
+ count++;
93
+ FindClose(hEnum);
94
+ }
95
+ t.check('enum *.txt count (a/c/CJK)', 3, count);
96
+ t.section('DeleteFile + RemoveDirectory: cleanup');
97
+ t.checkTrue('DeleteFile ×3', DeleteFile(pathA) !== 0 && DeleteFile(pathC) !== 0
98
+ && DeleteFile(pathCn) !== 0);
99
+ t.checkTrue('RemoveDirectory', RemoveDirectory(dir) !== 0);
100
+ t.check('dir gone', INVALID32, GetFileAttributes(dir));
101
+ },
102
+ };
@@ -0,0 +1,121 @@
1
+ import * as gui from 'gui'
2
+ import { WCHAR } from '../lib/ffi/bind.js'
3
+ import { NULL, PtrArrayBuffer } from '../lib/ffi/ctype.js'
4
+ import {
5
+ CloseHandle, CopyFile, CreateDirectory, CreateFile, DeleteFile,
6
+ FindClose, FindFirstFile, FindNextFile, GetFileAttributes, GetFileSize,
7
+ GetTempPath, MoveFileEx, ReadFile, RemoveDirectory, SetFilePointer, WriteFile,
8
+ } from '../lib/windows/kernel32.js'
9
+ import { WIN32_FIND_DATAW } from '../lib/windows/structs.js'
10
+ import { Tester } from './test_helper.js'
11
+
12
+ const GENERIC_READ = 0x80000000
13
+ const GENERIC_WRITE = 0x40000000
14
+ const FILE_ATTRIBUTE_DIRECTORY = 0x10
15
+ const INVALID32 = 0xffffffff
16
+
17
+ // ASCII payload ↔ UTF-16LE 缓冲互转(REG_SZ 同型,测试内联)
18
+ function toWide(s: string): PtrArrayBuffer<string> {
19
+ const p = new PtrArrayBuffer(s.length * 2)
20
+ const dv = new DataView(p)
21
+ for (let i = 0; i < s.length; i++) dv.setUint16(i * 2, s.charCodeAt(i), true)
22
+ return p
23
+ }
24
+ function fromWide(p: PtrArrayBuffer<string>, chars: number): string {
25
+ const dv = new DataView(p)
26
+ let out = ''
27
+ for (let i = 0; i < chars; i++) out += String.fromCharCode(dv.getUint16(i * 2, true))
28
+ return out
29
+ }
30
+
31
+ export const suite = {
32
+ name: 'files',
33
+ run: (t: Tester) => {
34
+ t.section('GetTempPath + CreateDirectory')
35
+ const tmpW = WCHAR.alloc(260)
36
+ const tmpLen = GetTempPath(260, tmpW)
37
+ t.checkTrue(`temp path non-empty (${tmpLen})`, tmpLen > 0)
38
+ const dir = `${WCHAR.decode(tmpW)}QuickWinTest` // GetTempPath 尾带反斜杠
39
+ // 目录可能残留自上次中断的 run:已存在(且是目录)则静默复用
40
+ if (CreateDirectory(dir, NULL) === 0 &&
41
+ (GetFileAttributes(dir) & FILE_ATTRIBUTE_DIRECTORY) === 0) {
42
+ t.checkTrue('CreateDirectory', false)
43
+ return
44
+ }
45
+ const pathA = `${dir}\\a.txt`
46
+ const pathB = `${dir}\\b.txt`
47
+ const pathC = `${dir}\\c.txt`
48
+ const pathCn = `${dir}\\中文文件.txt`
49
+
50
+ t.section('CreateFile + WriteFile: write payload')
51
+ const hW = CreateFile(pathA, GENERIC_WRITE, 0, NULL,
52
+ gui.FileCreation.CREATE_ALWAYS, 0x80 /* FILE_ATTRIBUTE_NORMAL */, NULL)
53
+ t.checkTrue(`write handle valid (${hW})`, hW !== -1) // 失败 = INVALID_HANDLE_VALUE(-1)
54
+ const payload = 'hello files'
55
+ const wData = toWide(payload)
56
+ const pWr = new PtrArrayBuffer(4)
57
+ t.checkTrue('WriteFile ok', WriteFile(hW, wData, payload.length * 2, pWr, NULL) !== 0)
58
+ t.check('bytes written', payload.length * 2, new DataView(pWr).getUint32(0, true))
59
+ t.checkTrue('CloseHandle (writer)', CloseHandle(hW) !== 0)
60
+
61
+ t.section('CreateFile(READ) + GetFileSize + SetFilePointer + ReadFile')
62
+ const hR = CreateFile(pathA, GENERIC_READ, 0, NULL,
63
+ gui.FileCreation.OPEN_EXISTING, 0x80, NULL)
64
+ t.checkTrue(`read handle valid (${hR})`, hR !== -1)
65
+ t.check('GetFileSize', payload.length * 2, GetFileSize(hR, NULL))
66
+ t.check('SetFilePointer BEGIN → 0', 0, SetFilePointer(hR, 0, NULL, gui.FileSeekFrom.BEGIN))
67
+ const rData = new PtrArrayBuffer(payload.length * 2)
68
+ const pRd = new PtrArrayBuffer(4)
69
+ t.checkTrue('ReadFile ok', ReadFile(hR, rData, payload.length * 2, pRd, NULL) !== 0)
70
+ t.check('read roundtrip', payload, fromWide(rData, payload.length))
71
+ // seek 到 END-4 验证负偏移 + 读尾 4 字节
72
+ t.check('SetFilePointer END-4', payload.length * 2 - 4,
73
+ SetFilePointer(hR, -4, NULL, gui.FileSeekFrom.END))
74
+ const rTail = new PtrArrayBuffer(8)
75
+ ReadFile(hR, rTail, 4, pRd, NULL)
76
+ t.check('seeked tail', payload.slice(-2), fromWide(rTail, 2))
77
+ CloseHandle(hR)
78
+
79
+ t.section('CopyFile + MoveFileEx + GetFileAttributes')
80
+ const hB = CreateFile(pathB, GENERIC_WRITE, 0, NULL,
81
+ gui.FileCreation.CREATE_ALWAYS, 0x80, NULL)
82
+ const bData = toWide('bb')
83
+ WriteFile(hB, bData, 4, pWr, NULL)
84
+ CloseHandle(hB)
85
+ t.checkTrue('CopyFile ok', CopyFile(pathB, pathC, 0) !== 0)
86
+ const hC = CreateFile(pathC, GENERIC_READ, 0, NULL,
87
+ gui.FileCreation.OPEN_EXISTING, 0x80, NULL)
88
+ t.check('copied size', 4, GetFileSize(hC, NULL))
89
+ t.checkTrue('CloseHandle (copy reader)', CloseHandle(hC) !== 0)
90
+ t.checkTrue('MoveFileEx replace ok',
91
+ MoveFileEx(pathB, pathC, 0x1 /* MOVEFILE_REPLACE_EXISTING */) !== 0)
92
+ t.check('moved: source gone', INVALID32, GetFileAttributes(pathB))
93
+ t.checkTrue('dest is file (not dir)',
94
+ (GetFileAttributes(pathC) & FILE_ATTRIBUTE_DIRECTORY) === 0)
95
+
96
+ t.section('FindFirstFile/FindNextFile: enumerate *.txt incl. CJK name')
97
+ const hCn = CreateFile(pathCn, GENERIC_WRITE, 0, NULL,
98
+ gui.FileCreation.CREATE_ALWAYS, 0x80, NULL)
99
+ CloseHandle(hCn) // 0 字节中文名文件
100
+ const fd = WIN32_FIND_DATAW.alloc()
101
+ const hFind = FindFirstFile(`${dir}\\a.txt`, fd.ptr)
102
+ t.checkTrue(`FindFirstFile ok (${hFind})`, hFind !== -1)
103
+ t.check('found name', 'a.txt', WIN32_FIND_DATAW.decode(fd).cFileName)
104
+ t.checkTrue('FindNextFile exhausted = 0', FindNextFile(hFind, fd.ptr) === 0)
105
+ t.checkTrue('FindClose', FindClose(hFind) !== 0)
106
+ let count = 0
107
+ const hEnum = FindFirstFile(`${dir}\\*.txt`, fd.ptr)
108
+ if (hEnum !== -1) {
109
+ count = 1
110
+ while (FindNextFile(hEnum, fd.ptr) !== 0) count++
111
+ FindClose(hEnum)
112
+ }
113
+ t.check('enum *.txt count (a/c/CJK)', 3, count)
114
+
115
+ t.section('DeleteFile + RemoveDirectory: cleanup')
116
+ t.checkTrue('DeleteFile ×3', DeleteFile(pathA) !== 0 && DeleteFile(pathC) !== 0
117
+ && DeleteFile(pathCn) !== 0)
118
+ t.checkTrue('RemoveDirectory', RemoveDirectory(dir) !== 0)
119
+ t.check('dir gone', INVALID32, GetFileAttributes(dir))
120
+ },
121
+ }