cj-tauri 1.1.2 → 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 (162) hide show
  1. package/README.md +137 -77
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +13 -2
  4. package/cli/cjpm.toml +22 -13
  5. package/cli/src/doctor.cj +308 -304
  6. package/cli/src/main.cj +129 -121
  7. package/cli/src/manifest.cj +632 -0
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -754
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +46 -38
  12. package/cli/src/sync.cj +526 -0
  13. package/cli/src/util.cj +9 -0
  14. package/cli/templates/app/README.md +30 -59
  15. package/cli/templates/app/{ui → assets/ui}/index.html +2 -0
  16. package/cli/templates/app/cj-tauri.toml +32 -0
  17. package/cli/templates/app/cjpm.toml +21 -4
  18. package/cli/templates/app/src/main.cj +29 -16
  19. package/cli/templates/app-react/README.md +34 -45
  20. package/cli/templates/app-react/{ui → assets/ui}/index.html +25 -25
  21. package/cli/templates/app-react/{ui → assets/ui}/src/App.css +67 -67
  22. package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +124 -124
  23. package/cli/templates/app-react/cj-tauri.toml +33 -0
  24. package/cli/templates/app-react/cjpm.toml +21 -4
  25. package/cli/templates/app-react/src/main.cj +18 -29
  26. package/cli/templates/app-vue/README.md +34 -38
  27. package/cli/templates/app-vue/{ui → assets/ui}/index.html +25 -25
  28. package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +176 -176
  29. package/cli/templates/app-vue/cj-tauri.toml +33 -0
  30. package/cli/templates/app-vue/cjpm.toml +21 -4
  31. package/cli/templates/app-vue/src/main.cj +18 -29
  32. package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +5 -0
  33. package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +15 -0
  34. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +8 -0
  35. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  36. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -0
  37. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +8 -0
  38. package/native/build_win.bat +62 -62
  39. package/native/core/bridge_core.c +1681 -1626
  40. package/native/core/bridge_core.h +675 -618
  41. package/native/core/bridge_js.h +108 -108
  42. package/native/platforms/harmonyos/README.md +115 -6
  43. package/native/platforms/linux/bridge_linux.c +2787 -2485
  44. package/native/platforms/windows/bridge_win.c +3740 -3248
  45. package/native/tests/gtk_threading_probe.c +143 -143
  46. package/native/tests/test_bridge_core.c +1339 -1228
  47. package/package.json +53 -51
  48. package/prebuilt/linux-x64/cj-tauri +0 -0
  49. package/src/api_system.cj +67 -67
  50. package/src/app.cj +325 -940
  51. package/src/app_events.cj +166 -0
  52. package/src/app_host_ops.cj +287 -0
  53. package/src/app_run.cj +283 -0
  54. package/src/app_spec.cj +76 -0
  55. package/src/arkweb_menu.cj +216 -0
  56. package/src/arkweb_script.cj +0 -12
  57. package/src/capability.cj +409 -409
  58. package/src/clipboard_bridge.cj +178 -0
  59. package/src/dialog_spec.cj +79 -0
  60. package/src/dragdrop.cj +26 -26
  61. package/src/embedded_assets.cj +98 -98
  62. package/src/host.cj +752 -604
  63. package/src/host_arkweb.cj +201 -17
  64. package/src/host_webkit.cj +663 -548
  65. package/src/host_webview2.cj +663 -548
  66. package/src/hotkey.cj +66 -66
  67. package/src/ipc_hub.cj +337 -337
  68. package/src/ipc_message.cj +286 -286
  69. package/src/launch.cj +62 -0
  70. package/src/logging.cj +127 -127
  71. package/src/menu.cj +197 -197
  72. package/src/platform/platform_app_context.cj +77 -0
  73. package/src/platform/platform_assets.cj +33 -0
  74. package/src/platform/platform_discover.cj +176 -0
  75. package/src/platform/platform_env.cj +29 -0
  76. package/src/platform/platform_feature.cj +31 -0
  77. package/src/platform/platform_page_source.cj +87 -0
  78. package/src/platform/platform_target_info.cj +32 -0
  79. package/src/platform_host_provider.cj +87 -0
  80. package/src/plugin.cj +333 -299
  81. package/src/plugins/clipboard/plugin_clipboard.cj +152 -144
  82. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  83. package/src/plugins/fs/plugin_fs.cj +105 -1
  84. package/src/plugins/http/http_backend.cj +131 -0
  85. package/src/plugins/http/http_desktop.cj +195 -0
  86. package/src/plugins/http/http_ohos.cj +204 -0
  87. package/src/plugins/http/plugin_http.cj +310 -459
  88. package/src/plugins/log/plugin_log.cj +105 -105
  89. package/src/plugins/menu/plugin_menu.cj +8 -0
  90. package/src/plugins/notification/plugin_notification.cj +154 -146
  91. package/src/plugins/serial/plugin_serial.cj +17 -1
  92. package/src/plugins/serial/serial_bridge.cj +9 -1
  93. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  94. package/src/plugins/serial/serial_ohos.cj +529 -0
  95. package/src/plugins/shell/plugin_shell.cj +29 -1
  96. package/src/plugins/sqlite/plugin_sqlite.cj +8 -0
  97. package/src/plugins/ws/plugin_ws.cj +488 -470
  98. package/src/process_hidden.cj +174 -174
  99. package/src/scope.cj +391 -391
  100. package/src/shell_event.cj +69 -69
  101. package/src/tests/app_spec_test.cj +126 -0
  102. package/src/tests/capability_test.cj +243 -243
  103. package/src/tests/dialog_spec_test.cj +73 -0
  104. package/src/tests/fake_host.cj +201 -186
  105. package/src/tests/ipc_hub_test.cj +487 -487
  106. package/src/tests/logging_test.cj +62 -62
  107. package/src/tests/platform_contract_test.cj +251 -0
  108. package/src/tests/plugin_clipboard_test.cj +222 -222
  109. package/src/tests/plugin_dialog_test.cj +237 -237
  110. package/src/tests/plugin_fs_test.cj +207 -0
  111. package/src/tests/plugin_http_test.cj +124 -124
  112. package/src/tests/plugin_notification_test.cj +205 -205
  113. package/src/tests/plugin_serial_test.cj +48 -0
  114. package/src/tests/plugin_test.cj +66 -3
  115. package/src/tests/plugin_ws_test.cj +40 -40
  116. package/src/tests/process_hidden_test.cj +129 -129
  117. package/src/tests/scope_test.cj +350 -350
  118. package/src/tests/system_host_test.cj +99 -81
  119. package/src/tests/urlutil_test.cj +53 -53
  120. package/src/tests/version_test.cj +75 -75
  121. package/src/tests/window_registry_test.cj +123 -123
  122. package/src/tray.cj +90 -90
  123. package/src/urlutil.cj +159 -159
  124. package/src/version.cj +44 -44
  125. package/src/window.cj +51 -51
  126. package/src/window_registry.cj +116 -116
  127. package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +0 -167
  128. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  129. package/src/host_factory.cj +0 -48
  130. /package/cli/templates/app/{capabilities → assets/capabilities}/default.json +0 -0
  131. /package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +0 -0
  132. /package/cli/templates/app-react/{ui → assets/ui}/package.json +0 -0
  133. /package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +0 -0
  134. /package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +0 -0
  135. /package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +0 -0
  136. /package/cli/templates/app-vue/{ui → assets/ui}/package.json +0 -0
  137. /package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +0 -0
  138. /package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +0 -0
  139. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +0 -0
  140. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +0 -0
  141. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
  142. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
  143. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +0 -0
  144. /package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +0 -0
  145. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +0 -0
  146. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +0 -0
  147. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +0 -0
  148. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +0 -0
  149. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +0 -0
  150. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +0 -0
  151. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +0 -0
  152. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +0 -0
  153. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
  154. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
  155. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +0 -0
  156. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
  157. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +0 -0
  158. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +0 -0
  159. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +0 -0
  160. /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +0 -0
  161. /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +0 -0
  162. /package/cli/templates/{ohos-pc → ohos-skeleton}/oh-package.json5 +0 -0
