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.
Files changed (157) hide show
  1. package/README.md +206 -139
  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 +643 -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 +638 -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 +3745 -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/src/api_system.cj +67 -67
  77. package/src/app.cj +335 -969
  78. package/src/app_events.cj +166 -0
  79. package/src/app_host_ops.cj +287 -0
  80. package/src/app_run.cj +283 -0
  81. package/src/app_spec.cj +76 -76
  82. package/src/arkweb_menu.cj +216 -0
  83. package/src/arkweb_script.cj +142 -142
  84. package/src/capability.cj +409 -409
  85. package/src/clipboard_bridge.cj +178 -0
  86. package/src/dialog_spec.cj +79 -0
  87. package/src/dragdrop.cj +26 -26
  88. package/src/embedded_assets.cj +98 -98
  89. package/src/host.cj +752 -607
  90. package/src/host_arkweb.cj +924 -778
  91. package/src/host_webkit.cj +663 -570
  92. package/src/host_webview2.cj +663 -570
  93. package/src/hotkey.cj +66 -66
  94. package/src/ipc_hub.cj +401 -337
  95. package/src/ipc_message.cj +286 -286
  96. package/src/launch.cj +62 -62
  97. package/src/logging.cj +127 -127
  98. package/src/menu.cj +197 -197
  99. package/src/platform/platform_app_context.cj +77 -77
  100. package/src/platform/platform_assets.cj +33 -33
  101. package/src/platform/platform_discover.cj +176 -176
  102. package/src/platform/platform_env.cj +29 -29
  103. package/src/platform/platform_feature.cj +31 -31
  104. package/src/platform/platform_page_source.cj +87 -87
  105. package/src/platform/platform_target_info.cj +32 -32
  106. package/src/platform_host_provider.cj +87 -87
  107. package/src/plugin.cj +333 -333
  108. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  109. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  110. package/src/plugins/fs/plugin_fs.cj +105 -1
  111. package/src/plugins/http/http_backend.cj +131 -0
  112. package/src/plugins/http/http_desktop.cj +195 -0
  113. package/src/plugins/http/http_ohos.cj +204 -0
  114. package/src/plugins/http/plugin_http.cj +310 -459
  115. package/src/plugins/log/plugin_log.cj +105 -105
  116. package/src/plugins/menu/plugin_menu.cj +158 -158
  117. package/src/plugins/notification/plugin_notification.cj +154 -154
  118. package/src/plugins/serial/plugin_serial.cj +458 -454
  119. package/src/plugins/serial/serial_bridge.cj +194 -194
  120. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  121. package/src/plugins/serial/serial_ohos.cj +529 -0
  122. package/src/plugins/shell/plugin_shell.cj +258 -258
  123. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  124. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  125. package/src/plugins/ws/plugin_ws.cj +488 -470
  126. package/src/process_hidden.cj +174 -174
  127. package/src/scope.cj +391 -391
  128. package/src/shell_event.cj +69 -69
  129. package/src/tests/app_spec_test.cj +130 -127
  130. package/src/tests/capability_test.cj +243 -243
  131. package/src/tests/dialog_spec_test.cj +73 -0
  132. package/src/tests/fake_host.cj +201 -201
  133. package/src/tests/ipc_hub_test.cj +563 -487
  134. package/src/tests/logging_test.cj +62 -62
  135. package/src/tests/platform_contract_test.cj +264 -251
  136. package/src/tests/plugin_clipboard_test.cj +222 -222
  137. package/src/tests/plugin_dialog_test.cj +237 -237
  138. package/src/tests/plugin_fs_test.cj +207 -0
  139. package/src/tests/plugin_http_test.cj +124 -124
  140. package/src/tests/plugin_notification_test.cj +205 -205
  141. package/src/tests/plugin_serial_test.cj +48 -0
  142. package/src/tests/plugin_test.cj +605 -585
  143. package/src/tests/plugin_ws_test.cj +40 -40
  144. package/src/tests/process_hidden_test.cj +129 -129
  145. package/src/tests/scope_test.cj +350 -350
  146. package/src/tests/system_host_test.cj +99 -81
  147. package/src/tests/urlutil_test.cj +53 -53
  148. package/src/tests/version_test.cj +75 -75
  149. package/src/tests/window_registry_test.cj +123 -123
  150. package/src/tray.cj +90 -90
  151. package/src/urlutil.cj +159 -159
  152. package/src/version.cj +44 -44
  153. package/src/window.cj +51 -51
  154. package/src/window_registry.cj +116 -116
  155. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  156. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  157. 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,264 @@
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
+ /** 经函数返回:调用点看到的是 PageSource 全类型,match 的穷尽性检查才不会与流分析打架 */
36
+ func assetPageSource(name: String): PageSource {
37
+ return PageSource.Asset(name)
38
+ }
39
+
40
+ @Test
41
+ func pageSourceMatch(): Unit {
42
+ let src = assetPageSource("ui/index.html")
43
+ match (src) {
44
+ case Asset(name) => @Expect(name, "ui/index.html")
45
+ case _ => @Expect(false, true) // 不应到达:工厂只造 Asset
46
+ }
47
+ }
48
+
49
+ @Test
50
+ func desktopDiscoverDefaults(): Unit {
51
+ let ctx = Platform.desktop("com.example.test", "x86_64")
52
+ @Expect(ctx.paths.dataDir.endsWith("com.example.test"), true)
53
+ @Expect(ctx.supports(PlatformFeature.Tray), true)
54
+ @Expect(ctx.target.display, "desktop")
55
+ }
56
+
57
+ @Test
58
+ func mobileDiscoverSandbox(): Unit {
59
+ let ctx = Platform.mobile("/data/app/el2/base/files", "aarch64")
60
+ @Expect(ctx.paths.dataDir, "/data/app/el2/base/files")
61
+ @Expect(ctx.dev, false)
62
+ @Expect(ctx.supports(PlatformFeature.SoftKeyboardInset), true)
63
+ @Expect(ctx.supports(PlatformFeature.Tray), false)
64
+ }
65
+
66
+ /** 优先级最高者胜;create 返回假宿主(pickHost 会真实调用它) */
67
+ class HighPrioProvider <: HostProvider {
68
+ public func priority(target: TargetInfo): Int64 {
69
+ let _ = target
70
+ return 10
71
+ }
72
+ public func create(label: String, target: TargetInfo): WebViewHost {
73
+ let _ = target
74
+ return FakeHost(label)
75
+ }
76
+ }
77
+
78
+ class LowPrioProvider <: HostProvider {
79
+ public func priority(target: TargetInfo): Int64 {
80
+ let _ = target
81
+ return 1
82
+ }
83
+ public func create(label: String, target: TargetInfo): WebViewHost {
84
+ let _ = target
85
+ return FakeHost(label)
86
+ }
87
+ }
88
+
89
+ class NullProvider <: HostProvider {
90
+ public func priority(target: TargetInfo): Int64 {
91
+ let _ = target
92
+ return 0
93
+ }
94
+ public func create(label: String, target: TargetInfo): WebViewHost {
95
+ let _ = target
96
+ return FakeHost(label)
97
+ }
98
+ }
99
+
100
+ @Test
101
+ func hostRegistryPicksHighestPriority(): Unit {
102
+ let before = hostProviderCount()
103
+ registerHost(NullProvider())
104
+ registerHost(LowPrioProvider())
105
+ registerHost(HighPrioProvider())
106
+ @Expect(hostProviderCount(), before + 3)
107
+ let target = TargetInfo("Linux", "x86_64", "webkitgtk", "desktop")
108
+ let picked = pickHost("main", target)
109
+ @Expect(picked.isSome(), true)
110
+ // 胜出者必须是优先级 10 的实现(create 真实执行,label 透传)
111
+ @Expect(picked.getOrThrow().label(), "main")
112
+ }
113
+
114
+ // ---- 批次 1:PageSource → PageLoad 决策 ----
115
+
116
+ @Test
117
+ func resolvePageInlineAndUrl(): Unit {
118
+ match (resolvePage(PageSource.Inline("<p>hi</p>"), None)) {
119
+ case StartHtml(html) => @Expect(html, "<p>hi</p>")
120
+ case _ => @Expect(false, true)
121
+ }
122
+ match (resolvePage(PageSource.Url("https://example.com"), None)) {
123
+ case StartUrl(url) => @Expect(url, "https://example.com")
124
+ case _ => @Expect(false, true)
125
+ }
126
+ // DevServer 与 Url 同路:都走 startUrl
127
+ match (resolvePage(PageSource.DevServer("http://127.0.0.1:5173"), None)) {
128
+ case StartUrl(url) => @Expect(url, "http://127.0.0.1:5173")
129
+ case _ => @Expect(false, true)
130
+ }
131
+ }
132
+
133
+ /** 返回固定内容的解析器 */
134
+ class StubResolver <: AssetResolver {
135
+ let table: HashMap<String, String>
136
+ init(table: HashMap<String, String>) {
137
+ this.table = table
138
+ }
139
+ public func readText(name: String): Option<String> {
140
+ return table.get(name)
141
+ }
142
+ public func exists(name: String): Bool {
143
+ return table.contains(name)
144
+ }
145
+ }
146
+
147
+ @Test
148
+ func resolvePageAssetViaResolver(): Unit {
149
+ let t = HashMap<String, String>()
150
+ t.add("ui/index.html", "<html>ok</html>")
151
+ let src = resolvePage(PageSource.Asset("ui/index.html"), Some(StubResolver(t)))
152
+ match (src) {
153
+ case StartHtml(html) => @Expect(html, "<html>ok</html>")
154
+ case _ => @Expect(false, true)
155
+ }
156
+ }
157
+
158
+ @Test
159
+ func resolvePageAssetMissingThrowsReadableError(): Unit {
160
+ let t = HashMap<String, String>()
161
+ var msg = ""
162
+ try {
163
+ let _ = resolvePage(PageSource.Asset("no/such.html"), Some(StubResolver(t)))
164
+ } catch (e: Exception) {
165
+ msg = e.message
166
+ }
167
+ @Expect(msg.contains("no/such.html"), true)
168
+ }
169
+
170
+ @Test
171
+ func resolvePageAssetNoResolverReadsFromCwd(): Unit {
172
+ // 未注入解析器:按 cwd 相对路径读盘(与模板 textOrFile 桌面行为等价)。
173
+ // 测试 cwd = 仓库根,读一个确定存在的文件验证「读到了」而非白屏。
174
+ let src = resolvePage(PageSource.Asset("cjpm.toml"), None)
175
+ match (src) {
176
+ case StartHtml(html) => @Expect(html.size > 0, true)
177
+ case _ => @Expect(false, true)
178
+ }
179
+ }
180
+
181
+ // ---- 批次 1 评审修正:devMode 自动改写(RFC §4.3)----
182
+
183
+ @Test
184
+ func rewriteForDevOnlyTouchesAsset(): Unit {
185
+ // Asset + devUrl → DevServer
186
+ match (rewriteForDev(PageSource.Asset("ui/index.html"), Some("http://127.0.0.1:5173"))) {
187
+ case DevServer(url) => @Expect(url, "http://127.0.0.1:5173")
188
+ case _ => @Expect(false, true)
189
+ }
190
+ // 显式 Inline 是业务意图,不改写
191
+ match (rewriteForDev(PageSource.Inline("<p/>"), Some("http://d"))) {
192
+ case Inline(html) => @Expect(html, "<p/>")
193
+ case _ => @Expect(false, true)
194
+ }
195
+ // 无 devUrl 原样返回
196
+ match (rewriteForDev(PageSource.Asset("ui/index.html"), None)) {
197
+ case Asset(name) => @Expect(name, "ui/index.html")
198
+ case _ => @Expect(false, true)
199
+ }
200
+ }
201
+
202
+ // ---- 批次 2:DirectoryAssetResolver ----
203
+
204
+ @Test
205
+ func directoryAssetResolverReadsRepo(): Unit {
206
+ let r = DirectoryAssetResolver(".")
207
+ let t = r.readText("cjpm.toml")
208
+ @Expect(t.isSome(), true)
209
+ @Expect(t.getOrThrow().size > 0, true)
210
+ @Expect(r.exists("cjpm.toml"), true)
211
+ @Expect(r.readText("no/such/file.txt").isSome(), false)
212
+ @Expect(r.exists("no/such/file.txt"), false)
213
+ }
214
+
215
+ // ---- 批次 1:guessTarget 与 resolveHost 接线 ----
216
+
217
+ @Test
218
+ func guessTargetDesktopFields(): Unit {
219
+ let t = Platform.guessTarget()
220
+ @Expect(t.display, "desktop")
221
+ @Expect(t.hostKind, expectedDesktopHostKind())
222
+ @Expect(t.os, expectedDesktopOs())
223
+ }
224
+
225
+ // 桌面两平台的宿主形态与系统名本来就不同(Windows = webview2 / Windows,Linux = webkitgtk / Linux)。
226
+ // 期望值写死一套会让用例在另一个平台上恒红——本机 Windows 上这条一直是红的(写死成 Linux 那套)。
227
+ @When[os == "Windows"]
228
+ func expectedDesktopHostKind(): String {
229
+ return "webview2"
230
+ }
231
+
232
+ @When[os != "Windows"]
233
+ func expectedDesktopHostKind(): String {
234
+ return "webkitgtk"
235
+ }
236
+
237
+ @When[os == "Windows"]
238
+ func expectedDesktopOs(): String {
239
+ return "Windows"
240
+ }
241
+
242
+ @When[os != "Windows"]
243
+ func expectedDesktopOs(): String {
244
+ return "Linux"
245
+ }
246
+
247
+ /** 注册表有匹配时,resolveHost 必须取注册表而不是内置宿主 */
248
+ class WinProvider <: HostProvider {
249
+ public func priority(target: TargetInfo): Int64 {
250
+ let _ = target
251
+ return 5
252
+ }
253
+ public func create(label: String, target: TargetInfo): WebViewHost {
254
+ let _ = target
255
+ return FakeHost(label)
256
+ }
257
+ }
258
+
259
+ @Test
260
+ func resolveHostPrefersRegistryOverBuiltin(): Unit {
261
+ registerHost(WinProvider())
262
+ let h = resolveHost("registry-win")
263
+ @Expect(h.label(), "registry-win")
264
+ }