cj-tauri 2.0.0 → 2.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.md +206 -139
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +63 -52
- package/cli/cjpm.toml +22 -22
- package/cli/src/doctor.cj +308 -306
- package/cli/src/main.cj +129 -129
- package/cli/src/manifest.cj +643 -558
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -770
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +369 -369
- package/cli/src/sync.cj +638 -526
- package/cli/src/util.cj +168 -168
- package/cli/templates/app/README.md +51 -48
- package/cli/templates/app/assets/capabilities/default.json +7 -7
- package/cli/templates/app/assets/ui/index.html +120 -118
- package/cli/templates/app/cj-tauri.toml +32 -22
- package/cli/templates/app/cjpm.toml +34 -34
- package/cli/templates/app/src/main.cj +101 -90
- package/cli/templates/app-react/README.md +56 -56
- package/cli/templates/app-react/assets/capabilities/default.json +7 -7
- package/cli/templates/app-react/assets/ui/index.html +25 -25
- package/cli/templates/app-react/assets/ui/package.json +19 -19
- package/cli/templates/app-react/assets/ui/src/App.css +67 -67
- package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
- package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
- package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-react/cj-tauri.toml +33 -23
- package/cli/templates/app-react/cjpm.toml +34 -34
- package/cli/templates/app-react/src/main.cj +91 -91
- package/cli/templates/app-vue/README.md +56 -56
- package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
- package/cli/templates/app-vue/assets/ui/index.html +25 -25
- package/cli/templates/app-vue/assets/ui/package.json +18 -18
- package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
- package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
- package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-vue/cj-tauri.toml +33 -23
- package/cli/templates/app-vue/cjpm.toml +34 -34
- package/cli/templates/app-vue/src/main.cj +91 -91
- package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
- package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
- package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/README.md +84 -79
- package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
- package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
- package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
- package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
- package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
- package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
- package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
- package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3745 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -53
- package/src/api_system.cj +67 -67
- package/src/app.cj +335 -969
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -76
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +142 -142
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -607
- package/src/host_arkweb.cj +924 -778
- package/src/host_webkit.cj +663 -570
- package/src/host_webview2.cj +663 -570
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +401 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -62
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -77
- package/src/platform/platform_assets.cj +33 -33
- package/src/platform/platform_discover.cj +176 -176
- package/src/platform/platform_env.cj +29 -29
- package/src/platform/platform_feature.cj +31 -31
- package/src/platform/platform_page_source.cj +87 -87
- package/src/platform/platform_target_info.cj +32 -32
- package/src/platform_host_provider.cj +87 -87
- package/src/plugin.cj +333 -333
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +158 -158
- package/src/plugins/notification/plugin_notification.cj +154 -154
- package/src/plugins/serial/plugin_serial.cj +458 -454
- package/src/plugins/serial/serial_bridge.cj +194 -194
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +258 -258
- package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +130 -127
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -201
- package/src/tests/ipc_hub_test.cj +563 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +264 -251
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +605 -585
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/plugins/serial/serial_ohos_stub.cj +0 -61
- package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
|
@@ -1,237 +1,237 @@
|
|
|
1
|
-
package cjTauri.tests
|
|
2
|
-
|
|
3
|
-
import cjTauri.*
|
|
4
|
-
import cjTauri.plugins.dialog.*
|
|
5
|
-
import std.collection.*
|
|
6
|
-
import stdx.encoding.json.*
|
|
7
|
-
import std.unittest.*
|
|
8
|
-
import std.unittest.testmacro.*
|
|
9
|
-
|
|
10
|
-
/*
|
|
11
|
-
* dialog 插件单元测试:命令面、命名权限集、参数校验、未装配宿主时的可读错误。
|
|
12
|
-
* 只碰纯逻辑——原生对话框要真窗口,归实机验证(examples/plugin-dialog)。
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/** 造一个只带 message 的对象 */
|
|
16
|
-
func messageArgs(text: String): JsonObject {
|
|
17
|
-
var o = JsonObject()
|
|
18
|
-
o.put("message", JsonString(text))
|
|
19
|
-
return o
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** 造一个只带 title 的对象 */
|
|
23
|
-
func titleArgs(t: String): JsonObject {
|
|
24
|
-
var o = JsonObject()
|
|
25
|
-
o.put("title", JsonString(t))
|
|
26
|
-
return o
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
@Test
|
|
30
|
-
func testDialogPluginSurface(): Unit {
|
|
31
|
-
let p = DialogPlugin()
|
|
32
|
-
@Expect(p.name(), "dialog")
|
|
33
|
-
let names = pluginCommandNames(p)
|
|
34
|
-
@Expect(names.size, 4)
|
|
35
|
-
var hasOpen = false
|
|
36
|
-
var hasSave = false
|
|
37
|
-
var hasPick = false
|
|
38
|
-
var hasMessage = false
|
|
39
|
-
for (n in names) {
|
|
40
|
-
if (n == "dialog:open") {
|
|
41
|
-
hasOpen = true
|
|
42
|
-
}
|
|
43
|
-
if (n == "dialog:save") {
|
|
44
|
-
hasSave = true
|
|
45
|
-
}
|
|
46
|
-
if (n == "dialog:pickFolder") {
|
|
47
|
-
hasPick = true
|
|
48
|
-
}
|
|
49
|
-
if (n == "dialog:message") {
|
|
50
|
-
hasMessage = true
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
@Expect(hasOpen, true)
|
|
54
|
-
@Expect(hasSave, true)
|
|
55
|
-
@Expect(hasPick, true)
|
|
56
|
-
@Expect(hasMessage, true)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
@Test
|
|
60
|
-
func testDialogPluginShimTargetsItsOwnNamespace(): Unit {
|
|
61
|
-
let js = DialogPlugin().jsShim()
|
|
62
|
-
@Expect(js.contains("t.dialog"), true)
|
|
63
|
-
@Expect(js.contains("'dialog:open'"), true)
|
|
64
|
-
@Expect(js.contains("'dialog:save'"), true)
|
|
65
|
-
@Expect(js.contains("'dialog:pickFolder'"), true)
|
|
66
|
-
@Expect(js.contains("'dialog:message'"), true)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
@Test
|
|
70
|
-
func testDialogPluginDeclaresPermissionSets(): Unit {
|
|
71
|
-
let sets = pluginPermissionSets(DialogPlugin())
|
|
72
|
-
@Expect(sets.size, 2)
|
|
73
|
-
let files = sets.get("dialog:files").getOrThrow()
|
|
74
|
-
@Expect(files.size, 3)
|
|
75
|
-
var filesHasMessage = false
|
|
76
|
-
for (m in files) {
|
|
77
|
-
if (m == "dialog:message") {
|
|
78
|
-
filesHasMessage = true
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// 只想挑文件(或目录)的应用不该顺带拿到「弹任意提示框」
|
|
82
|
-
@Expect(filesHasMessage, false)
|
|
83
|
-
@Expect(sets.get("dialog:default").getOrThrow().size, 4)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
@Test
|
|
87
|
-
func testDialogPickFolderGoesToFolderDialog(): Unit {
|
|
88
|
-
// 选目录走的是独立的宿主方法(不是文件框那条):桩把标题记下来,用例断言转交的确实是 title
|
|
89
|
-
let host = FakeHost("main")
|
|
90
|
-
let cmds = DialogCommands()
|
|
91
|
-
cmds.attachHost(host)
|
|
92
|
-
let ctx = IpcContext("main", IpcHub())
|
|
93
|
-
|
|
94
|
-
cmds.handle("dialog:pickFolder", titleArgs("选工作区"), ctx)
|
|
95
|
-
@Expect(host.folderDialogTitle, "选工作区")
|
|
96
|
-
|
|
97
|
-
// 不给标题时兜底,别把空标题交给平台(空串会让平台用自己的默认文案)
|
|
98
|
-
cmds.handle("dialog:pickFolder", JsonObject(), ctx)
|
|
99
|
-
@Expect(host.folderDialogTitle, "选择文件夹")
|
|
100
|
-
|
|
101
|
-
// 用户取消(桩恒回空串)时命令的返回也是空串——上游按「没选」处理
|
|
102
|
-
let r = cmds.handle("dialog:pickFolder", JsonObject(), ctx)
|
|
103
|
-
@Expect(r.toString(), "\"\"")
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
@Test
|
|
107
|
-
func testDialogPermissionSetGatesCommands(): Unit {
|
|
108
|
-
let out = ArrayList<String>()
|
|
109
|
-
let reg = CapabilityRegistry()
|
|
110
|
-
reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"permissions\":[\"dialog:files\"]}"))
|
|
111
|
-
let hub = hubWithRegistry(reg, out)
|
|
112
|
-
registerPluginCommands(hub, DialogPlugin())
|
|
113
|
-
registerPluginPermissionSets(reg, DialogPlugin())
|
|
114
|
-
|
|
115
|
-
// 集内命令放行:这条会真的去弹对话框,测试环境没有宿主 → 报的是业务错误,不是「未授权」
|
|
116
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":21,\"cmd\":\"dialog:open\"}")
|
|
117
|
-
waitSink(out, 1)
|
|
118
|
-
@Expect(out.size, 1)
|
|
119
|
-
let firstError = strField(out[0], "error")
|
|
120
|
-
@Expect(firstError.contains("command not allowed"), false)
|
|
121
|
-
|
|
122
|
-
// 集外命令仍拒:放行的是那一组,不是整个插件
|
|
123
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":22,\"cmd\":\"dialog:message\"}")
|
|
124
|
-
@Expect(out.size, 2)
|
|
125
|
-
@Expect(strField(out[1], "error"), "command not allowed: dialog:message")
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
@Test
|
|
129
|
-
func testDialogMessageRequiresTextArgument(): Unit {
|
|
130
|
-
let cmds = DialogPlugin().commands()
|
|
131
|
-
let ctx = IpcContext("main", IpcHub())
|
|
132
|
-
var threw = false
|
|
133
|
-
try {
|
|
134
|
-
cmds.get("message").getOrThrow().handle("dialog:message", JsonObject(), ctx)
|
|
135
|
-
} catch (e: CommandException) {
|
|
136
|
-
threw = true
|
|
137
|
-
@Expect(e.message.contains("missing argument \"message\""), true)
|
|
138
|
-
}
|
|
139
|
-
@Expect(threw, true)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
@Test
|
|
143
|
-
func testDialogWithoutHostIsReadableError(): Unit {
|
|
144
|
-
// 没经 .plugin() 装配(没有宿主)时调用:给的是可读错误,而不是空指针崩掉
|
|
145
|
-
let cmds = DialogPlugin().commands()
|
|
146
|
-
let ctx = IpcContext("main", IpcHub())
|
|
147
|
-
var openThrew = false
|
|
148
|
-
try {
|
|
149
|
-
cmds.get("open").getOrThrow().handle("dialog:open", JsonObject(), ctx)
|
|
150
|
-
} catch (e: CommandException) {
|
|
151
|
-
openThrew = true
|
|
152
|
-
@Expect(e.message.contains("宿主未装配"), true)
|
|
153
|
-
}
|
|
154
|
-
@Expect(openThrew, true)
|
|
155
|
-
|
|
156
|
-
var msgThrew = false
|
|
157
|
-
try {
|
|
158
|
-
cmds.get("message").getOrThrow().handle("dialog:message", messageArgs("hi"), ctx)
|
|
159
|
-
} catch (e: CommandException) {
|
|
160
|
-
msgThrew = true
|
|
161
|
-
}
|
|
162
|
-
@Expect(msgThrew, true)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
@Test
|
|
166
|
-
func testDialogRejectsUnknownCommand(): Unit {
|
|
167
|
-
let cmds = DialogPlugin().commands()
|
|
168
|
-
let ctx = IpcContext("main", IpcHub())
|
|
169
|
-
var threw = false
|
|
170
|
-
try {
|
|
171
|
-
cmds.get("open").getOrThrow().handle("dialog:nope", JsonObject(), ctx)
|
|
172
|
-
} catch (e: CommandException) {
|
|
173
|
-
threw = true
|
|
174
|
-
@Expect(e.message.contains("unknown dialog command"), true)
|
|
175
|
-
}
|
|
176
|
-
@Expect(threw, true)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* 桩侧「label → 宿主」解析器(模拟 `app.hostOf`)
|
|
181
|
-
*
|
|
182
|
-
* 用独立函数而不是内联 lambda:多分支要能 return,写成独立函数最稳。
|
|
183
|
-
*/
|
|
184
|
-
func routeProbeHost(label: String, mainHost: FakeHost, secondHost: FakeHost): Option<WebViewHost> {
|
|
185
|
-
if (label == "second") {
|
|
186
|
-
return Some(secondHost)
|
|
187
|
-
}
|
|
188
|
-
if (label == "main") {
|
|
189
|
-
return Some(mainHost)
|
|
190
|
-
}
|
|
191
|
-
return None
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
@Test
|
|
195
|
-
func testDialogRoutesToTheWindowThatAsked(): Unit {
|
|
196
|
-
// 多窗口的核心契约:报文里的 window 决定交给哪个宿主。
|
|
197
|
-
// 只用单槽(装配时那一个宿主)时,第二扇窗的框会排进 main 的队列——
|
|
198
|
-
// 2026-10-20 鸿蒙真机实测的串台就是这么来的(docs/鸿蒙ArkWeb接入踩坑.md §10.12)。
|
|
199
|
-
let mainHost = FakeHost("main")
|
|
200
|
-
let secondHost = FakeHost("second")
|
|
201
|
-
let cmds = DialogCommands()
|
|
202
|
-
cmds.attachHost(mainHost)
|
|
203
|
-
cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
|
|
204
|
-
let hub = IpcHub()
|
|
205
|
-
|
|
206
|
-
cmds.handle("dialog:message", messageArgs("second 的框"), IpcContext("second", hub))
|
|
207
|
-
@Expect(secondHost.messageDialogArgs.contains("second 的框"), true)
|
|
208
|
-
@Expect(secondHost.messageDialogArgs.contains("|second 的框"), true)
|
|
209
|
-
// 关键断言:另一扇窗**一点都没被碰到**
|
|
210
|
-
@Expect(mainHost.messageDialogArgs, "")
|
|
211
|
-
|
|
212
|
-
cmds.handle("dialog:message", messageArgs("main 的框"), IpcContext("main", hub))
|
|
213
|
-
@Expect(mainHost.messageDialogArgs.contains("main 的框"), true)
|
|
214
|
-
// 反向也成立:second 那一份不被覆盖(各持一份状态,不是共用一个槽)
|
|
215
|
-
@Expect(secondHost.messageDialogArgs.contains("second 的框"), true)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
@Test
|
|
219
|
-
func testDialogReportsUnknownWindowInsteadOfFallingBack(): Unit {
|
|
220
|
-
// 解析器在场而窗口没登记时如实报错:悄悄回落到单槽正是串台的来源
|
|
221
|
-
let mainHost = FakeHost("main")
|
|
222
|
-
let secondHost = FakeHost("second")
|
|
223
|
-
let cmds = DialogCommands()
|
|
224
|
-
cmds.attachHost(mainHost)
|
|
225
|
-
cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
|
|
226
|
-
var threw = false
|
|
227
|
-
try {
|
|
228
|
-
cmds.handle("dialog:message", messageArgs("谁的框"), IpcContext("ghost", IpcHub()))
|
|
229
|
-
} catch (e: CommandException) {
|
|
230
|
-
threw = true
|
|
231
|
-
@Expect(e.message.contains("ghost"), true)
|
|
232
|
-
}
|
|
233
|
-
@Expect(threw, true)
|
|
234
|
-
// 报错之后两个宿主都没被写过:宁可不弹,也不弹到别人窗口里
|
|
235
|
-
@Expect(mainHost.messageDialogArgs, "")
|
|
236
|
-
@Expect(secondHost.messageDialogArgs, "")
|
|
237
|
-
}
|
|
1
|
+
package cjTauri.tests
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
import cjTauri.plugins.dialog.*
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import stdx.encoding.json.*
|
|
7
|
+
import std.unittest.*
|
|
8
|
+
import std.unittest.testmacro.*
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* dialog 插件单元测试:命令面、命名权限集、参数校验、未装配宿主时的可读错误。
|
|
12
|
+
* 只碰纯逻辑——原生对话框要真窗口,归实机验证(examples/plugin-dialog)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** 造一个只带 message 的对象 */
|
|
16
|
+
func messageArgs(text: String): JsonObject {
|
|
17
|
+
var o = JsonObject()
|
|
18
|
+
o.put("message", JsonString(text))
|
|
19
|
+
return o
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 造一个只带 title 的对象 */
|
|
23
|
+
func titleArgs(t: String): JsonObject {
|
|
24
|
+
var o = JsonObject()
|
|
25
|
+
o.put("title", JsonString(t))
|
|
26
|
+
return o
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
func testDialogPluginSurface(): Unit {
|
|
31
|
+
let p = DialogPlugin()
|
|
32
|
+
@Expect(p.name(), "dialog")
|
|
33
|
+
let names = pluginCommandNames(p)
|
|
34
|
+
@Expect(names.size, 4)
|
|
35
|
+
var hasOpen = false
|
|
36
|
+
var hasSave = false
|
|
37
|
+
var hasPick = false
|
|
38
|
+
var hasMessage = false
|
|
39
|
+
for (n in names) {
|
|
40
|
+
if (n == "dialog:open") {
|
|
41
|
+
hasOpen = true
|
|
42
|
+
}
|
|
43
|
+
if (n == "dialog:save") {
|
|
44
|
+
hasSave = true
|
|
45
|
+
}
|
|
46
|
+
if (n == "dialog:pickFolder") {
|
|
47
|
+
hasPick = true
|
|
48
|
+
}
|
|
49
|
+
if (n == "dialog:message") {
|
|
50
|
+
hasMessage = true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
@Expect(hasOpen, true)
|
|
54
|
+
@Expect(hasSave, true)
|
|
55
|
+
@Expect(hasPick, true)
|
|
56
|
+
@Expect(hasMessage, true)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
func testDialogPluginShimTargetsItsOwnNamespace(): Unit {
|
|
61
|
+
let js = DialogPlugin().jsShim()
|
|
62
|
+
@Expect(js.contains("t.dialog"), true)
|
|
63
|
+
@Expect(js.contains("'dialog:open'"), true)
|
|
64
|
+
@Expect(js.contains("'dialog:save'"), true)
|
|
65
|
+
@Expect(js.contains("'dialog:pickFolder'"), true)
|
|
66
|
+
@Expect(js.contains("'dialog:message'"), true)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Test
|
|
70
|
+
func testDialogPluginDeclaresPermissionSets(): Unit {
|
|
71
|
+
let sets = pluginPermissionSets(DialogPlugin())
|
|
72
|
+
@Expect(sets.size, 2)
|
|
73
|
+
let files = sets.get("dialog:files").getOrThrow()
|
|
74
|
+
@Expect(files.size, 3)
|
|
75
|
+
var filesHasMessage = false
|
|
76
|
+
for (m in files) {
|
|
77
|
+
if (m == "dialog:message") {
|
|
78
|
+
filesHasMessage = true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 只想挑文件(或目录)的应用不该顺带拿到「弹任意提示框」
|
|
82
|
+
@Expect(filesHasMessage, false)
|
|
83
|
+
@Expect(sets.get("dialog:default").getOrThrow().size, 4)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Test
|
|
87
|
+
func testDialogPickFolderGoesToFolderDialog(): Unit {
|
|
88
|
+
// 选目录走的是独立的宿主方法(不是文件框那条):桩把标题记下来,用例断言转交的确实是 title
|
|
89
|
+
let host = FakeHost("main")
|
|
90
|
+
let cmds = DialogCommands()
|
|
91
|
+
cmds.attachHost(host)
|
|
92
|
+
let ctx = IpcContext("main", IpcHub())
|
|
93
|
+
|
|
94
|
+
cmds.handle("dialog:pickFolder", titleArgs("选工作区"), ctx)
|
|
95
|
+
@Expect(host.folderDialogTitle, "选工作区")
|
|
96
|
+
|
|
97
|
+
// 不给标题时兜底,别把空标题交给平台(空串会让平台用自己的默认文案)
|
|
98
|
+
cmds.handle("dialog:pickFolder", JsonObject(), ctx)
|
|
99
|
+
@Expect(host.folderDialogTitle, "选择文件夹")
|
|
100
|
+
|
|
101
|
+
// 用户取消(桩恒回空串)时命令的返回也是空串——上游按「没选」处理
|
|
102
|
+
let r = cmds.handle("dialog:pickFolder", JsonObject(), ctx)
|
|
103
|
+
@Expect(r.toString(), "\"\"")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test
|
|
107
|
+
func testDialogPermissionSetGatesCommands(): Unit {
|
|
108
|
+
let out = ArrayList<String>()
|
|
109
|
+
let reg = CapabilityRegistry()
|
|
110
|
+
reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"permissions\":[\"dialog:files\"]}"))
|
|
111
|
+
let hub = hubWithRegistry(reg, out)
|
|
112
|
+
registerPluginCommands(hub, DialogPlugin())
|
|
113
|
+
registerPluginPermissionSets(reg, DialogPlugin())
|
|
114
|
+
|
|
115
|
+
// 集内命令放行:这条会真的去弹对话框,测试环境没有宿主 → 报的是业务错误,不是「未授权」
|
|
116
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":21,\"cmd\":\"dialog:open\"}")
|
|
117
|
+
waitSink(out, 1)
|
|
118
|
+
@Expect(out.size, 1)
|
|
119
|
+
let firstError = strField(out[0], "error")
|
|
120
|
+
@Expect(firstError.contains("command not allowed"), false)
|
|
121
|
+
|
|
122
|
+
// 集外命令仍拒:放行的是那一组,不是整个插件
|
|
123
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":22,\"cmd\":\"dialog:message\"}")
|
|
124
|
+
@Expect(out.size, 2)
|
|
125
|
+
@Expect(strField(out[1], "error"), "command not allowed: dialog:message")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
func testDialogMessageRequiresTextArgument(): Unit {
|
|
130
|
+
let cmds = DialogPlugin().commands()
|
|
131
|
+
let ctx = IpcContext("main", IpcHub())
|
|
132
|
+
var threw = false
|
|
133
|
+
try {
|
|
134
|
+
cmds.get("message").getOrThrow().handle("dialog:message", JsonObject(), ctx)
|
|
135
|
+
} catch (e: CommandException) {
|
|
136
|
+
threw = true
|
|
137
|
+
@Expect(e.message.contains("missing argument \"message\""), true)
|
|
138
|
+
}
|
|
139
|
+
@Expect(threw, true)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Test
|
|
143
|
+
func testDialogWithoutHostIsReadableError(): Unit {
|
|
144
|
+
// 没经 .plugin() 装配(没有宿主)时调用:给的是可读错误,而不是空指针崩掉
|
|
145
|
+
let cmds = DialogPlugin().commands()
|
|
146
|
+
let ctx = IpcContext("main", IpcHub())
|
|
147
|
+
var openThrew = false
|
|
148
|
+
try {
|
|
149
|
+
cmds.get("open").getOrThrow().handle("dialog:open", JsonObject(), ctx)
|
|
150
|
+
} catch (e: CommandException) {
|
|
151
|
+
openThrew = true
|
|
152
|
+
@Expect(e.message.contains("宿主未装配"), true)
|
|
153
|
+
}
|
|
154
|
+
@Expect(openThrew, true)
|
|
155
|
+
|
|
156
|
+
var msgThrew = false
|
|
157
|
+
try {
|
|
158
|
+
cmds.get("message").getOrThrow().handle("dialog:message", messageArgs("hi"), ctx)
|
|
159
|
+
} catch (e: CommandException) {
|
|
160
|
+
msgThrew = true
|
|
161
|
+
}
|
|
162
|
+
@Expect(msgThrew, true)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Test
|
|
166
|
+
func testDialogRejectsUnknownCommand(): Unit {
|
|
167
|
+
let cmds = DialogPlugin().commands()
|
|
168
|
+
let ctx = IpcContext("main", IpcHub())
|
|
169
|
+
var threw = false
|
|
170
|
+
try {
|
|
171
|
+
cmds.get("open").getOrThrow().handle("dialog:nope", JsonObject(), ctx)
|
|
172
|
+
} catch (e: CommandException) {
|
|
173
|
+
threw = true
|
|
174
|
+
@Expect(e.message.contains("unknown dialog command"), true)
|
|
175
|
+
}
|
|
176
|
+
@Expect(threw, true)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 桩侧「label → 宿主」解析器(模拟 `app.hostOf`)
|
|
181
|
+
*
|
|
182
|
+
* 用独立函数而不是内联 lambda:多分支要能 return,写成独立函数最稳。
|
|
183
|
+
*/
|
|
184
|
+
func routeProbeHost(label: String, mainHost: FakeHost, secondHost: FakeHost): Option<WebViewHost> {
|
|
185
|
+
if (label == "second") {
|
|
186
|
+
return Some(secondHost)
|
|
187
|
+
}
|
|
188
|
+
if (label == "main") {
|
|
189
|
+
return Some(mainHost)
|
|
190
|
+
}
|
|
191
|
+
return None
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@Test
|
|
195
|
+
func testDialogRoutesToTheWindowThatAsked(): Unit {
|
|
196
|
+
// 多窗口的核心契约:报文里的 window 决定交给哪个宿主。
|
|
197
|
+
// 只用单槽(装配时那一个宿主)时,第二扇窗的框会排进 main 的队列——
|
|
198
|
+
// 2026-10-20 鸿蒙真机实测的串台就是这么来的(docs/鸿蒙ArkWeb接入踩坑.md §10.12)。
|
|
199
|
+
let mainHost = FakeHost("main")
|
|
200
|
+
let secondHost = FakeHost("second")
|
|
201
|
+
let cmds = DialogCommands()
|
|
202
|
+
cmds.attachHost(mainHost)
|
|
203
|
+
cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
|
|
204
|
+
let hub = IpcHub()
|
|
205
|
+
|
|
206
|
+
cmds.handle("dialog:message", messageArgs("second 的框"), IpcContext("second", hub))
|
|
207
|
+
@Expect(secondHost.messageDialogArgs.contains("second 的框"), true)
|
|
208
|
+
@Expect(secondHost.messageDialogArgs.contains("|second 的框"), true)
|
|
209
|
+
// 关键断言:另一扇窗**一点都没被碰到**
|
|
210
|
+
@Expect(mainHost.messageDialogArgs, "")
|
|
211
|
+
|
|
212
|
+
cmds.handle("dialog:message", messageArgs("main 的框"), IpcContext("main", hub))
|
|
213
|
+
@Expect(mainHost.messageDialogArgs.contains("main 的框"), true)
|
|
214
|
+
// 反向也成立:second 那一份不被覆盖(各持一份状态,不是共用一个槽)
|
|
215
|
+
@Expect(secondHost.messageDialogArgs.contains("second 的框"), true)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@Test
|
|
219
|
+
func testDialogReportsUnknownWindowInsteadOfFallingBack(): Unit {
|
|
220
|
+
// 解析器在场而窗口没登记时如实报错:悄悄回落到单槽正是串台的来源
|
|
221
|
+
let mainHost = FakeHost("main")
|
|
222
|
+
let secondHost = FakeHost("second")
|
|
223
|
+
let cmds = DialogCommands()
|
|
224
|
+
cmds.attachHost(mainHost)
|
|
225
|
+
cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
|
|
226
|
+
var threw = false
|
|
227
|
+
try {
|
|
228
|
+
cmds.handle("dialog:message", messageArgs("谁的框"), IpcContext("ghost", IpcHub()))
|
|
229
|
+
} catch (e: CommandException) {
|
|
230
|
+
threw = true
|
|
231
|
+
@Expect(e.message.contains("ghost"), true)
|
|
232
|
+
}
|
|
233
|
+
@Expect(threw, true)
|
|
234
|
+
// 报错之后两个宿主都没被写过:宁可不弹,也不弹到别人窗口里
|
|
235
|
+
@Expect(mainHost.messageDialogArgs, "")
|
|
236
|
+
@Expect(secondHost.messageDialogArgs, "")
|
|
237
|
+
}
|