@@ -1,205 +1,205 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import cjTauri.plugins.notification.*
5
- import std.collection.*
6
- import stdx.encoding.json.*
7
- import std.unittest.*
8
- import std.unittest.testmacro.*
9
-
10
- /*
11
- * notification 插件单元测试:命令面 / shim / 命名权限集闸门 / 参数校验 / 未装配宿主的可读错误
12
- * + 「宿主已装配」时的转交(FakeHost 记痕)。
13
- *
14
- * 真通知落在 C 桥(Linux 走 freedesktop Notifications,Windows 桩阶段未实现),
15
- * 归实机验证;这里只锁入口逻辑。body 可省是契约的一部分,单独覆盖。
16
- */
17
-
18
- /** 取一条「已装配宿主」的命令处理器 */
19
- func notifCmdWithHost(host: FakeHost): CommandHandler {
20
- let c = NotificationCommands()
21
- c.attachHost(host)
22
- return c
23
- }
24
-
25
- func notifErrOf(handler: CommandHandler, cmd: String, args: JsonObject): String {
26
- try {
27
- handler.handle(cmd, args, IpcContext("main", IpcHub()))
28
- return ""
29
- } catch (e: CommandException) {
30
- return e.message
31
- }
32
- }
33
-
34
- /** 造 { title, body } */
35
- func notifArgs(title: String, body: JsonValue): JsonObject {
36
- var o = JsonObject()
37
- o.put("title", JsonString(title))
38
- o.put("body", body)
39
- return o
40
- }
41
-
42
- @Test
43
- func testNotificationPluginSurface(): Unit {
44
- let p = NotificationPlugin()
45
- @Expect(p.name(), "notification")
46
- let names = pluginCommandNames(p)
47
- @Expect(names.size, 1)
48
- @Expect(names[0], "notification:notify")
49
- }
50
-
51
- @Test
52
- func testNotificationPluginShimTargetsItsOwnNamespace(): Unit {
53
- let js = NotificationPlugin().jsShim()
54
- @Expect(js.contains("t.notification"), true)
55
- @Expect(js.contains("'notification:notify'"), true)
56
- }
57
-
58
- @Test
59
- func testNotificationPluginDeclaresPermissionSets(): Unit {
60
- let sets = pluginPermissionSets(NotificationPlugin())
61
- @Expect(sets.size, 1)
62
- let d = sets.get("notification:default").getOrThrow()
63
- @Expect(d.size, 1)
64
- @Expect(d[0], "notification:notify")
65
- }
66
-
67
- @Test
68
- func testNotificationPermissionSetGatesCommand(): Unit {
69
- let out = ArrayList<String>()
70
- let reg = CapabilityRegistry()
71
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"n\",\"permissions\":[\"notification:default\"]}"))
72
- let hub = hubWithRegistry(reg, out)
73
- registerPluginCommands(hub, NotificationPlugin())
74
- registerPluginPermissionSets(reg, NotificationPlugin())
75
-
76
- // 集内命令放行:走到「宿主未装配」的业务错误,而不是「未授权」
77
- hub.handleRawMessage(
78
- "{\"type\":\"invoke\",\"id\":51,\"cmd\":\"notification:notify\",\"args\":{\"title\":\"t\"}}")
79
- waitSink(out, 1)
80
- @Expect(out.size, 1)
81
- @Expect(strField(out[0], "error").contains("command not allowed"), false)
82
- }
83
-
84
- @Test
85
- func testNotificationWithoutPermissionIsDenied(): Unit {
86
- let out = ArrayList<String>()
87
- let reg = CapabilityRegistry()
88
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"empty\",\"permissions\":[]}"))
89
- let hub = hubWithRegistry(reg, out)
90
- registerPluginCommands(hub, NotificationPlugin())
91
- registerPluginPermissionSets(reg, NotificationPlugin())
92
-
93
- hub.handleRawMessage(
94
- "{\"type\":\"invoke\",\"id\":52,\"cmd\":\"notification:notify\",\"args\":{\"title\":\"t\"}}")
95
- waitSink(out, 1)
96
- @Expect(strField(out[0], "error"), "command not allowed: notification:notify")
97
- }
98
-
99
- @Test
100
- func testNotificationRequiresTitle(): Unit {
101
- let cmd = NotificationPlugin().commands().get("notify").getOrThrow()
102
- let ctx = IpcContext("main", IpcHub())
103
- var threw = false
104
- try {
105
- cmd.handle("notification:notify", JsonObject(), ctx)
106
- } catch (e: CommandException) {
107
- threw = true
108
- @Expect(e.message.contains("missing argument \"title\""), true)
109
- }
110
- @Expect(threw, true)
111
- }
112
-
113
- @Test
114
- func testNotificationRejectsNonStringTitle(): Unit {
115
- var args = JsonObject()
116
- args.put("title", JsonBool(true))
117
- let cmd = NotificationPlugin().commands().get("notify").getOrThrow()
118
- var threw = false
119
- try {
120
- cmd.handle("notification:notify", args, IpcContext("main", IpcHub()))
121
- } catch (e: CommandException) {
122
- threw = true
123
- @Expect(e.message.contains("must be a string"), true)
124
- }
125
- @Expect(threw, true)
126
- }
127
-
128
- @Test
129
- func testNotificationWithoutHostIsReadableError(): Unit {
130
- @Expect(notifErrOf(NotificationPlugin().commands().get("notify").getOrThrow(), "notification:notify",
131
- notifArgs("hi", JsonString("body"))).contains("宿主未装配"), true)
132
- }
133
-
134
- @Test
135
- func testNotificationRejectsUnknownCommand(): Unit {
136
- @Expect(notifErrOf(NotificationPlugin().commands().get("notify").getOrThrow(), "notification:nope",
137
- JsonObject()).contains("unknown notification command"), true)
138
- }
139
-
140
- @Test
141
- func testNotificationBodyIsOptional(): Unit {
142
- let host = FakeHost("main")
143
- let cmd = notifCmdWithHost(host)
144
-
145
- // 只给 title:body 按空串
146
- var onlyTitle = JsonObject()
147
- onlyTitle.put("title", JsonString("标题"))
148
- let r1 = cmd.handle("notification:notify", onlyTitle, IpcContext("main", IpcHub()))
149
- match (r1.kind()) {
150
- case JsonKind.JsNull => ()
151
- case _ => @Expect(false, true)
152
- }
153
- @Expect(host.notifiedArgs, "标题|")
154
-
155
- // body 显式 null:同样按空串(前端 o.body 传 undefined 时序列化可能落成 null)
156
- cmd.handle("notification:notify", notifArgs("标题2", JsonNull()), IpcContext("main", IpcHub()))
157
- @Expect(host.notifiedArgs, "标题2|")
158
-
159
- // body 有值:原样转交
160
- cmd.handle("notification:notify", notifArgs("标题3", JsonString("正文")), IpcContext("main", IpcHub()))
161
- @Expect(host.notifiedArgs, "标题3|正文")
162
- }
163
-
164
- /** 桩侧「label → 宿主」解析器见 plugin_dialog_test.cj 的 routeProbeHost(同包可见) */
165
- @Test
166
- func testNotificationRoutesToTheWindowThatAsked(): Unit {
167
- // 与 dialog / clipboard 同一条契约:报文里的 window 决定交给哪个宿主
168
- let mainHost = FakeHost("main")
169
- let secondHost = FakeHost("second")
170
- let cmds = NotificationCommands()
171
- cmds.attachHost(mainHost)
172
- cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
173
-
174
- cmds.handle("notification:notify", notifArgs("second 的通知", JsonString("正文")),
175
- IpcContext("second", IpcHub()))
176
- @Expect(secondHost.notifiedArgs, "second 的通知|正文")
177
- // 关键断言:另一扇窗的宿主一点都没被碰到
178
- @Expect(mainHost.notifiedArgs, "")
179
-
180
- // 反向也成立
181
- cmds.handle("notification:notify", notifArgs("main 的通知", JsonNull()), IpcContext("main", IpcHub()))
182
- @Expect(mainHost.notifiedArgs, "main 的通知|")
183
- @Expect(secondHost.notifiedArgs, "second 的通知|正文")
184
- }
185
-
186
- @Test
187
- func testNotificationReportsUnknownWindowInsteadOfFallingBack(): Unit {
188
- let mainHost = FakeHost("main")
189
- let secondHost = FakeHost("second")
190
- let cmds = NotificationCommands()
191
- cmds.attachHost(mainHost)
192
- cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
193
-
194
- var threw = false
195
- try {
196
- cmds.handle("notification:notify", notifArgs("谁的", JsonNull()), IpcContext("ghost", IpcHub()))
197
- } catch (e: CommandException) {
198
- threw = true
199
- @Expect(e.message.contains("ghost"), true)
200
- }
201
- @Expect(threw, true)
202
- // 报错之后两个宿主都没被写过:不回落单槽
203
- @Expect(mainHost.notifiedArgs, "")
204
- @Expect(secondHost.notifiedArgs, "")
205
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import cjTauri.plugins.notification.*
5
+ import std.collection.*
6
+ import stdx.encoding.json.*
7
+ import std.unittest.*
8
+ import std.unittest.testmacro.*
9
+
10
+ /*
11
+ * notification 插件单元测试:命令面 / shim / 命名权限集闸门 / 参数校验 / 未装配宿主的可读错误
12
+ * + 「宿主已装配」时的转交(FakeHost 记痕)。
13
+ *
14
+ * 真通知落在 C 桥(Linux 走 freedesktop Notifications,Windows 桩阶段未实现),
15
+ * 归实机验证;这里只锁入口逻辑。body 可省是契约的一部分,单独覆盖。
16
+ */
17
+
18
+ /** 取一条「已装配宿主」的命令处理器 */
19
+ func notifCmdWithHost(host: FakeHost): CommandHandler {
20
+ let c = NotificationCommands()
21
+ c.attachHost(host)
22
+ return c
23
+ }
24
+
25
+ func notifErrOf(handler: CommandHandler, cmd: String, args: JsonObject): String {
26
+ try {
27
+ handler.handle(cmd, args, IpcContext("main", IpcHub()))
28
+ return ""
29
+ } catch (e: CommandException) {
30
+ return e.message
31
+ }
32
+ }
33
+
34
+ /** 造 { title, body } */
35
+ func notifArgs(title: String, body: JsonValue): JsonObject {
36
+ var o = JsonObject()
37
+ o.put("title", JsonString(title))
38
+ o.put("body", body)
39
+ return o
40
+ }
41
+
42
+ @Test
43
+ func testNotificationPluginSurface(): Unit {
44
+ let p = NotificationPlugin()
45
+ @Expect(p.name(), "notification")
46
+ let names = pluginCommandNames(p)
47
+ @Expect(names.size, 1)
48
+ @Expect(names[0], "notification:notify")
49
+ }
50
+
51
+ @Test
52
+ func testNotificationPluginShimTargetsItsOwnNamespace(): Unit {
53
+ let js = NotificationPlugin().jsShim()
54
+ @Expect(js.contains("t.notification"), true)
55
+ @Expect(js.contains("'notification:notify'"), true)
56
+ }
57
+
58
+ @Test
59
+ func testNotificationPluginDeclaresPermissionSets(): Unit {
60
+ let sets = pluginPermissionSets(NotificationPlugin())
61
+ @Expect(sets.size, 1)
62
+ let d = sets.get("notification:default").getOrThrow()
63
+ @Expect(d.size, 1)
64
+ @Expect(d[0], "notification:notify")
65
+ }
66
+
67
+ @Test
68
+ func testNotificationPermissionSetGatesCommand(): Unit {
69
+ let out = ArrayList<String>()
70
+ let reg = CapabilityRegistry()
71
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"n\",\"permissions\":[\"notification:default\"]}"))
72
+ let hub = hubWithRegistry(reg, out)
73
+ registerPluginCommands(hub, NotificationPlugin())
74
+ registerPluginPermissionSets(reg, NotificationPlugin())
75
+
76
+ // 集内命令放行:走到「宿主未装配」的业务错误,而不是「未授权」
77
+ hub.handleRawMessage(
78
+ "{\"type\":\"invoke\",\"id\":51,\"cmd\":\"notification:notify\",\"args\":{\"title\":\"t\"}}")
79
+ waitSink(out, 1)
80
+ @Expect(out.size, 1)
81
+ @Expect(strField(out[0], "error").contains("command not allowed"), false)
82
+ }
83
+
84
+ @Test
85
+ func testNotificationWithoutPermissionIsDenied(): Unit {
86
+ let out = ArrayList<String>()
87
+ let reg = CapabilityRegistry()
88
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"empty\",\"permissions\":[]}"))
89
+ let hub = hubWithRegistry(reg, out)
90
+ registerPluginCommands(hub, NotificationPlugin())
91
+ registerPluginPermissionSets(reg, NotificationPlugin())
92
+
93
+ hub.handleRawMessage(
94
+ "{\"type\":\"invoke\",\"id\":52,\"cmd\":\"notification:notify\",\"args\":{\"title\":\"t\"}}")
95
+ waitSink(out, 1)
96
+ @Expect(strField(out[0], "error"), "command not allowed: notification:notify")
97
+ }
98
+
99
+ @Test
100
+ func testNotificationRequiresTitle(): Unit {
101
+ let cmd = NotificationPlugin().commands().get("notify").getOrThrow()
102
+ let ctx = IpcContext("main", IpcHub())
103
+ var threw = false
104
+ try {
105
+ cmd.handle("notification:notify", JsonObject(), ctx)
106
+ } catch (e: CommandException) {
107
+ threw = true
108
+ @Expect(e.message.contains("missing argument \"title\""), true)
109
+ }
110
+ @Expect(threw, true)
111
+ }
112
+
113
+ @Test
114
+ func testNotificationRejectsNonStringTitle(): Unit {
115
+ var args = JsonObject()
116
+ args.put("title", JsonBool(true))
117
+ let cmd = NotificationPlugin().commands().get("notify").getOrThrow()
118
+ var threw = false
119
+ try {
120
+ cmd.handle("notification:notify", args, IpcContext("main", IpcHub()))
121
+ } catch (e: CommandException) {
122
+ threw = true
123
+ @Expect(e.message.contains("must be a string"), true)
124
+ }
125
+ @Expect(threw, true)
126
+ }
127
+
128
+ @Test
129
+ func testNotificationWithoutHostIsReadableError(): Unit {
130
+ @Expect(notifErrOf(NotificationPlugin().commands().get("notify").getOrThrow(), "notification:notify",
131
+ notifArgs("hi", JsonString("body"))).contains("宿主未装配"), true)
132
+ }
133
+
134
+ @Test
135
+ func testNotificationRejectsUnknownCommand(): Unit {
136
+ @Expect(notifErrOf(NotificationPlugin().commands().get("notify").getOrThrow(), "notification:nope",
137
+ JsonObject()).contains("unknown notification command"), true)
138
+ }
139
+
140
+ @Test
141
+ func testNotificationBodyIsOptional(): Unit {
142
+ let host = FakeHost("main")
143
+ let cmd = notifCmdWithHost(host)
144
+
145
+ // 只给 title:body 按空串
146
+ var onlyTitle = JsonObject()
147
+ onlyTitle.put("title", JsonString("标题"))
148
+ let r1 = cmd.handle("notification:notify", onlyTitle, IpcContext("main", IpcHub()))
149
+ match (r1.kind()) {
150
+ case JsonKind.JsNull => ()
151
+ case _ => @Expect(false, true)
152
+ }
153
+ @Expect(host.notifiedArgs, "标题|")
154
+
155
+ // body 显式 null:同样按空串(前端 o.body 传 undefined 时序列化可能落成 null)
156
+ cmd.handle("notification:notify", notifArgs("标题2", JsonNull()), IpcContext("main", IpcHub()))
157
+ @Expect(host.notifiedArgs, "标题2|")
158
+
159
+ // body 有值:原样转交
160
+ cmd.handle("notification:notify", notifArgs("标题3", JsonString("正文")), IpcContext("main", IpcHub()))
161
+ @Expect(host.notifiedArgs, "标题3|正文")
162
+ }
163
+
164
+ /** 桩侧「label → 宿主」解析器见 plugin_dialog_test.cj 的 routeProbeHost(同包可见) */
165
+ @Test
166
+ func testNotificationRoutesToTheWindowThatAsked(): Unit {
167
+ // 与 dialog / clipboard 同一条契约:报文里的 window 决定交给哪个宿主
168
+ let mainHost = FakeHost("main")
169
+ let secondHost = FakeHost("second")
170
+ let cmds = NotificationCommands()
171
+ cmds.attachHost(mainHost)
172
+ cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
173
+
174
+ cmds.handle("notification:notify", notifArgs("second 的通知", JsonString("正文")),
175
+ IpcContext("second", IpcHub()))
176
+ @Expect(secondHost.notifiedArgs, "second 的通知|正文")
177
+ // 关键断言:另一扇窗的宿主一点都没被碰到
178
+ @Expect(mainHost.notifiedArgs, "")
179
+
180
+ // 反向也成立
181
+ cmds.handle("notification:notify", notifArgs("main 的通知", JsonNull()), IpcContext("main", IpcHub()))
182
+ @Expect(mainHost.notifiedArgs, "main 的通知|")
183
+ @Expect(secondHost.notifiedArgs, "second 的通知|正文")
184
+ }
185
+
186
+ @Test
187
+ func testNotificationReportsUnknownWindowInsteadOfFallingBack(): Unit {
188
+ let mainHost = FakeHost("main")
189
+ let secondHost = FakeHost("second")
190
+ let cmds = NotificationCommands()
191
+ cmds.attachHost(mainHost)
192
+ cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
193
+
194
+ var threw = false
195
+ try {
196
+ cmds.handle("notification:notify", notifArgs("谁的", JsonNull()), IpcContext("ghost", IpcHub()))
197
+ } catch (e: CommandException) {
198
+ threw = true
199
+ @Expect(e.message.contains("ghost"), true)
200
+ }
201
+ @Expect(threw, true)
202
+ // 报错之后两个宿主都没被写过:不回落单槽
203
+ @Expect(mainHost.notifiedArgs, "")
204
+ @Expect(secondHost.notifiedArgs, "")
205
+ }
@@ -267,3 +267,51 @@ func testSerialPortListIsSortedAndWhitelisted(): Unit {
267
267
  i++
268
268
  }
