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,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
|
+
}
|