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,222 +1,222 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import cjTauri.plugins.clipboard.*
5
- import std.collection.*
6
- import stdx.encoding.json.*
7
- import std.unittest.*
8
- import std.unittest.testmacro.*
9
-
10
- /*
11
- * clipboard 插件单元测试:命令面 / shim / 命名权限集闸门 / 参数校验 / 未装配宿主的可读错误
12
- * + 「宿主已装配」时的读写转交(用 FakeHost 替身记痕)。
13
- *
14
- * 真剪贴板读写落在 C 桥(GTK / Win32 的剪贴板调用必须在宿主 UI 线程),归实机验证
15
- * (examples/plugin-clipboard);这里只锁「不碰剪贴板就能判定的入口逻辑」。
16
- */
17
-
18
- /** 造 { text } 参数 */
19
- func clipTextArgs(text: String): JsonObject {
20
- var o = JsonObject()
21
- o.put("text", JsonString(text))
22
- return o
23
- }
24
-
25
- /** 取一条「已装配宿主」的命令处理器:剪贴板命令要先经 attachHost 才有宿主可用 */
26
- func clipCmdWithHost(short: String, host: FakeHost): CommandHandler {
27
- let _ = short
28
- let c = ClipboardCommands()
29
- c.attachHost(host)
30
- return c
31
- }
32
-
33
- /** 直接调命令,把 CommandException 的原文取出来(没抛就返回空串)*/
34
- func clipErrOf(handler: CommandHandler, cmd: String, args: JsonObject): String {
35
- try {
36
- handler.handle(cmd, args, IpcContext("main", IpcHub()))
37
- return ""
38
- } catch (e: CommandException) {
39
- return e.message
40
- }
41
- }
42
-
43
- @Test
44
- func testClipboardPluginSurface(): Unit {
45
- let p = ClipboardPlugin()
46
- @Expect(p.name(), "clipboard")
47
- let names = pluginCommandNames(p)
48
- @Expect(names.size, 2)
49
- var hasRead = false
50
- var hasWrite = false
51
- for (n in names) {
52
- if (n == "clipboard:read") {
53
- hasRead = true
54
- }
55
- if (n == "clipboard:write") {
56
- hasWrite = true
57
- }
58
- }
59
- @Expect(hasRead, true)
60
- @Expect(hasWrite, true)
61
- }
62
-
63
- @Test
64
- func testClipboardPluginShimTargetsItsOwnNamespace(): Unit {
65
- let js = ClipboardPlugin().jsShim()
66
- @Expect(js.contains("t.clipboard"), true)
67
- @Expect(js.contains("'clipboard:write'"), true)
68
- @Expect(js.contains("'clipboard:read'"), true)
69
- }
70
-
71
- @Test
72
- func testClipboardPluginDeclaresPermissionSets(): Unit {
73
- let sets = pluginPermissionSets(ClipboardPlugin())
74
- @Expect(sets.size, 2)
75
- // readonly 只放行读:剪贴板里可能有用户敏感内容,「只写不读」是合理的最小授权面
76
- let ro = sets.get("clipboard:readonly").getOrThrow()
77
- @Expect(ro.size, 1)
78
- @Expect(ro[0], "clipboard:read")
79
- @Expect(sets.get("clipboard:default").getOrThrow().size, 2)
80
- }
81
-
82
- @Test
83
- func testClipboardPermissionSetGatesCommands(): Unit {
84
- let out = ArrayList<String>()
85
- let reg = CapabilityRegistry()
86
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"c\",\"permissions\":[\"clipboard:readonly\"]}"))
87
- let hub = hubWithRegistry(reg, out)
88
- registerPluginCommands(hub, ClipboardPlugin())
89
- registerPluginPermissionSets(reg, ClipboardPlugin())
90
-
91
- // 集内命令放行:这条会走到「宿主未装配」的业务错误,而不是「未授权」
92
- hub.handleRawMessage("{\"type\":\"invoke\",\"id\":41,\"cmd\":\"clipboard:read\"}")
93
- waitSink(out, 1)
94
- @Expect(out.size, 1)
95
- @Expect(strField(out[0], "error").contains("command not allowed"), false)
96
-
97
- // 集外命令仍拒:readonly 不夹带写能力
98
- hub.handleRawMessage("{\"type\":\"invoke\",\"id\":42,\"cmd\":\"clipboard:write\",\"args\":{\"text\":\"x\"}}")
99
- @Expect(out.size, 2)
100
- @Expect(strField(out[1], "error"), "command not allowed: clipboard:write")
101
- }
102
-
103
- @Test
104
- func testClipboardWriteRequiresTextArgument(): Unit {
105
- let cmd = ClipboardPlugin().commands().get("write").getOrThrow()
106
- let ctx = IpcContext("main", IpcHub())
107
- var threw = false
108
- try {
109
- cmd.handle("clipboard:write", JsonObject(), ctx)
110
- } catch (e: CommandException) {
111
- threw = true
112
- @Expect(e.message.contains("missing argument \"text\""), true)
113
- }
114
- @Expect(threw, true)
115
- }
116
-
117
- @Test
118
- func testClipboardWriteRejectsNonString(): Unit {
119
- var args = JsonObject()
120
- args.put("text", JsonInt(7))
121
- let cmd = ClipboardPlugin().commands().get("write").getOrThrow()
122
- let ctx = IpcContext("main", IpcHub())
123
- var threw = false
124
- try {
125
- cmd.handle("clipboard:write", args, ctx)
126
- } catch (e: CommandException) {
127
- threw = true
128
- @Expect(e.message.contains("must be a string"), true)
129
- }
130
- @Expect(threw, true)
131
- }
132
-
133
- @Test
134
- func testClipboardWithoutHostIsReadableError(): Unit {
135
- // 没经 .plugin() 装配(没有宿主)时调用:给的是可读错误,而不是空指针崩掉
136
- @Expect(clipErrOf(ClipboardPlugin().commands().get("read").getOrThrow(), "clipboard:read", JsonObject())
137
- .contains("宿主未装配"), true)
138
- @Expect(clipErrOf(ClipboardPlugin().commands().get("write").getOrThrow(), "clipboard:write",
139
- clipTextArgs("hi")).contains("宿主未装配"), true)
140
- }
141
-
142
- @Test
143
- func testClipboardRejectsUnknownCommand(): Unit {
144
- @Expect(clipErrOf(ClipboardPlugin().commands().get("read").getOrThrow(), "clipboard:nope", JsonObject())
145
- .contains("unknown clipboard command"), true)
146
- }
147
-
148
- @Test
149
- func testClipboardWriteHandsTextToHost(): Unit {
150
- let host = FakeHost("main")
151
- let cmd = clipCmdWithHost("write", host)
152
- let r = cmd.handle("clipboard:write", clipTextArgs("hello 剪贴板"), IpcContext("main", IpcHub()))
153
- // 成功回 JsonNull(写命令没有返回值,与 Tauri 的 writeText 对齐)
154
- match (r.kind()) {
155
- case JsonKind.JsNull => ()
156
- case _ =>
157
- @Expect(false, true) // 成功路径不该回别的类型
158
- }
159
- @Expect(host.clipboardWritten, "hello 剪贴板")
160
- }
161
-
162
- @Test
163
- func testClipboardReadReturnsHostValue(): Unit {
164
- let host = FakeHost("main")
165
- let cmd = clipCmdWithHost("read", host)
166
-
167
- // 空 / 非文本 / 平台失败都归 None,转成 JsonNull——这是合法结果,不是错误
168
- let empty = cmd.handle("clipboard:read", JsonObject(), IpcContext("main", IpcHub()))
169
- match (empty.kind()) {
170
- case JsonKind.JsNull => ()
171
- case _ => @Expect(false, true)
172
- }
173
-
174
- // 有内容:原样转成 JsonString 回给前端
175
- host.clipboardReadValue = Some("pasted 文本")
176
- let got = cmd.handle("clipboard:read", JsonObject(), IpcContext("main", IpcHub()))
177
- @Expect(got.asString().getValue(), "pasted 文本")
178
- }
179
-
180
- /** 桩侧「label → 宿主」解析器见 plugin_dialog_test.cj 的 routeProbeHost(同包可见) */
181
- @Test
182
- func testClipboardRoutesToTheWindowThatAsked(): Unit {
183
- // 与 dialog 同一条契约:报文里的 window 决定交给哪个宿主。
184
- // 只装单槽时第二扇窗的读写会落到默认窗那个宿主上(多窗口下的串台来源)。
185
- let mainHost = FakeHost("main")
186
- let secondHost = FakeHost("second")
187
- let cmds = ClipboardCommands()
188
- cmds.attachHost(mainHost)
189
- cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
190
-
191
- cmds.handle("clipboard:write", clipTextArgs("second 的剪贴板"), IpcContext("second", IpcHub()))
192
- @Expect(secondHost.clipboardWritten, "second 的剪贴板")
193
- // 关键断言:另一扇窗的宿主一点都没被碰到
194
- @Expect(mainHost.clipboardWritten, "")
195
-
196
- // 读同样按窗口解析
197
- secondHost.clipboardReadValue = Some("from second")
198
- let got = cmds.handle("clipboard:read", JsonObject(), IpcContext("second", IpcHub()))
199
- @Expect(got.asString().getValue(), "from second")
200
- @Expect(mainHost.clipboardReadValue.isNone(), true)
201
- }
202
-
203
- @Test
204
- func testClipboardReportsUnknownWindowInsteadOfFallingBack(): Unit {
205
- let mainHost = FakeHost("main")
206
- let secondHost = FakeHost("second")
207
- let cmds = ClipboardCommands()
208
- cmds.attachHost(mainHost)
209
- cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
210
-
211
- var threw = false
212
- try {
213
- cmds.handle("clipboard:write", clipTextArgs("x"), IpcContext("ghost", IpcHub()))
214
- } catch (e: CommandException) {
215
- threw = true
216
- @Expect(e.message.contains("ghost"), true)
217
- }
218
- @Expect(threw, true)
219
- // 报错之后两个宿主都没被写过:不回落单槽,宁可不写
220
- @Expect(mainHost.clipboardWritten, "")
221
- @Expect(secondHost.clipboardWritten, "")
222
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import cjTauri.plugins.clipboard.*
5
+ import std.collection.*
6
+ import stdx.encoding.json.*
7
+ import std.unittest.*
8
+ import std.unittest.testmacro.*
9
+
10
+ /*
11
+ * clipboard 插件单元测试:命令面 / shim / 命名权限集闸门 / 参数校验 / 未装配宿主的可读错误
12
+ * + 「宿主已装配」时的读写转交(用 FakeHost 替身记痕)。
13
+ *
14
+ * 真剪贴板读写落在 C 桥(GTK / Win32 的剪贴板调用必须在宿主 UI 线程),归实机验证
15
+ * (examples/plugin-clipboard);这里只锁「不碰剪贴板就能判定的入口逻辑」。
16
+ */
17
+
18
+ /** 造 { text } 参数 */
19
+ func clipTextArgs(text: String): JsonObject {
20
+ var o = JsonObject()
21
+ o.put("text", JsonString(text))
22
+ return o
23
+ }
24
+
25
+ /** 取一条「已装配宿主」的命令处理器:剪贴板命令要先经 attachHost 才有宿主可用 */
26
+ func clipCmdWithHost(short: String, host: FakeHost): CommandHandler {
27
+ let _ = short
28
+ let c = ClipboardCommands()
29
+ c.attachHost(host)
30
+ return c
31
+ }
32
+
33
+ /** 直接调命令,把 CommandException 的原文取出来(没抛就返回空串)*/
34
+ func clipErrOf(handler: CommandHandler, cmd: String, args: JsonObject): String {
35
+ try {
36
+ handler.handle(cmd, args, IpcContext("main", IpcHub()))
37
+ return ""
38
+ } catch (e: CommandException) {
39
+ return e.message
40
+ }
41
+ }
42
+
43
+ @Test
44
+ func testClipboardPluginSurface(): Unit {
45
+ let p = ClipboardPlugin()
46
+ @Expect(p.name(), "clipboard")
47
+ let names = pluginCommandNames(p)
48
+ @Expect(names.size, 2)
49
+ var hasRead = false
50
+ var hasWrite = false
51
+ for (n in names) {
52
+ if (n == "clipboard:read") {
53
+ hasRead = true
54
+ }
55
+ if (n == "clipboard:write") {
56
+ hasWrite = true
57
+ }
58
+ }
59
+ @Expect(hasRead, true)
60
+ @Expect(hasWrite, true)
61
+ }
62
+
63
+ @Test
64
+ func testClipboardPluginShimTargetsItsOwnNamespace(): Unit {
65
+ let js = ClipboardPlugin().jsShim()
66
+ @Expect(js.contains("t.clipboard"), true)
67
+ @Expect(js.contains("'clipboard:write'"), true)
68
+ @Expect(js.contains("'clipboard:read'"), true)
69
+ }
70
+
71
+ @Test
72
+ func testClipboardPluginDeclaresPermissionSets(): Unit {
73
+ let sets = pluginPermissionSets(ClipboardPlugin())
74
+ @Expect(sets.size, 2)
75
+ // readonly 只放行读:剪贴板里可能有用户敏感内容,「只写不读」是合理的最小授权面
76
+ let ro = sets.get("clipboard:readonly").getOrThrow()
77
+ @Expect(ro.size, 1)
78
+ @Expect(ro[0], "clipboard:read")
79
+ @Expect(sets.get("clipboard:default").getOrThrow().size, 2)
80
+ }
81
+
82
+ @Test
83
+ func testClipboardPermissionSetGatesCommands(): Unit {
84
+ let out = ArrayList<String>()
85
+ let reg = CapabilityRegistry()
86
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"c\",\"permissions\":[\"clipboard:readonly\"]}"))
87
+ let hub = hubWithRegistry(reg, out)
88
+ registerPluginCommands(hub, ClipboardPlugin())
89
+ registerPluginPermissionSets(reg, ClipboardPlugin())
90
+
91
+ // 集内命令放行:这条会走到「宿主未装配」的业务错误,而不是「未授权」
92
+ hub.handleRawMessage("{\"type\":\"invoke\",\"id\":41,\"cmd\":\"clipboard:read\"}")
93
+ waitSink(out, 1)
94
+ @Expect(out.size, 1)
95
+ @Expect(strField(out[0], "error").contains("command not allowed"), false)
96
+
97
+ // 集外命令仍拒:readonly 不夹带写能力
98
+ hub.handleRawMessage("{\"type\":\"invoke\",\"id\":42,\"cmd\":\"clipboard:write\",\"args\":{\"text\":\"x\"}}")
99
+ @Expect(out.size, 2)
100
+ @Expect(strField(out[1], "error"), "command not allowed: clipboard:write")
101
+ }
102
+
103
+ @Test
104
+ func testClipboardWriteRequiresTextArgument(): Unit {
105
+ let cmd = ClipboardPlugin().commands().get("write").getOrThrow()
106
+ let ctx = IpcContext("main", IpcHub())
107
+ var threw = false
108
+ try {
109
+ cmd.handle("clipboard:write", JsonObject(), ctx)
110
+ } catch (e: CommandException) {
111
+ threw = true
112
+ @Expect(e.message.contains("missing argument \"text\""), true)
113
+ }
114
+ @Expect(threw, true)
115
+ }
116
+
117
+ @Test
118
+ func testClipboardWriteRejectsNonString(): Unit {
119
+ var args = JsonObject()
120
+ args.put("text", JsonInt(7))
121
+ let cmd = ClipboardPlugin().commands().get("write").getOrThrow()
122
+ let ctx = IpcContext("main", IpcHub())
123
+ var threw = false
124
+ try {
125
+ cmd.handle("clipboard:write", args, ctx)
126
+ } catch (e: CommandException) {
127
+ threw = true
128
+ @Expect(e.message.contains("must be a string"), true)
129
+ }
130
+ @Expect(threw, true)
131
+ }
132
+
133
+ @Test
134
+ func testClipboardWithoutHostIsReadableError(): Unit {
135
+ // 没经 .plugin() 装配(没有宿主)时调用:给的是可读错误,而不是空指针崩掉
136
+ @Expect(clipErrOf(ClipboardPlugin().commands().get("read").getOrThrow(), "clipboard:read", JsonObject())
137
+ .contains("宿主未装配"), true)
138
+ @Expect(clipErrOf(ClipboardPlugin().commands().get("write").getOrThrow(), "clipboard:write",
139
+ clipTextArgs("hi")).contains("宿主未装配"), true)
140
+ }
141
+
142
+ @Test
143
+ func testClipboardRejectsUnknownCommand(): Unit {
144
+ @Expect(clipErrOf(ClipboardPlugin().commands().get("read").getOrThrow(), "clipboard:nope", JsonObject())
145
+ .contains("unknown clipboard command"), true)
146
+ }
147
+
148
+ @Test
149
+ func testClipboardWriteHandsTextToHost(): Unit {
150
+ let host = FakeHost("main")
151
+ let cmd = clipCmdWithHost("write", host)
152
+ let r = cmd.handle("clipboard:write", clipTextArgs("hello 剪贴板"), IpcContext("main", IpcHub()))
153
+ // 成功回 JsonNull(写命令没有返回值,与 Tauri 的 writeText 对齐)
154
+ match (r.kind()) {
155
+ case JsonKind.JsNull => ()
156
+ case _ =>
157
+ @Expect(false, true) // 成功路径不该回别的类型
158
+ }
159
+ @Expect(host.clipboardWritten, "hello 剪贴板")
160
+ }
161
+
162
+ @Test
163
+ func testClipboardReadReturnsHostValue(): Unit {
164
+ let host = FakeHost("main")
165
+ let cmd = clipCmdWithHost("read", host)
166
+
167
+ // 空 / 非文本 / 平台失败都归 None,转成 JsonNull——这是合法结果,不是错误
168
+ let empty = cmd.handle("clipboard:read", JsonObject(), IpcContext("main", IpcHub()))
169
+ match (empty.kind()) {
170
+ case JsonKind.JsNull => ()
171
+ case _ => @Expect(false, true)
172
+ }
173
+
174
+ // 有内容:原样转成 JsonString 回给前端
175
+ host.clipboardReadValue = Some("pasted 文本")
176
+ let got = cmd.handle("clipboard:read", JsonObject(), IpcContext("main", IpcHub()))
177
+ @Expect(got.asString().getValue(), "pasted 文本")
178
+ }
179
+
180
+ /** 桩侧「label → 宿主」解析器见 plugin_dialog_test.cj 的 routeProbeHost(同包可见) */
181
+ @Test
182
+ func testClipboardRoutesToTheWindowThatAsked(): Unit {
183
+ // 与 dialog 同一条契约:报文里的 window 决定交给哪个宿主。
184
+ // 只装单槽时第二扇窗的读写会落到默认窗那个宿主上(多窗口下的串台来源)。
185
+ let mainHost = FakeHost("main")
186
+ let secondHost = FakeHost("second")
187
+ let cmds = ClipboardCommands()
188
+ cmds.attachHost(mainHost)
189
+ cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
190
+
191
+ cmds.handle("clipboard:write", clipTextArgs("second 的剪贴板"), IpcContext("second", IpcHub()))
192
+ @Expect(secondHost.clipboardWritten, "second 的剪贴板")
193
+ // 关键断言:另一扇窗的宿主一点都没被碰到
194
+ @Expect(mainHost.clipboardWritten, "")
195
+
196
+ // 读同样按窗口解析
197
+ secondHost.clipboardReadValue = Some("from second")
198
+ let got = cmds.handle("clipboard:read", JsonObject(), IpcContext("second", IpcHub()))
199
+ @Expect(got.asString().getValue(), "from second")
200
+ @Expect(mainHost.clipboardReadValue.isNone(), true)
201
+ }
202
+
203
+ @Test
204
+ func testClipboardReportsUnknownWindowInsteadOfFallingBack(): Unit {
205
+ let mainHost = FakeHost("main")
206
+ let secondHost = FakeHost("second")
207
+ let cmds = ClipboardCommands()
208
+ cmds.attachHost(mainHost)
209
+ cmds.attachHostResolver({ label => routeProbeHost(label, mainHost, secondHost) })
210
+
211
+ var threw = false
212
+ try {
213
+ cmds.handle("clipboard:write", clipTextArgs("x"), IpcContext("ghost", IpcHub()))
214
+ } catch (e: CommandException) {
215
+ threw = true
216
+ @Expect(e.message.contains("ghost"), true)
217
+ }
218
+ @Expect(threw, true)
219
+ // 报错之后两个宿主都没被写过:不回落单槽,宁可不写
220
+ @Expect(mainHost.clipboardWritten, "")
221
+ @Expect(secondHost.clipboardWritten, "")
222
+ }