269
269
  }
270
+
271
+ /* ===== ohos 分支的可桌面验证面(serial_ohos.cj) =====
272
+ * 真正的 open/read/write 走 ohos 专属 FFI(@When 门禁),桌面上编译不到——那部分归真机取证
273
+ * (issue #5:USB 转串口接 OTG、DDK 权限)。下面锁的是 ohos 与桌面**共用同一份代码**的纯函数:
274
+ * 波特率 → termios 常量映射(不带门禁,专为可测升 public)与设备白名单的 /dev/bus 路径。
275
+ */
276
+
277
+ @Test
278
+ func testSerialOhosBaudConstMatchesDesktopTiers(): Unit {
279
+ // 与 SERIAL_BAUDS 同一套档位:桌面允许的每一档,ohos 侧都必须有对应的 termios 常量
280
+ let tiers: Array<Int32> = [300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
281
+ 230400, 460800, 500000, 921600, 1000000]
282
+ for (b in tiers) {
283
+ @Expect(serialOhosBaudConst(b).isSome(), true)
284
+ }
285
+ // 关键取值抽查(asm-generic:B38400 以下连续,往上 0x1000+n)
286
+ @Expect(serialOhosBaudConst(115200).getOrThrow(), 0x1002)
287
+ @Expect(serialOhosBaudConst(9600).getOrThrow(), 0xd)
288
+ @Expect(serialOhosBaudConst(1000000).getOrThrow(), 0x1008)
289
+ // 桌面已拒的档位,ohos 侧同样不收(两层拦同一套名单,不会出现桌面放行、ohos 拒)
290
+ @Expect(serialOhosBaudConst(14400).isNone(), true)
291
+ @Expect(serialOhosBaudConst(4000000).isNone(), true)
292
+ }
293
+
294
+ @Test
295
+ func testSerialPathLooksAllowedCoversOhosDevicePaths(): Unit {
296
+ // 鸿蒙特有的 USB 串口落点必须过白名单(ohos 侧 open 的边界就靠它)
297
+ @Expect(serialPathLooksAllowed("/dev/bus/ttyUSB0"), true)
298
+ @Expect(serialPathLooksAllowed("/dev/bus/ttyACM0"), true)
299
+ @Expect(serialPathLooksAllowed("/dev/bus/ttyS1"), true)
300
+ // 穿越与任意路径照旧拒
301
+ @Expect(serialPathLooksAllowed("/dev/bus/../ttyUSB0"), false)
302
+ @Expect(serialPathLooksAllowed("/data/local/ttyUSB0"), false)
303
+ }
304
+
305
+ @Test
306
+ func testSerialPluginDescribesAllThreeHosts(): Unit {
307
+ let d = SerialPlugin().describe()
308
+ let hosts = d.supportedHosts
309
+ @Expect(hosts.size, 3)
310
+ var hasArkweb = false
311
+ for (h in hosts) {
312
+ if (h == "arkweb") {
313
+ hasArkweb = true
314
+ }
315
+ }
316
+ @Expect(hasArkweb, true)
317
+ }
@@ -218,12 +218,16 @@ func testFsPluginSurface(): Unit {
218
218
  let p = FsPlugin()
219
219
  @Expect(p.name(), "fs")
220
220
  let names = pluginCommandNames(p)
221
- @Expect(names.size, 5)
221
+ @Expect(names.size, 9)
222
222
  var hasRead = false
223
223
  var hasWrite = false
224
224
  var hasExists = false
225
225
  var hasList = false
226
226
  var hasRemove = false
227
+ var hasMkdir = false
228
+ var hasRename = false
229
+ var hasCopy = false
230
+ var hasStat = false
227
231
  for (n in names) {
228
232
  if (n == "fs:readText") {
229
233
  hasRead = true
@@ -240,12 +244,28 @@ func testFsPluginSurface(): Unit {
240
244
  if (n == "fs:remove") {
241
245
  hasRemove = true
242
246
  }
247
+ if (n == "fs:mkdir") {
248
+ hasMkdir = true
249
+ }
250
+ if (n == "fs:rename") {
251
+ hasRename = true
252
+ }
253
+ if (n == "fs:copy") {
254
+ hasCopy = true
255
+ }
256
+ if (n == "fs:stat") {
257
+ hasStat = true
258
+ }
243
259
  }
244
260
  @Expect(hasRead, true)
245
261
  @Expect(hasWrite, true)
246
262
  @Expect(hasExists, true)
247
263
  @Expect(hasList, true) // 目录列举:笔记工具的文件树靠它,应用层不必自己拼 std.fs
248
264
  @Expect(hasRemove, true)
265
+ @Expect(hasMkdir, true)
266
+ @Expect(hasRename, true)
267
+ @Expect(hasCopy, true)
268
+ @Expect(hasStat, true)
249
269
  }
250
270
 
251
271
  @Test
@@ -262,7 +282,7 @@ func testFsPluginDeclaresPermissionSets(): Unit {
262
282
  let sets = pluginPermissionSets(FsPlugin())
263
283
  @Expect(sets.size, 2)
264
284
  let ro = sets.get("fs:readonly").getOrThrow()
265
- @Expect(ro.size, 3)
285
+ @Expect(ro.size, 4)
266
286
  var roHasWrite = false
267
287
  var roHasRemove = false
268
288
  for (m in ro) {
@@ -275,7 +295,7 @@ func testFsPluginDeclaresPermissionSets(): Unit {
275
295
  }
276
296
  @Expect(roHasWrite, false) // 只读集不含写:最小权限不打折
277
297
  @Expect(roHasRemove, false) // 删也同理——列目录是「读」,删除不是
278
- @Expect(sets.get("fs:default").getOrThrow().size, 5)
298
+ @Expect(sets.get("fs:default").getOrThrow().size, 9)
279
299
  }
280
300
 
281
301
  @Test
@@ -540,3 +560,46 @@ func testRenderPluginManifestEmptyList(): Unit {
540
560
  @Expect(renderPluginManifest(none, "text"), "(未装配插件)\n")
541
561
  @Expect(JsonValue.fromStr(renderPluginManifest(none, "json")).asArray().size(), 0)
542
562
  }
563
+
564
+ class HostedDemoPlugin <: Plugin {
565
+ public init() {}
566
+
567
+ public func name(): String {
568
+ return "demo2"
569
+ }
570
+
571
+ public func commands(): HashMap<String, CommandHandler> {
572
+ return HashMap<String, CommandHandler>()
573
+ }
574
+
575
+ public func describe(): PluginManifest {
576
+ let hosts = ArrayList<String>()
577
+ hosts.add("webkitgtk")
578
+ return PluginManifest(hosts, ArrayList<String>())
579
+ }
580
+ }
581
+
582
+ @Test
583
+ func testDescribeDefault(): Unit {
584
+ let d = DemoPlugin().describe()
585
+ // 默认实现:全宿主、无能力依赖(空数组 = 不限)
586
+ @Expect(d.supportedHosts.size, 0)
587
+ @Expect(d.requiredCapabilities.size, 0)
588
+ }
589
+
590
+ @Test
591
+ func testDescribeOverride(): Unit {
592
+ let d = HostedDemoPlugin().describe()
593
+ @Expect(d.supportedHosts.size, 1)
594
+ @Expect(d.supportedHosts.get(0), "webkitgtk")
595
+ }
596
+
597
+ @Test
598
+ func testManifestJsonCarriesDescribe(): Unit {
599
+ let plugins = ArrayList<Plugin>()
600
+ plugins.add(HostedDemoPlugin())
601
+ let arr = jsonItemsOf(JsonValue.fromStr(renderPluginManifest(plugins, "json")))
602
+ let obj = arr[0].asObject()
603
+ @Expect(joinWithPipe(jsonStringsOf(obj, "supportedHosts")), "webkitgtk")
604
+ @Expect(jsonStringsOf(obj, "requiredCapabilities").size, 0)
605
+ }
@@ -1,40 +1,40 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import cjTauri.plugins.ws.*
5
- import std.unittest.*
6
- import std.unittest.testmacro.*
7
-
8
- /*
9
- * ws 插件的策略层用例(帧 IO 归实机:真连接需要对端,单测不起网络)。
10
- * urlHostOf 的 ws/wss 用例也放这里(核心共享层函数,随 ws 插件落地一起验)。
11
- */
12
-
13
- @Test
14
- func testUrlHostOfAcceptsWsSchemes(): Unit {
15
- // cjh web 的默认端点形状:ws://127.0.0.1:8765/ws
16
- @Expect(urlHostOf("ws://127.0.0.1:8765/ws"), "127.0.0.1")
17
- @Expect(urlHostOf("wss://Example.COM/ws"), "example.com")
18
- }
19
-
20
- @Test
21
- func testWsPluginDeclarations(): Unit {
22
- let p = WsPlugin()
23
- @Expect(p.name(), "ws")
24
- @Expect(p.scopeAware(), true)
25
- // 权限集 default = connect + send + close:集声明 != 放行,清单引用才生效
26
- @Expect(p.permissions().contains("default"), true)
27
- let cmds = p.commands()
28
- @Expect(cmds.contains("connect"), true)
29
- @Expect(cmds.contains("send"), true)
30
- @Expect(cmds.contains("close"), true)
31
- // 推两个事件(frame / state),无前端 shim
32
- @Expect(p.events().size == 2, true)
33
- @Expect(p.jsShim().size == 0, true)
34
- }
35
-
36
- @Test
37
- func testWsStateConstants(): Unit {
38
- @Expect(CJ_WS_STATE_CONNECTED, "connected")
39
- @Expect(CJ_WS_STATE_CLOSED, "closed")
40
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import cjTauri.plugins.ws.*
5
+ import std.unittest.*
6
+ import std.unittest.testmacro.*
7
+
8
+ /*
9
+ * ws 插件的策略层用例(帧 IO 归实机:真连接需要对端,单测不起网络)。
10
+ * urlHostOf 的 ws/wss 用例也放这里(核心共享层函数,随 ws 插件落地一起验)。
11
+ */
12
+
13
+ @Test
14
+ func testUrlHostOfAcceptsWsSchemes(): Unit {
15
+ // cjh web 的默认端点形状:ws://127.0.0.1:8765/ws
16
+ @Expect(urlHostOf("ws://127.0.0.1:8765/ws"), "127.0.0.1")
17
+ @Expect(urlHostOf("wss://Example.COM/ws"), "example.com")
18
+ }
19
+
20
+ @Test
21
+ func testWsPluginDeclarations(): Unit {
22
+ let p = WsPlugin()
23
+ @Expect(p.name(), "ws")
24
+ @Expect(p.scopeAware(), true)
25
+ // 权限集 default = connect + send + close:集声明 != 放行,清单引用才生效
26
+ @Expect(p.permissions().contains("default"), true)
27
+ let cmds = p.commands()
28
+ @Expect(cmds.contains("connect"), true)
29
+ @Expect(cmds.contains("send"), true)
30
+ @Expect(cmds.contains("close"), true)
31
+ // 推两个事件(frame / state),无前端 shim
32
+ @Expect(p.events().size == 2, true)
33
+ @Expect(p.jsShim().size == 0, true)
34
+ }
35
+
36
+ @Test
37
+ func testWsStateConstants(): Unit {
38
+ @Expect(CJ_WS_STATE_CONNECTED, "connected")
39
+ @Expect(CJ_WS_STATE_CLOSED, "closed")
40
+ }