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.
Files changed (158) hide show
  1. package/README.md +113 -99
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +632 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +526 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3740 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/prebuilt/linux-x64/cj-tauri +0 -0
  77. package/src/api_system.cj +67 -67
  78. package/src/app.cj +325 -969
  79. package/src/app_events.cj +166 -0
  80. package/src/app_host_ops.cj +287 -0
  81. package/src/app_run.cj +283 -0
  82. package/src/app_spec.cj +76 -76
  83. package/src/arkweb_menu.cj +216 -0
  84. package/src/arkweb_script.cj +142 -142
  85. package/src/capability.cj +409 -409
  86. package/src/clipboard_bridge.cj +178 -0
  87. package/src/dialog_spec.cj +79 -0
  88. package/src/dragdrop.cj +26 -26
  89. package/src/embedded_assets.cj +98 -98
  90. package/src/host.cj +752 -607
  91. package/src/host_arkweb.cj +924 -778
  92. package/src/host_webkit.cj +663 -570
  93. package/src/host_webview2.cj +663 -570
  94. package/src/hotkey.cj +66 -66
  95. package/src/ipc_hub.cj +337 -337
  96. package/src/ipc_message.cj +286 -286
  97. package/src/launch.cj +62 -62
  98. package/src/logging.cj +127 -127
  99. package/src/menu.cj +197 -197
  100. package/src/platform/platform_app_context.cj +77 -77
  101. package/src/platform/platform_assets.cj +33 -33
  102. package/src/platform/platform_discover.cj +176 -176
  103. package/src/platform/platform_env.cj +29 -29
  104. package/src/platform/platform_feature.cj +31 -31
  105. package/src/platform/platform_page_source.cj +87 -87
  106. package/src/platform/platform_target_info.cj +32 -32
  107. package/src/platform_host_provider.cj +87 -87
  108. package/src/plugin.cj +333 -333
  109. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  110. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  111. package/src/plugins/fs/plugin_fs.cj +105 -1
  112. package/src/plugins/http/http_backend.cj +131 -0
  113. package/src/plugins/http/http_desktop.cj +195 -0
  114. package/src/plugins/http/http_ohos.cj +204 -0
  115. package/src/plugins/http/plugin_http.cj +310 -459
  116. package/src/plugins/log/plugin_log.cj +105 -105
  117. package/src/plugins/menu/plugin_menu.cj +158 -158
  118. package/src/plugins/notification/plugin_notification.cj +154 -154
  119. package/src/plugins/serial/plugin_serial.cj +458 -454
  120. package/src/plugins/serial/serial_bridge.cj +194 -194
  121. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  122. package/src/plugins/serial/serial_ohos.cj +529 -0
  123. package/src/plugins/shell/plugin_shell.cj +258 -258
  124. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  125. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  126. package/src/plugins/ws/plugin_ws.cj +488 -470
  127. package/src/process_hidden.cj +174 -174
  128. package/src/scope.cj +391 -391
  129. package/src/shell_event.cj +69 -69
  130. package/src/tests/app_spec_test.cj +126 -127
  131. package/src/tests/capability_test.cj +243 -243
  132. package/src/tests/dialog_spec_test.cj +73 -0
  133. package/src/tests/fake_host.cj +201 -201
  134. package/src/tests/ipc_hub_test.cj +487 -487
  135. package/src/tests/logging_test.cj +62 -62
  136. package/src/tests/platform_contract_test.cj +251 -251
  137. package/src/tests/plugin_clipboard_test.cj +222 -222
  138. package/src/tests/plugin_dialog_test.cj +237 -237
  139. package/src/tests/plugin_fs_test.cj +207 -0
  140. package/src/tests/plugin_http_test.cj +124 -124
  141. package/src/tests/plugin_notification_test.cj +205 -205
  142. package/src/tests/plugin_serial_test.cj +48 -0
  143. package/src/tests/plugin_test.cj +605 -585
  144. package/src/tests/plugin_ws_test.cj +40 -40
  145. package/src/tests/process_hidden_test.cj +129 -129
  146. package/src/tests/scope_test.cj +350 -350
  147. package/src/tests/system_host_test.cj +99 -81
  148. package/src/tests/urlutil_test.cj +53 -53
  149. package/src/tests/version_test.cj +75 -75
  150. package/src/tests/window_registry_test.cj +123 -123
  151. package/src/tray.cj +90 -90
  152. package/src/urlutil.cj +159 -159
  153. package/src/version.cj +44 -44
  154. package/src/window.cj +51 -51
  155. package/src/window_registry.cj +116 -116
  156. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  157. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  158. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
