cj-tauri 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -99
- 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 +632 -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 +526 -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 +3740 -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/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -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 +337 -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 +126 -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 +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -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
package/src/tests/plugin_test.cj
CHANGED
|
@@ -1,585 +1,605 @@
|
|
|
1
|
-
package cjTauri.tests
|
|
2
|
-
|
|
3
|
-
import cjTauri.*
|
|
4
|
-
import cjTauri.plugins.fs.*
|
|
5
|
-
import std.collection.*
|
|
6
|
-
import std.env.*
|
|
7
|
-
import stdx.encoding.json.*
|
|
8
|
-
import std.unittest.*
|
|
9
|
-
import std.unittest.testmacro.*
|
|
10
|
-
|
|
11
|
-
/*
|
|
12
|
-
* 插件体系单元测试:命名空间前缀、接口默认实现、JS 汇总与 HTML 注入位置、fs 插件的读写。
|
|
13
|
-
* 纯逻辑为主(不创建窗口),可在无 GUI 环境跑;文件用例只碰 /tmp 下的固定路径。
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/** 只实现必写成员的最小插件:用来验证接口默认实现(events / jsShim / setup)够用 */
|
|
17
|
-
class MiniPlugin <: Plugin {
|
|
18
|
-
public init() {}
|
|
19
|
-
|
|
20
|
-
public func name(): String {
|
|
21
|
-
return "mini"
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
public func commands(): HashMap<String, CommandHandler> {
|
|
25
|
-
let m = HashMap<String, CommandHandler>()
|
|
26
|
-
m.add("ping", PluginEchoHandler())
|
|
27
|
-
return m
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** 带事件与前端 shim 的插件 */
|
|
32
|
-
class DemoPlugin <: Plugin {
|
|
33
|
-
public init() {}
|
|
34
|
-
|
|
35
|
-
public func name(): String {
|
|
36
|
-
return "demo"
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
public func commands(): HashMap<String, CommandHandler> {
|
|
40
|
-
let m = HashMap<String, CommandHandler>()
|
|
41
|
-
m.add("do", PluginEchoHandler())
|
|
42
|
-
return m
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
public func events(): ArrayList<String> {
|
|
46
|
-
let e = ArrayList<String>()
|
|
47
|
-
e.add("changed")
|
|
48
|
-
return e
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
public func jsShim(): String {
|
|
52
|
-
return "window.__CJ_TAURI__.demo = {};\n"
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** 回显命令全名,便于断言「前端调到的到底是哪个名字」 */
|
|
57
|
-
class PluginEchoHandler <: CommandHandler {
|
|
58
|
-
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
59
|
-
let _ = args
|
|
60
|
-
let _ = ipc
|
|
61
|
-
return JsonString("pong:${cmd}")
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** 声明命名权限集的插件:验证「短名补前缀」「集声明 ≠ 放行」 */
|
|
66
|
-
class ScopedPlugin <: Plugin {
|
|
67
|
-
public init() {}
|
|
68
|
-
|
|
69
|
-
public func name(): String {
|
|
70
|
-
return "scoped"
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
public func commands(): HashMap<String, CommandHandler> {
|
|
74
|
-
let m = HashMap<String, CommandHandler>()
|
|
75
|
-
m.add("read", PluginEchoHandler())
|
|
76
|
-
m.add("write", PluginEchoHandler())
|
|
77
|
-
return m
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
public func events(): ArrayList<String> {
|
|
81
|
-
let e = ArrayList<String>()
|
|
82
|
-
e.add("changed")
|
|
83
|
-
return e
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
87
|
-
let m = HashMap<String, ArrayList<String>>()
|
|
88
|
-
let watch = ArrayList<String>()
|
|
89
|
-
watch.add("read") // 短名:应补成 scoped:read
|
|
90
|
-
watch.add("scoped:changed") // 全名:原样保留
|
|
91
|
-
m.add("watch", watch)
|
|
92
|
-
return m
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** 用给定能力注册中心装配的 hub(命名权限集用例用;不碰宿主层) */
|
|
97
|
-
func hubWithRegistry(reg: CapabilityRegistry, sink: ArrayList<String>): IpcHub {
|
|
98
|
-
let hub = IpcHub()
|
|
99
|
-
hub.setCapabilities(reg)
|
|
100
|
-
// 第二个参数是目标窗口 label:本文件只关心「投出去了什么」,路由由 TauriApp 层的用例覆盖
|
|
101
|
-
hub.jsSink = { s, _ => addToTestSink(sink, s) }
|
|
102
|
-
return hub
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** 造一个只有 path 的对象 */
|
|
106
|
-
func pathArgs(path: String): JsonObject {
|
|
107
|
-
var o = JsonObject()
|
|
108
|
-
o.put("path", JsonString(path))
|
|
109
|
-
return o
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
@Test
|
|
113
|
-
func testPluginNameValidation(): Unit {
|
|
114
|
-
@Expect(isValidPluginName("fs"), true)
|
|
115
|
-
@Expect(isValidPluginName("dialog2"), true)
|
|
116
|
-
@Expect(isValidPluginName(""), false)
|
|
117
|
-
@Expect(isValidPluginName("a:b"), false)
|
|
118
|
-
@Expect(isValidPluginName("a:b"), false) // 全角冒号同样不行
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
@Test
|
|
122
|
-
func testPluginCommandNamesArePrefixed(): Unit {
|
|
123
|
-
let names = pluginCommandNames(MiniPlugin())
|
|
124
|
-
@Expect(names.size, 1)
|
|
125
|
-
@Expect(names[0], "mini:ping")
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
@Test
|
|
129
|
-
func testPluginEventNamesArePrefixed(): Unit {
|
|
130
|
-
@Expect(pluginEventNames(MiniPlugin()).size, 0) // 默认实现:没有事件
|
|
131
|
-
let names = pluginEventNames(DemoPlugin())
|
|
132
|
-
@Expect(names.size, 1)
|
|
133
|
-
@Expect(names[0], "demo:changed")
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
@Test
|
|
137
|
-
func testRegisterPluginCommandsUsesNamespace(): Unit {
|
|
138
|
-
let out = ArrayList<String>()
|
|
139
|
-
let hub = hubWith(["mini:ping"], [], out)
|
|
140
|
-
registerPluginCommands(hub, MiniPlugin())
|
|
141
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":1,\"cmd\":\"mini:ping\"}")
|
|
142
|
-
waitSink(out, 1)
|
|
143
|
-
@Expect(out.size, 1)
|
|
144
|
-
@Expect(strField(out[0], "type"), "resolve")
|
|
145
|
-
@Expect(strField(out[0], "data"), "pong:mini:ping")
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
@Test
|
|
149
|
-
func testPluginDoesNotAutoGrantCapability(): Unit {
|
|
150
|
-
let out = ArrayList<String>()
|
|
151
|
-
// 插件把命令注册进来了,但能力清单没放行:默认最小权限仍生效(插件不自动放行)
|
|
152
|
-
let hub = hubWith([], [], out)
|
|
153
|
-
registerPluginCommands(hub, MiniPlugin())
|
|
154
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":2,\"cmd\":\"mini:ping\"}")
|
|
155
|
-
@Expect(out.size, 1)
|
|
156
|
-
@Expect(strField(out[0], "error"), "command not allowed: mini:ping")
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
@Test
|
|
160
|
-
func testPluginInitScriptsSkipEmpty(): Unit {
|
|
161
|
-
let none = ArrayList<Plugin>()
|
|
162
|
-
@Expect(pluginInitScripts(none).size, 0) // 没有任何 shim 就不注册脚本
|
|
163
|
-
|
|
164
|
-
let onlyEmpty = ArrayList<Plugin>()
|
|
165
|
-
onlyEmpty.add(MiniPlugin()) // 默认 shim 为空,应被跳过
|
|
166
|
-
@Expect(pluginInitScripts(onlyEmpty).size, 0)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
@Test
|
|
170
|
-
func testPluginInitScriptsPassShimThrough(): Unit {
|
|
171
|
-
let plugins = ArrayList<Plugin>()
|
|
172
|
-
plugins.add(DemoPlugin())
|
|
173
|
-
let scripts = pluginInitScripts(plugins)
|
|
174
|
-
@Expect(scripts.size, 1)
|
|
175
|
-
@Expect(scripts[0].contains("__CJ_TAURI__.demo"), true)
|
|
176
|
-
// 交给宿主的是裸 JS 片段、不是 script 标签块:桥按脚本注入,不再拼 HTML(RFC §5.4 的 v2)
|
|
177
|
-
@Expect(scripts[0].contains("<script>"), false)
|
|
178
|
-
@Expect(scripts[0].size, DemoPlugin().jsShim().size) // 原样透传,不改写
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
@Test
|
|
182
|
-
func testPluginInitScriptsOnePerPluginInOrder(): Unit {
|
|
183
|
-
let plugins = ArrayList<Plugin>()
|
|
184
|
-
plugins.add(DemoPlugin())
|
|
185
|
-
plugins.add(FsPlugin())
|
|
186
|
-
let scripts = pluginInitScripts(plugins)
|
|
187
|
-
// 一段一个插件(不拼成一大段):某个插件的 shim 写坏了不会带崩别的插件
|
|
188
|
-
@Expect(scripts.size, 2)
|
|
189
|
-
@Expect(scripts[0].contains("__CJ_TAURI__.demo"), true)
|
|
190
|
-
@Expect(scripts[1].contains("t.fs ="), true)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
@Test
|
|
194
|
-
func testPluginInitScriptsMixedPluginsKeepOrder(): Unit {
|
|
195
|
-
// 空 shim 的插件不占位,剩下的仍按装配顺序排列
|
|
196
|
-
let plugins = ArrayList<Plugin>()
|
|
197
|
-
plugins.add(MiniPlugin())
|
|
198
|
-
plugins.add(DemoPlugin())
|
|
199
|
-
plugins.add(FsPlugin())
|
|
200
|
-
let scripts = pluginInitScripts(plugins)
|
|
201
|
-
@Expect(scripts.size, 2)
|
|
202
|
-
@Expect(scripts[0].contains("__CJ_TAURI__.demo"), true)
|
|
203
|
-
@Expect(scripts[1].contains("t.fs ="), true)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
@Test
|
|
207
|
-
func testDescribePluginsListsCommandsAndEvents(): Unit {
|
|
208
|
-
let plugins = ArrayList<Plugin>()
|
|
209
|
-
plugins.add(DemoPlugin())
|
|
210
|
-
let text = describePlugins(plugins)
|
|
211
|
-
@Expect(text.contains("demo"), true)
|
|
212
|
-
@Expect(text.contains("demo:do"), true)
|
|
213
|
-
@Expect(text.contains("events: demo:changed"), true)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
@Test
|
|
217
|
-
func testFsPluginSurface(): Unit {
|
|
218
|
-
let p = FsPlugin()
|
|
219
|
-
@Expect(p.name(), "fs")
|
|
220
|
-
let names = pluginCommandNames(p)
|
|
221
|
-
@Expect(names.size,
|
|
222
|
-
var hasRead = false
|
|
223
|
-
var hasWrite = false
|
|
224
|
-
var hasExists = false
|
|
225
|
-
var hasList = false
|
|
226
|
-
var hasRemove = false
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
@
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
@Expect(
|
|
264
|
-
|
|
265
|
-
@Expect(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
@Expect(
|
|
277
|
-
@Expect(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
@Expect(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
@Expect(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
@Expect(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
@
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":
|
|
335
|
-
@Expect(out.size,
|
|
336
|
-
@Expect(strField(out[
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
@Test
|
|
340
|
-
func
|
|
341
|
-
let
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
@
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
let
|
|
362
|
-
@Expect(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
let
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
@
|
|
390
|
-
|
|
391
|
-
let
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
let
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
@
|
|
469
|
-
|
|
470
|
-
//
|
|
471
|
-
let
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
@Expect(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
@Expect(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
@Expect(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
@Expect(renderPluginManifest(
|
|
541
|
-
@Expect(
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
func
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
1
|
+
package cjTauri.tests
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
import cjTauri.plugins.fs.*
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import std.env.*
|
|
7
|
+
import stdx.encoding.json.*
|
|
8
|
+
import std.unittest.*
|
|
9
|
+
import std.unittest.testmacro.*
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* 插件体系单元测试:命名空间前缀、接口默认实现、JS 汇总与 HTML 注入位置、fs 插件的读写。
|
|
13
|
+
* 纯逻辑为主(不创建窗口),可在无 GUI 环境跑;文件用例只碰 /tmp 下的固定路径。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 只实现必写成员的最小插件:用来验证接口默认实现(events / jsShim / setup)够用 */
|
|
17
|
+
class MiniPlugin <: Plugin {
|
|
18
|
+
public init() {}
|
|
19
|
+
|
|
20
|
+
public func name(): String {
|
|
21
|
+
return "mini"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
25
|
+
let m = HashMap<String, CommandHandler>()
|
|
26
|
+
m.add("ping", PluginEchoHandler())
|
|
27
|
+
return m
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 带事件与前端 shim 的插件 */
|
|
32
|
+
class DemoPlugin <: Plugin {
|
|
33
|
+
public init() {}
|
|
34
|
+
|
|
35
|
+
public func name(): String {
|
|
36
|
+
return "demo"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
40
|
+
let m = HashMap<String, CommandHandler>()
|
|
41
|
+
m.add("do", PluginEchoHandler())
|
|
42
|
+
return m
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public func events(): ArrayList<String> {
|
|
46
|
+
let e = ArrayList<String>()
|
|
47
|
+
e.add("changed")
|
|
48
|
+
return e
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public func jsShim(): String {
|
|
52
|
+
return "window.__CJ_TAURI__.demo = {};\n"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 回显命令全名,便于断言「前端调到的到底是哪个名字」 */
|
|
57
|
+
class PluginEchoHandler <: CommandHandler {
|
|
58
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
59
|
+
let _ = args
|
|
60
|
+
let _ = ipc
|
|
61
|
+
return JsonString("pong:${cmd}")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 声明命名权限集的插件:验证「短名补前缀」「集声明 ≠ 放行」 */
|
|
66
|
+
class ScopedPlugin <: Plugin {
|
|
67
|
+
public init() {}
|
|
68
|
+
|
|
69
|
+
public func name(): String {
|
|
70
|
+
return "scoped"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
74
|
+
let m = HashMap<String, CommandHandler>()
|
|
75
|
+
m.add("read", PluginEchoHandler())
|
|
76
|
+
m.add("write", PluginEchoHandler())
|
|
77
|
+
return m
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public func events(): ArrayList<String> {
|
|
81
|
+
let e = ArrayList<String>()
|
|
82
|
+
e.add("changed")
|
|
83
|
+
return e
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
87
|
+
let m = HashMap<String, ArrayList<String>>()
|
|
88
|
+
let watch = ArrayList<String>()
|
|
89
|
+
watch.add("read") // 短名:应补成 scoped:read
|
|
90
|
+
watch.add("scoped:changed") // 全名:原样保留
|
|
91
|
+
m.add("watch", watch)
|
|
92
|
+
return m
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 用给定能力注册中心装配的 hub(命名权限集用例用;不碰宿主层) */
|
|
97
|
+
func hubWithRegistry(reg: CapabilityRegistry, sink: ArrayList<String>): IpcHub {
|
|
98
|
+
let hub = IpcHub()
|
|
99
|
+
hub.setCapabilities(reg)
|
|
100
|
+
// 第二个参数是目标窗口 label:本文件只关心「投出去了什么」,路由由 TauriApp 层的用例覆盖
|
|
101
|
+
hub.jsSink = { s, _ => addToTestSink(sink, s) }
|
|
102
|
+
return hub
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 造一个只有 path 的对象 */
|
|
106
|
+
func pathArgs(path: String): JsonObject {
|
|
107
|
+
var o = JsonObject()
|
|
108
|
+
o.put("path", JsonString(path))
|
|
109
|
+
return o
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Test
|
|
113
|
+
func testPluginNameValidation(): Unit {
|
|
114
|
+
@Expect(isValidPluginName("fs"), true)
|
|
115
|
+
@Expect(isValidPluginName("dialog2"), true)
|
|
116
|
+
@Expect(isValidPluginName(""), false)
|
|
117
|
+
@Expect(isValidPluginName("a:b"), false)
|
|
118
|
+
@Expect(isValidPluginName("a:b"), false) // 全角冒号同样不行
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Test
|
|
122
|
+
func testPluginCommandNamesArePrefixed(): Unit {
|
|
123
|
+
let names = pluginCommandNames(MiniPlugin())
|
|
124
|
+
@Expect(names.size, 1)
|
|
125
|
+
@Expect(names[0], "mini:ping")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
func testPluginEventNamesArePrefixed(): Unit {
|
|
130
|
+
@Expect(pluginEventNames(MiniPlugin()).size, 0) // 默认实现:没有事件
|
|
131
|
+
let names = pluginEventNames(DemoPlugin())
|
|
132
|
+
@Expect(names.size, 1)
|
|
133
|
+
@Expect(names[0], "demo:changed")
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test
|
|
137
|
+
func testRegisterPluginCommandsUsesNamespace(): Unit {
|
|
138
|
+
let out = ArrayList<String>()
|
|
139
|
+
let hub = hubWith(["mini:ping"], [], out)
|
|
140
|
+
registerPluginCommands(hub, MiniPlugin())
|
|
141
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":1,\"cmd\":\"mini:ping\"}")
|
|
142
|
+
waitSink(out, 1)
|
|
143
|
+
@Expect(out.size, 1)
|
|
144
|
+
@Expect(strField(out[0], "type"), "resolve")
|
|
145
|
+
@Expect(strField(out[0], "data"), "pong:mini:ping")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Test
|
|
149
|
+
func testPluginDoesNotAutoGrantCapability(): Unit {
|
|
150
|
+
let out = ArrayList<String>()
|
|
151
|
+
// 插件把命令注册进来了,但能力清单没放行:默认最小权限仍生效(插件不自动放行)
|
|
152
|
+
let hub = hubWith([], [], out)
|
|
153
|
+
registerPluginCommands(hub, MiniPlugin())
|
|
154
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":2,\"cmd\":\"mini:ping\"}")
|
|
155
|
+
@Expect(out.size, 1)
|
|
156
|
+
@Expect(strField(out[0], "error"), "command not allowed: mini:ping")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Test
|
|
160
|
+
func testPluginInitScriptsSkipEmpty(): Unit {
|
|
161
|
+
let none = ArrayList<Plugin>()
|
|
162
|
+
@Expect(pluginInitScripts(none).size, 0) // 没有任何 shim 就不注册脚本
|
|
163
|
+
|
|
164
|
+
let onlyEmpty = ArrayList<Plugin>()
|
|
165
|
+
onlyEmpty.add(MiniPlugin()) // 默认 shim 为空,应被跳过
|
|
166
|
+
@Expect(pluginInitScripts(onlyEmpty).size, 0)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Test
|
|
170
|
+
func testPluginInitScriptsPassShimThrough(): Unit {
|
|
171
|
+
let plugins = ArrayList<Plugin>()
|
|
172
|
+
plugins.add(DemoPlugin())
|
|
173
|
+
let scripts = pluginInitScripts(plugins)
|
|
174
|
+
@Expect(scripts.size, 1)
|
|
175
|
+
@Expect(scripts[0].contains("__CJ_TAURI__.demo"), true)
|
|
176
|
+
// 交给宿主的是裸 JS 片段、不是 script 标签块:桥按脚本注入,不再拼 HTML(RFC §5.4 的 v2)
|
|
177
|
+
@Expect(scripts[0].contains("<script>"), false)
|
|
178
|
+
@Expect(scripts[0].size, DemoPlugin().jsShim().size) // 原样透传,不改写
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@Test
|
|
182
|
+
func testPluginInitScriptsOnePerPluginInOrder(): Unit {
|
|
183
|
+
let plugins = ArrayList<Plugin>()
|
|
184
|
+
plugins.add(DemoPlugin())
|
|
185
|
+
plugins.add(FsPlugin())
|
|
186
|
+
let scripts = pluginInitScripts(plugins)
|
|
187
|
+
// 一段一个插件(不拼成一大段):某个插件的 shim 写坏了不会带崩别的插件
|
|
188
|
+
@Expect(scripts.size, 2)
|
|
189
|
+
@Expect(scripts[0].contains("__CJ_TAURI__.demo"), true)
|
|
190
|
+
@Expect(scripts[1].contains("t.fs ="), true)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@Test
|
|
194
|
+
func testPluginInitScriptsMixedPluginsKeepOrder(): Unit {
|
|
195
|
+
// 空 shim 的插件不占位,剩下的仍按装配顺序排列
|
|
196
|
+
let plugins = ArrayList<Plugin>()
|
|
197
|
+
plugins.add(MiniPlugin())
|
|
198
|
+
plugins.add(DemoPlugin())
|
|
199
|
+
plugins.add(FsPlugin())
|
|
200
|
+
let scripts = pluginInitScripts(plugins)
|
|
201
|
+
@Expect(scripts.size, 2)
|
|
202
|
+
@Expect(scripts[0].contains("__CJ_TAURI__.demo"), true)
|
|
203
|
+
@Expect(scripts[1].contains("t.fs ="), true)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Test
|
|
207
|
+
func testDescribePluginsListsCommandsAndEvents(): Unit {
|
|
208
|
+
let plugins = ArrayList<Plugin>()
|
|
209
|
+
plugins.add(DemoPlugin())
|
|
210
|
+
let text = describePlugins(plugins)
|
|
211
|
+
@Expect(text.contains("demo"), true)
|
|
212
|
+
@Expect(text.contains("demo:do"), true)
|
|
213
|
+
@Expect(text.contains("events: demo:changed"), true)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@Test
|
|
217
|
+
func testFsPluginSurface(): Unit {
|
|
218
|
+
let p = FsPlugin()
|
|
219
|
+
@Expect(p.name(), "fs")
|
|
220
|
+
let names = pluginCommandNames(p)
|
|
221
|
+
@Expect(names.size, 9)
|
|
222
|
+
var hasRead = false
|
|
223
|
+
var hasWrite = false
|
|
224
|
+
var hasExists = false
|
|
225
|
+
var hasList = false
|
|
226
|
+
var hasRemove = false
|
|
227
|
+
var hasMkdir = false
|
|
228
|
+
var hasRename = false
|
|
229
|
+
var hasCopy = false
|
|
230
|
+
var hasStat = false
|
|
231
|
+
for (n in names) {
|
|
232
|
+
if (n == "fs:readText") {
|
|
233
|
+
hasRead = true
|
|
234
|
+
}
|
|
235
|
+
if (n == "fs:writeText") {
|
|
236
|
+
hasWrite = true
|
|
237
|
+
}
|
|
238
|
+
if (n == "fs:exists") {
|
|
239
|
+
hasExists = true
|
|
240
|
+
}
|
|
241
|
+
if (n == "fs:list") {
|
|
242
|
+
hasList = true
|
|
243
|
+
}
|
|
244
|
+
if (n == "fs:remove") {
|
|
245
|
+
hasRemove = true
|
|
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
|
+
}
|
|
259
|
+
}
|
|
260
|
+
@Expect(hasRead, true)
|
|
261
|
+
@Expect(hasWrite, true)
|
|
262
|
+
@Expect(hasExists, true)
|
|
263
|
+
@Expect(hasList, true) // 目录列举:笔记工具的文件树靠它,应用层不必自己拼 std.fs
|
|
264
|
+
@Expect(hasRemove, true)
|
|
265
|
+
@Expect(hasMkdir, true)
|
|
266
|
+
@Expect(hasRename, true)
|
|
267
|
+
@Expect(hasCopy, true)
|
|
268
|
+
@Expect(hasStat, true)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@Test
|
|
272
|
+
func testFsPluginShimTargetsItsOwnNamespace(): Unit {
|
|
273
|
+
let js = FsPlugin().jsShim()
|
|
274
|
+
@Expect(js.contains("t.fs"), true)
|
|
275
|
+
@Expect(js.contains("'fs:readText'"), true)
|
|
276
|
+
@Expect(js.contains("'fs:writeText'"), true)
|
|
277
|
+
@Expect(js.contains("'fs:exists'"), true)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@Test
|
|
281
|
+
func testFsPluginDeclaresPermissionSets(): Unit {
|
|
282
|
+
let sets = pluginPermissionSets(FsPlugin())
|
|
283
|
+
@Expect(sets.size, 2)
|
|
284
|
+
let ro = sets.get("fs:readonly").getOrThrow()
|
|
285
|
+
@Expect(ro.size, 4)
|
|
286
|
+
var roHasWrite = false
|
|
287
|
+
var roHasRemove = false
|
|
288
|
+
for (m in ro) {
|
|
289
|
+
if (m == "fs:writeText") {
|
|
290
|
+
roHasWrite = true
|
|
291
|
+
}
|
|
292
|
+
if (m == "fs:remove") {
|
|
293
|
+
roHasRemove = true
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
@Expect(roHasWrite, false) // 只读集不含写:最小权限不打折
|
|
297
|
+
@Expect(roHasRemove, false) // 删也同理——列目录是「读」,删除不是
|
|
298
|
+
@Expect(sets.get("fs:default").getOrThrow().size, 9)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@Test
|
|
302
|
+
func testPluginPermissionSetsAreNamespacedAndExpanded(): Unit {
|
|
303
|
+
let sets = pluginPermissionSets(ScopedPlugin())
|
|
304
|
+
@Expect(sets.size, 1)
|
|
305
|
+
@Expect(sets.contains("scoped:watch"), true)
|
|
306
|
+
let members = sets.get("scoped:watch").getOrThrow()
|
|
307
|
+
@Expect(members.size, 2)
|
|
308
|
+
var hasShort = false
|
|
309
|
+
var hasFull = false
|
|
310
|
+
for (m in members) {
|
|
311
|
+
if (m == "scoped:read") {
|
|
312
|
+
hasShort = true
|
|
313
|
+
}
|
|
314
|
+
if (m == "scoped:changed") {
|
|
315
|
+
hasFull = true
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
@Expect(hasShort, true) // 短名补了插件名前缀
|
|
319
|
+
@Expect(hasFull, true) // 全名原样
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@Test
|
|
323
|
+
func testPluginPermissionSetsDefaultEmpty(): Unit {
|
|
324
|
+
// 接口默认实现:没写 permissions() 的插件没有集(MiniPlugin 只写了必写成员)
|
|
325
|
+
@Expect(pluginPermissionSets(MiniPlugin()).size, 0)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
@Test
|
|
329
|
+
func testPluginPermissionSetDoesNotAutoGrant(): Unit {
|
|
330
|
+
let out = ArrayList<String>()
|
|
331
|
+
// 插件声明了 scoped:watch,但清单没引用它 → 命令仍被拒(集声明不等于放行)
|
|
332
|
+
let hub = hubWith([], [], out)
|
|
333
|
+
registerPluginCommands(hub, ScopedPlugin())
|
|
334
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":11,\"cmd\":\"scoped:read\"}")
|
|
335
|
+
@Expect(out.size, 1)
|
|
336
|
+
@Expect(strField(out[0], "error"), "command not allowed: scoped:read")
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@Test
|
|
340
|
+
func testPluginPermissionSetAuthorizesWhenReferenced(): Unit {
|
|
341
|
+
let out = ArrayList<String>()
|
|
342
|
+
let reg = CapabilityRegistry()
|
|
343
|
+
reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"d\",\"permissions\":[\"scoped:watch\"]}"))
|
|
344
|
+
let hub = hubWithRegistry(reg, out)
|
|
345
|
+
registerPluginCommands(hub, ScopedPlugin())
|
|
346
|
+
registerPluginPermissionSets(reg, ScopedPlugin())
|
|
347
|
+
// 集内命令放行
|
|
348
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":12,\"cmd\":\"scoped:read\"}")
|
|
349
|
+
waitSink(out, 1) // 执行是异步的,先等这条 resolve 落地,再发下一条同步拒绝
|
|
350
|
+
@Expect(out.size, 1)
|
|
351
|
+
@Expect(strField(out[0], "type"), "resolve")
|
|
352
|
+
@Expect(strField(out[0], "data"), "pong:scoped:read")
|
|
353
|
+
// 集外命令仍拒:放行的是那一组,不是整个插件
|
|
354
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":13,\"cmd\":\"scoped:write\"}")
|
|
355
|
+
@Expect(out.size, 2)
|
|
356
|
+
@Expect(strField(out[1], "error"), "command not allowed: scoped:write")
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@Test
|
|
360
|
+
func testDescribePermissionSets(): Unit {
|
|
361
|
+
let none = ArrayList<Plugin>()
|
|
362
|
+
@Expect(describePermissionSets(none), "")
|
|
363
|
+
let plugins = ArrayList<Plugin>()
|
|
364
|
+
plugins.add(ScopedPlugin())
|
|
365
|
+
let text = describePermissionSets(plugins)
|
|
366
|
+
@Expect(text.contains("scoped:watch"), true)
|
|
367
|
+
@Expect(text.contains("scoped:read"), true)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
@Test
|
|
371
|
+
func testFsWriteThenReadRoundTrip(): Unit {
|
|
372
|
+
let cmds = FsPlugin().commands()
|
|
373
|
+
let ctx = IpcContext("main", IpcHub())
|
|
374
|
+
let path = "/tmp/cj-tauri-plugin-roundtrip.txt"
|
|
375
|
+
|
|
376
|
+
var wArgs = pathArgs(path)
|
|
377
|
+
wArgs.put("contents", JsonString("插件写入的内容 CJ"))
|
|
378
|
+
let wrote = cmds.get("writeText").getOrThrow().handle("fs:writeText", wArgs, ctx)
|
|
379
|
+
@Expect(wrote.asBool().getValue(), true)
|
|
380
|
+
|
|
381
|
+
let read = cmds.get("readText").getOrThrow().handle("fs:readText", pathArgs(path), ctx)
|
|
382
|
+
@Expect(read.asString().getValue(), "插件写入的内容 CJ")
|
|
383
|
+
|
|
384
|
+
// 覆盖写:再写一次短内容,读回来应是新的(不是追加、也不是旧内容)
|
|
385
|
+
var w2 = pathArgs(path)
|
|
386
|
+
w2.put("contents", JsonString("覆盖"))
|
|
387
|
+
cmds.get("writeText").getOrThrow().handle("fs:writeText", w2, ctx)
|
|
388
|
+
let read2 = cmds.get("readText").getOrThrow().handle("fs:readText", pathArgs(path), ctx)
|
|
389
|
+
@Expect(read2.asString().getValue(), "覆盖")
|
|
390
|
+
|
|
391
|
+
let exists = cmds.get("exists").getOrThrow().handle("fs:exists", pathArgs(path), ctx)
|
|
392
|
+
@Expect(exists.asBool().getValue(), true)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
@Test
|
|
396
|
+
func testFsRejectsMissingArgument(): Unit {
|
|
397
|
+
let cmds = FsPlugin().commands()
|
|
398
|
+
let ctx = IpcContext("main", IpcHub())
|
|
399
|
+
var threw = false
|
|
400
|
+
try {
|
|
401
|
+
cmds.get("readText").getOrThrow().handle("fs:readText", JsonObject(), ctx)
|
|
402
|
+
} catch (e: CommandException) {
|
|
403
|
+
threw = true
|
|
404
|
+
@Expect(e.message.contains("missing argument"), true)
|
|
405
|
+
}
|
|
406
|
+
@Expect(threw, true)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
@Test
|
|
410
|
+
func testFsReadMissingFileBecomesBusinessError(): Unit {
|
|
411
|
+
let cmds = FsPlugin().commands()
|
|
412
|
+
let ctx = IpcContext("main", IpcHub())
|
|
413
|
+
var threw = false
|
|
414
|
+
try {
|
|
415
|
+
cmds.get("readText").getOrThrow().handle("fs:readText", pathArgs("/tmp/cj-tauri-plugin-no-such-file.txt"), ctx)
|
|
416
|
+
} catch (e: CommandException) {
|
|
417
|
+
// 抛 CommandException 而不是裸异常:前端拿到的是可读原文,不是 "command failed: ..."
|
|
418
|
+
threw = true
|
|
419
|
+
@Expect(e.message.startsWith("fs:readText failed:"), true)
|
|
420
|
+
}
|
|
421
|
+
@Expect(threw, true)
|
|
422
|
+
|
|
423
|
+
let exists = cmds.get("exists").getOrThrow().handle("fs:exists", pathArgs("/tmp/cj-tauri-plugin-no-such-file.txt"), ctx)
|
|
424
|
+
@Expect(exists.asBool().getValue(), false)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* JsonValue → ArrayList<JsonValue>(测试助手)
|
|
429
|
+
* JsonArray 本身不支持 for-in,且 arr.get(i) 的元素类型要靠上下文推断——
|
|
430
|
+
* 统一在这儿把它转成 ArrayList,后面就能直接下标访问、也能 for-in。
|
|
431
|
+
*/
|
|
432
|
+
func jsonItemsOf(v: JsonValue): ArrayList<JsonValue> {
|
|
433
|
+
let out = ArrayList<JsonValue>()
|
|
434
|
+
let arr = v.asArray()
|
|
435
|
+
for (i in 0..arr.size()) {
|
|
436
|
+
out.add(arr.get(i).getOrThrow())
|
|
437
|
+
}
|
|
438
|
+
return out
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** 取出对象里某个键的字符串数组(describePluginsJson 的测试助手) */
|
|
442
|
+
func jsonStringsOf(obj: JsonObject, key: String): ArrayList<String> {
|
|
443
|
+
let out = ArrayList<String>()
|
|
444
|
+
for (e in jsonItemsOf(obj.get(key).getOrThrow())) {
|
|
445
|
+
out.add(e.asString().getValue())
|
|
446
|
+
}
|
|
447
|
+
return out
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** "a|b" 形式,便于用字符串断言数组的内容与顺序 */
|
|
451
|
+
func joinWithPipe(items: ArrayList<String>): String {
|
|
452
|
+
var s = ""
|
|
453
|
+
for (x in items) {
|
|
454
|
+
if (s.size > 0) {
|
|
455
|
+
s += "|"
|
|
456
|
+
}
|
|
457
|
+
s += x
|
|
458
|
+
}
|
|
459
|
+
return s
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
@Test
|
|
463
|
+
func testDescribePluginsJsonShape(): Unit {
|
|
464
|
+
let plugins = ArrayList<Plugin>()
|
|
465
|
+
plugins.add(DemoPlugin())
|
|
466
|
+
plugins.add(ScopedPlugin())
|
|
467
|
+
let arr = jsonItemsOf(describePluginsJson(plugins))
|
|
468
|
+
@Expect(arr.size, 2)
|
|
469
|
+
|
|
470
|
+
// 数组顺序 = 装配顺序
|
|
471
|
+
let demo = arr[0].asObject()
|
|
472
|
+
@Expect(demo.get("name").getOrThrow().asString().getValue(), "demo")
|
|
473
|
+
@Expect(joinWithPipe(jsonStringsOf(demo, "commands")), "demo:do")
|
|
474
|
+
@Expect(joinWithPipe(jsonStringsOf(demo, "events")), "demo:changed")
|
|
475
|
+
@Expect(demo.get("permissions").getOrThrow().asObject().size(), 0)
|
|
476
|
+
@Expect(demo.get("hasShim").getOrThrow().asBool().getValue(), true)
|
|
477
|
+
|
|
478
|
+
let scoped = arr[1].asObject()
|
|
479
|
+
// 命令/事件按字典序(read 在 write 前),而不是 HashMap 的迭代顺序
|
|
480
|
+
@Expect(joinWithPipe(jsonStringsOf(scoped, "commands")), "scoped:read|scoped:write")
|
|
481
|
+
@Expect(scoped.get("hasShim").getOrThrow().asBool().getValue(), false)
|
|
482
|
+
// 权限集用「框架展开后」的全名,成员同样排序(scoped:changed < scoped:read)
|
|
483
|
+
let perms = scoped.get("permissions").getOrThrow().asObject()
|
|
484
|
+
@Expect(perms.size(), 1)
|
|
485
|
+
@Expect(joinWithPipe(jsonStringsOf(perms, "scoped:watch")), "scoped:changed|scoped:read")
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
@Test
|
|
489
|
+
func testDescribePluginsJsonStableAcrossRuns(): Unit {
|
|
490
|
+
// 连续两次序列化必须逐字节相同:命令/事件/权限键都排过序,不受 HashMap 迭代顺序影响
|
|
491
|
+
let plugins = ArrayList<Plugin>()
|
|
492
|
+
plugins.add(DemoPlugin())
|
|
493
|
+
plugins.add(ScopedPlugin())
|
|
494
|
+
plugins.add(MiniPlugin())
|
|
495
|
+
@Expect(describePluginsJson(plugins).toJsonString(), describePluginsJson(plugins).toJsonString())
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
@Test
|
|
499
|
+
func testDescribePluginsJsonEmptyList(): Unit {
|
|
500
|
+
let none = ArrayList<Plugin>()
|
|
501
|
+
@Expect(jsonItemsOf(describePluginsJson(none)).size, 0)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/*
|
|
505
|
+
* 清单导出通道(CJ_TAURI_MANIFEST):`cj-tauri info` 靠它从「跑到装配完成、但不建窗口」的应用里取清单。
|
|
506
|
+
* 这里只测纯逻辑(环境变量解析 + 两种模式的渲染与可解析性),不创建窗口。
|
|
507
|
+
*/
|
|
508
|
+
|
|
509
|
+
/** 把 manifestDumpMode() 的 Option 归一成字符串,便于用 @Expect 断言 */
|
|
510
|
+
func manifestModeOf(): String {
|
|
511
|
+
match (manifestDumpMode()) {
|
|
512
|
+
case Some(m) => return m
|
|
513
|
+
case None => return "<none>"
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
@Test
|
|
518
|
+
func testManifestDumpModeFromEnv(): Unit {
|
|
519
|
+
removeVariable("CJ_TAURI_MANIFEST")
|
|
520
|
+
@Expect(manifestModeOf(), "<none>")
|
|
521
|
+
|
|
522
|
+
setVariable("CJ_TAURI_MANIFEST", "")
|
|
523
|
+
@Expect(manifestModeOf(), "<none>") // 空串等同未设置
|
|
524
|
+
|
|
525
|
+
setVariable("CJ_TAURI_MANIFEST", "json")
|
|
526
|
+
@Expect(manifestModeOf(), "json")
|
|
527
|
+
|
|
528
|
+
setVariable("CJ_TAURI_MANIFEST", "1")
|
|
529
|
+
@Expect(manifestModeOf(), "text") // 只认 "json" 这一个值,其它一律当人类可读形态
|
|
530
|
+
|
|
531
|
+
removeVariable("CJ_TAURI_MANIFEST")
|
|
532
|
+
@Expect(manifestModeOf(), "<none>") // 收尾:别把环境变量漏给后面的用例
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
@Test
|
|
536
|
+
func testRenderPluginManifestTextMode(): Unit {
|
|
537
|
+
let plugins = ArrayList<Plugin>()
|
|
538
|
+
plugins.add(DemoPlugin())
|
|
539
|
+
// 人读形态就是启动日志里那份表,两种出口不能各写一套
|
|
540
|
+
@Expect(renderPluginManifest(plugins, "text"), describePlugins(plugins))
|
|
541
|
+
@Expect(renderPluginManifest(plugins, "text").contains("demo:do"), true)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
@Test
|
|
545
|
+
func testRenderPluginManifestJsonMode(): Unit {
|
|
546
|
+
let plugins = ArrayList<Plugin>()
|
|
547
|
+
plugins.add(DemoPlugin())
|
|
548
|
+
let text = renderPluginManifest(plugins, "json")
|
|
549
|
+
// 必须能被标准 JSON 解析器重新解析(CLI / CI 消费的就是它)
|
|
550
|
+
let arr = jsonItemsOf(JsonValue.fromStr(text))
|
|
551
|
+
@Expect(arr.size, 1)
|
|
552
|
+
let demo = arr[0].asObject()
|
|
553
|
+
@Expect(demo.get("name").getOrThrow().asString().getValue(), "demo")
|
|
554
|
+
@Expect(joinWithPipe(jsonStringsOf(demo, "commands")), "demo:do")
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
@Test
|
|
558
|
+
func testRenderPluginManifestEmptyList(): Unit {
|
|
559
|
+
let none = ArrayList<Plugin>()
|
|
560
|
+
@Expect(renderPluginManifest(none, "text"), "(未装配插件)\n")
|
|
561
|
+
@Expect(JsonValue.fromStr(renderPluginManifest(none, "json")).asArray().size(), 0)
|
|
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
|
+
}
|