@@ -1,62 +1,62 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import std.unittest.*
5
- import std.unittest.testmacro.*
6
-
7
- /*
8
- * 日志落盘的策略层用例(RFC-003 第 5 期)。
9
- *
10
- * 只测**纯函数**(默认目录推导、环境变量回退、路径拼接):真的开 fd 那一段归桩自检
11
- * (`native/tests/test_bridge_core.c` 的 `log file` 用例)与实机 E2E——单测里调桥的
12
- * `foreign func` 会踩「子包调不了 foreign func」那条(AGENTS §4,链接期报未定义符号)。
13
- *
14
- * 环境变量类的用例一律用**肯定不存在**的变量名:跑测试的机器上 `CJ_TAURI_LOG` 之类
15
- * 可能真的设着,拿它当「未设置」来断言就是一条会随机器飘的红线。
16
- */
17
-
18
- /** 保证不存在的变量名(前缀统一,便于将来 grep) */
19
- let ABSENT_LOG_ENV: String = "CJ_TAURI_LOG_TEST_ABSENT"
20
-
21
- @Test
22
- func testLogEnvFallsBackWhenVariableMissing(): Unit {
23
- @Expect(logEnvOr(ABSENT_LOG_ENV, "dflt"), "dflt")
24
- }
25
-
26
- @Test
27
- func testLogBudgetFallsBackWhenVariableMissing(): Unit {
28
- @Expect(logBudgetFromEnv(ABSENT_LOG_ENV, 7), 7)
29
- // 0 与负数在调用方语义里是「不轮转」,不是「用 0 当默认」——回退值原样带回
30
- @Expect(logBudgetFromEnv(ABSENT_LOG_ENV, 0), 0)
31
- }
32
-
33
- @Test
34
- func testDefaultLogPathUsesAppNameAndPlatformDir(): Unit {
35
- let p = defaultLogPath("music")
36
- // 分隔符按平台给,所以不整体比对路径;钉的是「文件名 = 应用名」、「落在 cj-tauri 目录下」
37
- @Expect(p.endsWith("music.log"), true)
38
- @Expect(p.contains("cj-tauri"), true)
39
- // 应用没给名字时走兜底:不能拼出没有主名的 ".log"
40
- @Expect(defaultLogPath("").endsWith("app.log"), true)
41
- }
42
-
43
- @Test
44
- func testDefaultLogDirIsNonEmptyAndConventional(): Unit {
45
- let d = defaultLogDir()
46
- @Expect(d.size > 0, true)
47
- @Expect(d.contains("cj-tauri"), true)
48
- // 默认预算是个能落地的值:>0 才会轮转,keep>0 才有档案可留
49
- @Expect(CJ_LOG_DEFAULT_MAX_KB > 0, true)
50
- @Expect(CJ_LOG_DEFAULT_KEEP > 0, true)
51
- }
52
-
53
- @Test
54
- func testLogPathOrDefaultPrefersEnvOverPlatformDir(): Unit {
55
- // 报障路径优先:环境变量给了路径就用它,平台惯例目录被忽略(人家不必知道目录在哪)
56
- @Expect(logPathOrDefault("/tmp/from-env.log", "music"), "/tmp/from-env.log")
57
- // 空串=未设置,才落平台惯例目录,文件名跟应用名走(与 logEnvOr 同口径)
58
- let d = logPathOrDefault("", "music")
59
- @Expect(d.endsWith("music.log"), true)
60
- @Expect(d.contains("cj-tauri"), true)
61
- @Expect(d == defaultLogPath("music"), true)
62
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import std.unittest.*
5
+ import std.unittest.testmacro.*
6
+
7
+ /*
8
+ * 日志落盘的策略层用例(RFC-003 第 5 期)。
9
+ *
10
+ * 只测**纯函数**(默认目录推导、环境变量回退、路径拼接):真的开 fd 那一段归桩自检
11
+ * (`native/tests/test_bridge_core.c` 的 `log file` 用例)与实机 E2E——单测里调桥的
12
+ * `foreign func` 会踩「子包调不了 foreign func」那条(AGENTS §4,链接期报未定义符号)。
13
+ *
14
+ * 环境变量类的用例一律用**肯定不存在**的变量名:跑测试的机器上 `CJ_TAURI_LOG` 之类
15
+ * 可能真的设着,拿它当「未设置」来断言就是一条会随机器飘的红线。
16
+ */
17
+
18
+ /** 保证不存在的变量名(前缀统一,便于将来 grep) */
19
+ let ABSENT_LOG_ENV: String = "CJ_TAURI_LOG_TEST_ABSENT"
20
+
21
+ @Test
22
+ func testLogEnvFallsBackWhenVariableMissing(): Unit {
23
+ @Expect(logEnvOr(ABSENT_LOG_ENV, "dflt"), "dflt")
24
+ }
25
+
26
+ @Test
27
+ func testLogBudgetFallsBackWhenVariableMissing(): Unit {
28
+ @Expect(logBudgetFromEnv(ABSENT_LOG_ENV, 7), 7)
29
+ // 0 与负数在调用方语义里是「不轮转」,不是「用 0 当默认」——回退值原样带回
30
+ @Expect(logBudgetFromEnv(ABSENT_LOG_ENV, 0), 0)
31
+ }
32
+
33
+ @Test
34
+ func testDefaultLogPathUsesAppNameAndPlatformDir(): Unit {
35
+ let p = defaultLogPath("music")
36
+ // 分隔符按平台给,所以不整体比对路径;钉的是「文件名 = 应用名」、「落在 cj-tauri 目录下」
37
+ @Expect(p.endsWith("music.log"), true)
38
+ @Expect(p.contains("cj-tauri"), true)
39
+ // 应用没给名字时走兜底:不能拼出没有主名的 ".log"
40
+ @Expect(defaultLogPath("").endsWith("app.log"), true)
41
+ }
42
+
43
+ @Test
44
+ func testDefaultLogDirIsNonEmptyAndConventional(): Unit {
45
+ let d = defaultLogDir()
46
+ @Expect(d.size > 0, true)
47
+ @Expect(d.contains("cj-tauri"), true)
48
+ // 默认预算是个能落地的值:>0 才会轮转,keep>0 才有档案可留
49
+ @Expect(CJ_LOG_DEFAULT_MAX_KB > 0, true)
50
+ @Expect(CJ_LOG_DEFAULT_KEEP > 0, true)
51
+ }
52
+
53
+ @Test
54
+ func testLogPathOrDefaultPrefersEnvOverPlatformDir(): Unit {
55
+ // 报障路径优先:环境变量给了路径就用它,平台惯例目录被忽略(人家不必知道目录在哪)
56
+ @Expect(logPathOrDefault("/tmp/from-env.log", "music"), "/tmp/from-env.log")
57
+ // 空串=未设置,才落平台惯例目录,文件名跟应用名走(与 logEnvOr 同口径)
58
+ let d = logPathOrDefault("", "music")
59
+ @Expect(d.endsWith("music.log"), true)
60
+ @Expect(d.contains("cj-tauri"), true)
61
+ @Expect(d == defaultLogPath("music"), true)
62
+ }
@@ -1,251 +1,251 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import cjTauri.platform.*
5
- import std.collection.*
6
- import std.unittest.*
7
- import std.unittest.testmacro.*
8
-
9
- /*
10
- * 平台契约骨架测试(RFC-002 批次 0)
11
- * 不创建窗口、不依赖宿主:TargetInfo / 特性探测 / PageSource / 宿主注册表,纯逻辑。
12
- */
13
-
14
- @Test
15
- func targetInfoDescribe(): Unit {
16
- let t = TargetInfo("Linux", "x86_64", "webkitgtk", "desktop")
17
- @Expect(t.os, "Linux")
18
- @Expect(t.describe(), "Linux/x86_64 host=webkitgtk display=desktop")
19
- }
20
-
21
- @Test
22
- func appContextFeatureProbe(): Unit {
23
- let fs = ArrayList<PlatformFeature>()
24
- fs.add(PlatformFeature.Tray)
25
- fs.add(PlatformFeature.MultiWindow)
26
- let ctx = AppContext(
27
- AppPaths("/tmp/d", "/tmp/c", "/tmp/t"),
28
- TargetInfo("Linux", "x86_64", "webkitgtk", "desktop"),
29
- false, fs)
30
- @Expect(ctx.supports(PlatformFeature.Tray), true)
31
- @Expect(ctx.supports(PlatformFeature.FilePicker), false)
32
- @Expect(ctx.dev, false)
33
- }
34
-
35
- @Test
36
- func pageSourceMatch(): Unit {
37
- let src = PageSource.Asset("ui/index.html")
38
- match (src) {
39
- case Asset(name) => @Expect(name, "ui/index.html")
40
- case _ => @Expect(false, true) // 不应到达:Asset 之外的分支
41
- }
42
- }
43
-
44
- @Test
45
- func desktopDiscoverDefaults(): Unit {
46
- let ctx = Platform.desktop("com.example.test", "x86_64")
47
- @Expect(ctx.paths.dataDir.endsWith("com.example.test"), true)
48
- @Expect(ctx.supports(PlatformFeature.Tray), true)
49
- @Expect(ctx.target.display, "desktop")
50
- }
51
-
52
- @Test
53
- func mobileDiscoverSandbox(): Unit {
54
- let ctx = Platform.mobile("/data/app/el2/base/files", "aarch64")
55
- @Expect(ctx.paths.dataDir, "/data/app/el2/base/files")
56
- @Expect(ctx.dev, false)
57
- @Expect(ctx.supports(PlatformFeature.SoftKeyboardInset), true)
58
- @Expect(ctx.supports(PlatformFeature.Tray), false)
59
- }
60
-
61
- /** 优先级最高者胜;create 返回假宿主(pickHost 会真实调用它) */
62
- class HighPrioProvider <: HostProvider {
63
- public func priority(target: TargetInfo): Int64 {
64
- return 10
65
- }
66
- public func create(label: String, target: TargetInfo): WebViewHost {
67
- return FakeHost(label)
68
- }
69
- }
70
-
71
- class LowPrioProvider <: HostProvider {
72
- public func priority(target: TargetInfo): Int64 {
73
- return 1
74
- }
75
- public func create(label: String, target: TargetInfo): WebViewHost {
76
- return FakeHost(label)
77
- }
78
- }
79
-
80
- class NullProvider <: HostProvider {
81
- public func priority(target: TargetInfo): Int64 {
82
- return 0
83
- }
84
- public func create(label: String, target: TargetInfo): WebViewHost {
85
- return FakeHost(label)
86
- }
87
- }
88
-
89
- @Test
90
- func hostRegistryPicksHighestPriority(): Unit {
91
- let before = hostProviderCount()
92
- registerHost(NullProvider())
93
- registerHost(LowPrioProvider())
94
- registerHost(HighPrioProvider())
95
- @Expect(hostProviderCount(), before + 3)
96
- let target = TargetInfo("Linux", "x86_64", "webkitgtk", "desktop")
97
- let picked = pickHost("main", target)
98
- @Expect(picked.isSome(), true)
99
- // 胜出者必须是优先级 10 的实现(create 真实执行,label 透传)
100
- @Expect(picked.getOrThrow().label(), "main")
101
- }
102
-
103
- // ---- 批次 1:PageSource → PageLoad 决策 ----
104
-
105
- @Test
106
- func resolvePageInlineAndUrl(): Unit {
107
- match (resolvePage(PageSource.Inline("<p>hi</p>"), None)) {
108
- case StartHtml(html) => @Expect(html, "<p>hi</p>")
109
- case _ => @Expect(false, true)
110
- }
111
- match (resolvePage(PageSource.Url("https://example.com"), None)) {
112
- case StartUrl(url) => @Expect(url, "https://example.com")
113
- case _ => @Expect(false, true)
114
- }
115
- // DevServer 与 Url 同路:都走 startUrl
116
- match (resolvePage(PageSource.DevServer("http://127.0.0.1:5173"), None)) {
117
- case StartUrl(url) => @Expect(url, "http://127.0.0.1:5173")
118
- case _ => @Expect(false, true)
119
- }
120
- }
121
-
122
- /** 返回固定内容的解析器 */
123
- class StubResolver <: AssetResolver {
124
- let table: HashMap<String, String>
125
- init(table: HashMap<String, String>) {
126
- this.table = table
127
- }
128
- public func readText(name: String): Option<String> {
129
- return table.get(name)
130
- }
131
- public func exists(name: String): Bool {
132
- return table.contains(name)
133
- }
134
- }
135
-
136
- @Test
137
- func resolvePageAssetViaResolver(): Unit {
138
- let t = HashMap<String, String>()
139
- t.add("ui/index.html", "<html>ok</html>")
140
- let src = resolvePage(PageSource.Asset("ui/index.html"), Some(StubResolver(t)))
141
- match (src) {
142
- case StartHtml(html) => @Expect(html, "<html>ok</html>")
143
- case _ => @Expect(false, true)
144
- }
145
- }
146
-
147
- @Test
148
- func resolvePageAssetMissingThrowsReadableError(): Unit {
149
- let t = HashMap<String, String>()
150
- var msg = ""
151
- try {
152
- let _ = resolvePage(PageSource.Asset("no/such.html"), Some(StubResolver(t)))
153
- } catch (e: Exception) {
154
- msg = e.message
155
- }
156
- @Expect(msg.contains("no/such.html"), true)
157
- }
158
-
159
- @Test
160
- func resolvePageAssetNoResolverReadsFromCwd(): Unit {
161
- // 未注入解析器:按 cwd 相对路径读盘(与模板 textOrFile 桌面行为等价)。
162
- // 测试 cwd = 仓库根,读一个确定存在的文件验证「读到了」而非白屏。
163
- let src = resolvePage(PageSource.Asset("cjpm.toml"), None)
164
- match (src) {
165
- case StartHtml(html) => @Expect(html.size > 0, true)
166
- case _ => @Expect(false, true)
167
- }
168
- }
169
-
170
- // ---- 批次 1 评审修正:devMode 自动改写(RFC §4.3)----
171
-
172
- @Test
173
- func rewriteForDevOnlyTouchesAsset(): Unit {
174
- // Asset + devUrl → DevServer
175
- match (rewriteForDev(PageSource.Asset("ui/index.html"), Some("http://127.0.0.1:5173"))) {
176
- case DevServer(url) => @Expect(url, "http://127.0.0.1:5173")
177
- case _ => @Expect(false, true)
178
- }
179
- // 显式 Inline 是业务意图,不改写
180
- match (rewriteForDev(PageSource.Inline("<p/>"), Some("http://d"))) {
181
- case Inline(html) => @Expect(html, "<p/>")
182
- case _ => @Expect(false, true)
183
- }
184
- // 无 devUrl 原样返回
185
- match (rewriteForDev(PageSource.Asset("ui/index.html"), None)) {
186
- case Asset(name) => @Expect(name, "ui/index.html")
187
- case _ => @Expect(false, true)
188
- }
189
- }
190
-
191
- // ---- 批次 2:DirectoryAssetResolver ----
192
-
193
- @Test
194
- func directoryAssetResolverReadsRepo(): Unit {
195
- let r = DirectoryAssetResolver(".")
196
- let t = r.readText("cjpm.toml")
197
- @Expect(t.isSome(), true)
198
- @Expect(t.getOrThrow().size > 0, true)
199
- @Expect(r.exists("cjpm.toml"), true)
200
- @Expect(r.readText("no/such/file.txt").isSome(), false)
201
- @Expect(r.exists("no/such/file.txt"), false)
202
- }
203
-
204
- // ---- 批次 1:guessTarget 与 resolveHost 接线 ----
205
-
206
- @Test
207
- func guessTargetDesktopFields(): Unit {
208
- let t = Platform.guessTarget()
209
- @Expect(t.display, "desktop")
210
- @Expect(t.hostKind, expectedDesktopHostKind())
211
- @Expect(t.os, expectedDesktopOs())
212
- }
213
-
214
- // 桌面两平台的宿主形态与系统名本来就不同(Windows = webview2 / Windows,Linux = webkitgtk / Linux)。
215
- // 期望值写死一套会让用例在另一个平台上恒红——本机 Windows 上这条一直是红的(写死成 Linux 那套)。
216
- @When[os == "Windows"]
217
- func expectedDesktopHostKind(): String {
218
- return "webview2"
219
- }
220
-
221
- @When[os != "Windows"]
222
- func expectedDesktopHostKind(): String {
223
- return "webkitgtk"
224
- }
225
-
226
- @When[os == "Windows"]
227
- func expectedDesktopOs(): String {
228
- return "Windows"
229
- }
230
-
231
- @When[os != "Windows"]
232
- func expectedDesktopOs(): String {
233
- return "Linux"
234
- }
235
-
236
- /** 注册表有匹配时,resolveHost 必须取注册表而不是内置宿主 */
237
- class WinProvider <: HostProvider {
238
- public func priority(target: TargetInfo): Int64 {
239
- return 5
240
- }
241
- public func create(label: String, target: TargetInfo): WebViewHost {
242
- return FakeHost(label)
243
- }
244
- }
245
-
246
- @Test
247
- func resolveHostPrefersRegistryOverBuiltin(): Unit {
248
- registerHost(WinProvider())
249
- let h = resolveHost("registry-win")
250
- @Expect(h.label(), "registry-win")
251
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import cjTauri.platform.*
5
+ import std.collection.*
6
+ import std.unittest.*
7
+ import std.unittest.testmacro.*
8
+
9
+ /*
10
+ * 平台契约骨架测试(RFC-002 批次 0)
11
+ * 不创建窗口、不依赖宿主:TargetInfo / 特性探测 / PageSource / 宿主注册表,纯逻辑。
12
+ */
13
+
14
+ @Test
15
+ func targetInfoDescribe(): Unit {
16
+ let t = TargetInfo("Linux", "x86_64", "webkitgtk", "desktop")
17
+ @Expect(t.os, "Linux")
18
+ @Expect(t.describe(), "Linux/x86_64 host=webkitgtk display=desktop")
19
+ }
20
+
21
+ @Test
22
+ func appContextFeatureProbe(): Unit {
23
+ let fs = ArrayList<PlatformFeature>()
24
+ fs.add(PlatformFeature.Tray)
25
+ fs.add(PlatformFeature.MultiWindow)
26
+ let ctx = AppContext(
27
+ AppPaths("/tmp/d", "/tmp/c", "/tmp/t"),
28
+ TargetInfo("Linux", "x86_64", "webkitgtk", "desktop"),
29
+ false, fs)
30
+ @Expect(ctx.supports(PlatformFeature.Tray), true)
31
+ @Expect(ctx.supports(PlatformFeature.FilePicker), false)
32
+ @Expect(ctx.dev, false)
33
+ }
34
+
35
+ @Test
36
+ func pageSourceMatch(): Unit {
37
+ let src = PageSource.Asset("ui/index.html")
38
+ match (src) {
39
+ case Asset(name) => @Expect(name, "ui/index.html")
40
+ case _ => @Expect(false, true) // 不应到达:Asset 之外的分支
41
+ }
42
+ }
43
+
44
+ @Test
45
+ func desktopDiscoverDefaults(): Unit {
46
+ let ctx = Platform.desktop("com.example.test", "x86_64")
47
+ @Expect(ctx.paths.dataDir.endsWith("com.example.test"), true)
48
+ @Expect(ctx.supports(PlatformFeature.Tray), true)
49
+ @Expect(ctx.target.display, "desktop")
50
+ }
51
+
52
+ @Test
53
+ func mobileDiscoverSandbox(): Unit {
54
+ let ctx = Platform.mobile("/data/app/el2/base/files", "aarch64")
55
+ @Expect(ctx.paths.dataDir, "/data/app/el2/base/files")
56
+ @Expect(ctx.dev, false)
57
+ @Expect(ctx.supports(PlatformFeature.SoftKeyboardInset), true)
58
+ @Expect(ctx.supports(PlatformFeature.Tray), false)
59
+ }
60
+
61
+ /** 优先级最高者胜;create 返回假宿主(pickHost 会真实调用它) */
62
+ class HighPrioProvider <: HostProvider {
63
+ public func priority(target: TargetInfo): Int64 {
64
+ return 10
65
+ }
66
+ public func create(label: String, target: TargetInfo): WebViewHost {
67
+ return FakeHost(label)
68
+ }
69
+ }
70
+
71
+ class LowPrioProvider <: HostProvider {
72
+ public func priority(target: TargetInfo): Int64 {
73
+ return 1
74
+ }
75
+ public func create(label: String, target: TargetInfo): WebViewHost {
76
+ return FakeHost(label)
77
+ }
78
+ }
79
+
80
+ class NullProvider <: HostProvider {
81
+ public func priority(target: TargetInfo): Int64 {
82
+ return 0
83
+ }
84
+ public func create(label: String, target: TargetInfo): WebViewHost {
85
+ return FakeHost(label)
86
+ }
87
+ }
88
+
89
+ @Test
90
+ func hostRegistryPicksHighestPriority(): Unit {
91
+ let before = hostProviderCount()
92
+ registerHost(NullProvider())
93
+ registerHost(LowPrioProvider())
94
+ registerHost(HighPrioProvider())
95
+ @Expect(hostProviderCount(), before + 3)
96
+ let target = TargetInfo("Linux", "x86_64", "webkitgtk", "desktop")
97
+ let picked = pickHost("main", target)
98
+ @Expect(picked.isSome(), true)
99
+ // 胜出者必须是优先级 10 的实现(create 真实执行,label 透传)
100
+ @Expect(picked.getOrThrow().label(), "main")
101
+ }
102
+
103
+ // ---- 批次 1:PageSource → PageLoad 决策 ----
104
+
105
+ @Test
106
+ func resolvePageInlineAndUrl(): Unit {
107
+ match (resolvePage(PageSource.Inline("<p>hi</p>"), None)) {
108
+ case StartHtml(html) => @Expect(html, "<p>hi</p>")
109
+ case _ => @Expect(false, true)
110
+ }
111
+ match (resolvePage(PageSource.Url("https://example.com"), None)) {
112
+ case StartUrl(url) => @Expect(url, "https://example.com")
113
+ case _ => @Expect(false, true)
114
+ }
115
+ // DevServer 与 Url 同路:都走 startUrl
116
+ match (resolvePage(PageSource.DevServer("http://127.0.0.1:5173"), None)) {
117
+ case StartUrl(url) => @Expect(url, "http://127.0.0.1:5173")
118
+ case _ => @Expect(false, true)
119
+ }
120
+ }
121
+
122
+ /** 返回固定内容的解析器 */
123
+ class StubResolver <: AssetResolver {
124
+ let table: HashMap<String, String>
125
+ init(table: HashMap<String, String>) {
126
+ this.table = table
127
+ }
128
+ public func readText(name: String): Option<String> {
129
+ return table.get(name)
130
+ }
131
+ public func exists(name: String): Bool {
132
+ return table.contains(name)
133
+ }
134
+ }
135
+
136
+ @Test
137
+ func resolvePageAssetViaResolver(): Unit {
138
+ let t = HashMap<String, String>()
139
+ t.add("ui/index.html", "<html>ok</html>")
140
+ let src = resolvePage(PageSource.Asset("ui/index.html"), Some(StubResolver(t)))
141
+ match (src) {
142
+ case StartHtml(html) => @Expect(html, "<html>ok</html>")
143
+ case _ => @Expect(false, true)
144
+ }
145
+ }
146
+
147
+ @Test
148
+ func resolvePageAssetMissingThrowsReadableError(): Unit {
149
+ let t = HashMap<String, String>()
150
+ var msg = ""
151
+ try {
152
+ let _ = resolvePage(PageSource.Asset("no/such.html"), Some(StubResolver(t)))
153
+ } catch (e: Exception) {
154
+ msg = e.message
155
+ }
156
+ @Expect(msg.contains("no/such.html"), true)
157
+ }
158
+
159
+ @Test
160
+ func resolvePageAssetNoResolverReadsFromCwd(): Unit {
161
+ // 未注入解析器:按 cwd 相对路径读盘(与模板 textOrFile 桌面行为等价)。
162
+ // 测试 cwd = 仓库根,读一个确定存在的文件验证「读到了」而非白屏。
163
+ let src = resolvePage(PageSource.Asset("cjpm.toml"), None)
164
+ match (src) {
165
+ case StartHtml(html) => @Expect(html.size > 0, true)
166
+ case _ => @Expect(false, true)
167
+ }
168
+ }
169
+
170
+ // ---- 批次 1 评审修正:devMode 自动改写(RFC §4.3)----
171
+
172
+ @Test
173
+ func rewriteForDevOnlyTouchesAsset(): Unit {
174
+ // Asset + devUrl → DevServer
175
+ match (rewriteForDev(PageSource.Asset("ui/index.html"), Some("http://127.0.0.1:5173"))) {
176
+ case DevServer(url) => @Expect(url, "http://127.0.0.1:5173")
177
+ case _ => @Expect(false, true)
178
+ }
179
+ // 显式 Inline 是业务意图,不改写
180
+ match (rewriteForDev(PageSource.Inline("<p/>"), Some("http://d"))) {
181
+ case Inline(html) => @Expect(html, "<p/>")
182
+ case _ => @Expect(false, true)
183
+ }
184
+ // 无 devUrl 原样返回
185
+ match (rewriteForDev(PageSource.Asset("ui/index.html"), None)) {
186
+ case Asset(name) => @Expect(name, "ui/index.html")
187
+ case _ => @Expect(false, true)
188
+ }
189
+ }
190
+
191
+ // ---- 批次 2:DirectoryAssetResolver ----
192
+
193
+ @Test
194
+ func directoryAssetResolverReadsRepo(): Unit {
195
+ let r = DirectoryAssetResolver(".")
196
+ let t = r.readText("cjpm.toml")
197
+ @Expect(t.isSome(), true)
198
+ @Expect(t.getOrThrow().size > 0, true)
199
+ @Expect(r.exists("cjpm.toml"), true)
200
+ @Expect(r.readText("no/such/file.txt").isSome(), false)
201
+ @Expect(r.exists("no/such/file.txt"), false)
202
+ }
203
+
204
+ // ---- 批次 1:guessTarget 与 resolveHost 接线 ----
205
+
206
+ @Test
207
+ func guessTargetDesktopFields(): Unit {
208
+ let t = Platform.guessTarget()
209
+ @Expect(t.display, "desktop")
210
+ @Expect(t.hostKind, expectedDesktopHostKind())
211
+ @Expect(t.os, expectedDesktopOs())
212
+ }
213
+
214
+ // 桌面两平台的宿主形态与系统名本来就不同(Windows = webview2 / Windows,Linux = webkitgtk / Linux)。
215
+ // 期望值写死一套会让用例在另一个平台上恒红——本机 Windows 上这条一直是红的(写死成 Linux 那套)。
216
+ @When[os == "Windows"]
217
+ func expectedDesktopHostKind(): String {
218
+ return "webview2"
219
+ }
220
+
221
+ @When[os != "Windows"]
222
+ func expectedDesktopHostKind(): String {
223
+ return "webkitgtk"
224
+ }
225
+
226
+ @When[os == "Windows"]
227
+ func expectedDesktopOs(): String {
228
+ return "Windows"
229
+ }
230
+
231
+ @When[os != "Windows"]
232
+ func expectedDesktopOs(): String {
233
+ return "Linux"
234
+ }
235
+
236
+ /** 注册表有匹配时,resolveHost 必须取注册表而不是内置宿主 */
237
+ class WinProvider <: HostProvider {
238
+ public func priority(target: TargetInfo): Int64 {
239
+ return 5
240
+ }
241
+ public func create(label: String, target: TargetInfo): WebViewHost {
242
+ return FakeHost(label)
243
+ }
244
+ }
245
+
246
+ @Test
247
+ func resolveHostPrefersRegistryOverBuiltin(): Unit {
248
+ registerHost(WinProvider())
249
+ let h = resolveHost("registry-win")
250
+ @Expect(h.label(), "registry-win")
251
+ }