jac-client 0.2.8__py3-none-any.whl → 0.2.11__py3-none-any.whl

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 (119) hide show
  1. jac_client/examples/all-in-one/button.jac +4 -3
  2. jac_client/examples/all-in-one/components/CategoryFilter.jac +36 -24
  3. jac_client/examples/all-in-one/components/Header.jac +12 -8
  4. jac_client/examples/all-in-one/components/ProfitOverview.jac +49 -35
  5. jac_client/examples/all-in-one/components/Summary.jac +59 -36
  6. jac_client/examples/all-in-one/components/TransactionForm.jac +142 -112
  7. jac_client/examples/all-in-one/components/TransactionItem.jac +37 -30
  8. jac_client/examples/all-in-one/components/TransactionList.jac +33 -26
  9. jac_client/examples/all-in-one/components/button.jac +4 -3
  10. jac_client/examples/all-in-one/components/navigation.jac +111 -117
  11. jac_client/examples/all-in-one/constants/categories.jac +23 -24
  12. jac_client/examples/all-in-one/constants/clients.jac +7 -8
  13. jac_client/examples/all-in-one/context/BudgetContext.jac +9 -6
  14. jac_client/examples/all-in-one/hooks/useBudget.jac +18 -12
  15. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +14 -13
  16. jac_client/examples/all-in-one/main.jac +542 -0
  17. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +26 -12
  18. jac_client/examples/all-in-one/pages/FeaturesTest.jac +43 -12
  19. jac_client/examples/all-in-one/pages/LandingPage.jac +113 -90
  20. jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +65 -0
  21. jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +675 -0
  22. jac_client/examples/all-in-one/pages/loginPage.jac +114 -119
  23. jac_client/examples/all-in-one/pages/nestedDemo.jac +44 -51
  24. jac_client/examples/all-in-one/pages/notFound.jac +15 -21
  25. jac_client/examples/all-in-one/pages/signupPage.jac +113 -119
  26. jac_client/examples/all-in-one/utils/formatters.jac +5 -8
  27. jac_client/examples/asset-serving/css-with-image/main.jac +92 -0
  28. jac_client/examples/asset-serving/image-asset/main.jac +56 -0
  29. jac_client/examples/asset-serving/import-alias/main.jac +109 -0
  30. jac_client/examples/basic/main.jac +23 -0
  31. jac_client/examples/basic-auth/main.jac +363 -0
  32. jac_client/examples/basic-auth-with-router/main.jac +451 -0
  33. jac_client/examples/basic-full-stack/main.jac +362 -0
  34. jac_client/examples/css-styling/js-styling/main.jac +63 -0
  35. jac_client/examples/css-styling/material-ui/main.jac +122 -0
  36. jac_client/examples/css-styling/pure-css/main.jac +55 -0
  37. jac_client/examples/css-styling/sass-example/main.jac +55 -0
  38. jac_client/examples/css-styling/styled-components/main.jac +62 -0
  39. jac_client/examples/css-styling/tailwind-example/main.jac +74 -0
  40. jac_client/examples/full-stack-with-auth/main.jac +696 -0
  41. jac_client/examples/little-x/main.jac +681 -0
  42. jac_client/examples/little-x/src/submit-button.jac +15 -14
  43. jac_client/examples/nested-folders/nested-advance/main.jac +26 -0
  44. jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +4 -6
  45. jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +9 -13
  46. jac_client/examples/nested-folders/nested-advance/src/level1/Card.jac +29 -32
  47. jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +12 -18
  48. jac_client/examples/nested-folders/nested-basic/{src/app.jac → main.jac} +7 -5
  49. jac_client/examples/nested-folders/nested-basic/src/button.jac +4 -3
  50. jac_client/examples/nested-folders/nested-basic/src/components/button.jac +4 -3
  51. jac_client/examples/ts-support/main.jac +35 -0
  52. jac_client/examples/with-router/main.jac +286 -0
  53. jac_client/plugin/cli.jac +491 -411
  54. jac_client/plugin/client.jac +25 -0
  55. jac_client/plugin/client_runtime.cl.jac +10 -4
  56. jac_client/plugin/impl/client.impl.jac +96 -55
  57. jac_client/plugin/impl/client_runtime.impl.jac +155 -1
  58. jac_client/plugin/plugin_config.jac +211 -29
  59. jac_client/plugin/src/__init__.jac +0 -2
  60. jac_client/plugin/src/compiler.jac +0 -1
  61. jac_client/plugin/src/config_loader.jac +1 -0
  62. jac_client/plugin/src/desktop_config.jac +31 -0
  63. jac_client/plugin/src/impl/compiler.impl.jac +49 -17
  64. jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
  65. jac_client/plugin/src/impl/desktop_config.impl.jac +191 -0
  66. jac_client/plugin/src/impl/jac_to_js.impl.jac +5 -1
  67. jac_client/plugin/src/impl/package_installer.impl.jac +20 -20
  68. jac_client/plugin/src/impl/vite_bundler.impl.jac +191 -64
  69. jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
  70. jac_client/plugin/src/targets/desktop_target.jac +37 -0
  71. jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2347 -0
  72. jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
  73. jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
  74. jac_client/plugin/src/targets/register.jac +21 -0
  75. jac_client/plugin/src/targets/registry.jac +87 -0
  76. jac_client/plugin/src/targets/web_target.jac +35 -0
  77. jac_client/plugin/src/vite_bundler.jac +6 -0
  78. jac_client/plugin/utils/__init__.jac +3 -0
  79. jac_client/plugin/utils/bun_installer.jac +16 -0
  80. jac_client/plugin/utils/impl/bun_installer.impl.jac +99 -0
  81. jac_client/templates/client.jacpack +72 -0
  82. jac_client/templates/fullstack.jacpack +61 -0
  83. jac_client/tests/conftest.py +103 -47
  84. jac_client/tests/fixtures/spawn_test/app.jac +49 -52
  85. jac_client/tests/fixtures/with-ts/app.jac +27 -27
  86. jac_client/tests/test_cli.py +182 -71
  87. jac_client/tests/test_e2e.py +232 -0
  88. jac_client/tests/test_helpers.py +58 -0
  89. jac_client/tests/test_it.py +91 -135
  90. jac_client/tests/test_it_desktop.py +891 -0
  91. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/METADATA +6 -6
  92. jac_client-0.2.11.dist-info/RECORD +113 -0
  93. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/WHEEL +1 -1
  94. jac_client/examples/all-in-one/app.jac +0 -573
  95. jac_client/examples/all-in-one/pages/BudgetPlanner.cl.jac +0 -70
  96. jac_client/examples/all-in-one/pages/FeaturesTest.cl.jac +0 -552
  97. jac_client/examples/asset-serving/css-with-image/src/app.jac +0 -88
  98. jac_client/examples/asset-serving/image-asset/src/app.jac +0 -55
  99. jac_client/examples/asset-serving/import-alias/src/app.jac +0 -111
  100. jac_client/examples/basic/src/app.jac +0 -21
  101. jac_client/examples/basic-auth/src/app.jac +0 -371
  102. jac_client/examples/basic-auth-with-router/src/app.jac +0 -464
  103. jac_client/examples/basic-full-stack/src/app.jac +0 -359
  104. jac_client/examples/css-styling/js-styling/src/app.jac +0 -84
  105. jac_client/examples/css-styling/material-ui/src/app.jac +0 -122
  106. jac_client/examples/css-styling/pure-css/src/app.jac +0 -64
  107. jac_client/examples/css-styling/sass-example/src/app.jac +0 -64
  108. jac_client/examples/css-styling/styled-components/src/app.jac +0 -71
  109. jac_client/examples/css-styling/tailwind-example/src/app.jac +0 -63
  110. jac_client/examples/full-stack-with-auth/src/app.jac +0 -722
  111. jac_client/examples/little-x/src/app.jac +0 -719
  112. jac_client/examples/nested-folders/nested-advance/src/app.jac +0 -35
  113. jac_client/examples/ts-support/src/app.jac +0 -35
  114. jac_client/examples/with-router/src/app.jac +0 -323
  115. jac_client/plugin/src/babel_processor.jac +0 -18
  116. jac_client/plugin/src/impl/babel_processor.impl.jac +0 -89
  117. jac_client-0.2.8.dist-info/RECORD +0 -97
  118. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/entry_points.txt +0 -0
  119. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,891 @@
1
+ """Integration tests for desktop target (Tauri) functionality.
2
+
3
+ These tests validate the desktop target setup, build, and sidecar functionality
4
+ as documented in FRESH_PROJECT_TESTING_GUIDE.md.
5
+
6
+ Tests are designed to:
7
+ 1. Skip gracefully if Rust/Tauri toolchain is not installed
8
+ 2. Test setup command (scaffolding) without requiring full build
9
+ 3. Optionally test build if all dependencies are available
10
+ 4. Test sidecar functionality in isolation
11
+
12
+ Note: Some tests call the target's methods directly via Python imports to avoid
13
+ CLI plugin registration conflicts that can occur in monorepo test environments.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import gc
19
+ import json
20
+ import os
21
+ import signal
22
+ import subprocess
23
+ import sys
24
+ import tempfile
25
+ import time
26
+ from pathlib import Path
27
+ from subprocess import PIPE, Popen, run
28
+ from urllib.error import HTTPError, URLError
29
+ from urllib.request import Request, urlopen
30
+
31
+ import pytest
32
+
33
+ from .test_helpers import (
34
+ get_env_with_npm,
35
+ get_free_port,
36
+ get_jac_command,
37
+ wait_for_port,
38
+ )
39
+
40
+
41
+ def is_jac_setup_available() -> bool:
42
+ """Check if the 'jac setup' CLI command is available."""
43
+ jac_cmd = get_jac_command()
44
+ result = run(
45
+ [*jac_cmd, "--help"],
46
+ capture_output=True,
47
+ text=True,
48
+ timeout=30,
49
+ )
50
+ return "setup" in result.stdout
51
+
52
+
53
+ def run_jac_setup_desktop(project_dir: Path) -> tuple[int, str, str]:
54
+ """Run jac setup desktop command and return (returncode, stdout, stderr).
55
+
56
+ Returns (1, "", "CLI not available") if the CLI command is not available.
57
+ """
58
+ jac_cmd = get_jac_command()
59
+ env = get_env_with_npm()
60
+
61
+ # Try the CLI command
62
+ result = run(
63
+ [*jac_cmd, "setup", "desktop"],
64
+ cwd=project_dir,
65
+ capture_output=True,
66
+ text=True,
67
+ env=env,
68
+ timeout=120,
69
+ )
70
+
71
+ # If CLI command doesn't exist, return a special error
72
+ if result.returncode != 0 and "invalid choice: 'setup'" in result.stderr:
73
+ return (1, "", "CLI_NOT_AVAILABLE: 'jac setup' command not registered")
74
+
75
+ return (result.returncode, result.stdout, result.stderr)
76
+
77
+
78
+ # Skip marker for tests requiring jac setup CLI
79
+ requires_jac_setup_cli = pytest.mark.skipif(
80
+ not is_jac_setup_available(),
81
+ reason="'jac setup' CLI command not available (plugin not fully loaded)",
82
+ )
83
+
84
+
85
+ # =============================================================================
86
+ # Fixtures and Helpers
87
+ # =============================================================================
88
+
89
+
90
+ def is_rust_installed() -> bool:
91
+ """Check if Rust toolchain is installed."""
92
+ try:
93
+ result = run(["cargo", "--version"], capture_output=True, timeout=10)
94
+ return result.returncode == 0
95
+ except (FileNotFoundError, subprocess.TimeoutExpired):
96
+ return False
97
+
98
+
99
+ def is_tauri_cli_installed() -> bool:
100
+ """Check if Tauri CLI is installed."""
101
+ try:
102
+ result = run(["cargo", "tauri", "--version"], capture_output=True, timeout=10)
103
+ return result.returncode == 0
104
+ except (FileNotFoundError, subprocess.TimeoutExpired):
105
+ return False
106
+
107
+
108
+ def is_pyinstaller_installed() -> bool:
109
+ """Check if PyInstaller is installed."""
110
+ try:
111
+ result = run(["pyinstaller", "--version"], capture_output=True, timeout=10)
112
+ return result.returncode == 0
113
+ except (FileNotFoundError, subprocess.TimeoutExpired):
114
+ return False
115
+
116
+
117
+ def has_display() -> bool:
118
+ """Check if a display is available (for GUI tests)."""
119
+ # Check common environment variables indicating a display
120
+ if os.environ.get("DISPLAY"):
121
+ return True
122
+ if os.environ.get("WAYLAND_DISPLAY"):
123
+ return True
124
+ # CI environments typically don't have displays
125
+ if os.environ.get("CI"):
126
+ return False
127
+ return False
128
+
129
+
130
+ # Skip markers
131
+ requires_rust = pytest.mark.skipif(
132
+ not is_rust_installed(),
133
+ reason="Rust toolchain not installed",
134
+ )
135
+
136
+ requires_tauri = pytest.mark.skipif(
137
+ not is_tauri_cli_installed(),
138
+ reason="Tauri CLI not installed (run: cargo install tauri-cli)",
139
+ )
140
+
141
+ requires_pyinstaller = pytest.mark.skipif(
142
+ not is_pyinstaller_installed(),
143
+ reason="PyInstaller not installed (run: pip install pyinstaller)",
144
+ )
145
+
146
+ requires_display = pytest.mark.skipif(
147
+ not has_display(),
148
+ reason="No display available for GUI tests",
149
+ )
150
+
151
+
152
+ def get_minimal_desktop_jac() -> str:
153
+ """Get minimal main.jac content for desktop testing."""
154
+ return '''"""Minimal desktop app for testing."""
155
+
156
+ # Backend function for sidecar testing
157
+ def:pub greet(name: str) -> str {
158
+ return f"Hello, {name}!";
159
+ }
160
+
161
+ def:pub add(a: int, b: int) -> int {
162
+ return a + b;
163
+ }
164
+
165
+ # Client-side component
166
+ cl import from react { useEffect }
167
+
168
+ cl {
169
+ def:pub app() -> any {
170
+ has count: int = 0;
171
+
172
+ return <div>
173
+ <h1>Desktop Test App</h1>
174
+ <p>Count: {count}</p>
175
+ <button onClick={lambda -> None { count = count + 1; }}>
176
+ Increment
177
+ </button>
178
+ </div>;
179
+ }
180
+ }
181
+ '''
182
+
183
+
184
+ def get_minimal_jac_toml(name: str = "test-desktop-app") -> str:
185
+ """Get minimal jac.toml content for desktop testing."""
186
+ return f'''[project]
187
+ name = "{name}"
188
+ version = "1.0.0"
189
+ description = "Desktop test app"
190
+ entry-point = "main.jac"
191
+
192
+ [dependencies.npm]
193
+ jac-client-node = "1.0.4"
194
+
195
+ [dependencies.npm.dev]
196
+ "@jac-client/dev-deps" = "1.0.0"
197
+
198
+ [serve]
199
+ base_route_app = "app"
200
+ '''
201
+
202
+
203
+ # =============================================================================
204
+ # Test: Desktop Target Files Exist (no CLI required)
205
+ # =============================================================================
206
+
207
+
208
+ def test_desktop_target_files_exist() -> None:
209
+ """Test that the desktop target implementation files exist.
210
+
211
+ This test verifies the desktop target implementation is properly structured,
212
+ without requiring the CLI to be available.
213
+ """
214
+ print("[DEBUG] Starting test_desktop_target_files_exist")
215
+
216
+ # Get path to jac_client plugin
217
+ plugin_dir = Path(__file__).parent.parent / "plugin"
218
+
219
+ # Verify desktop target files
220
+ desktop_target_jac = plugin_dir / "src" / "targets" / "desktop_target.jac"
221
+ assert desktop_target_jac.exists(), (
222
+ f"desktop_target.jac not found at {desktop_target_jac}"
223
+ )
224
+
225
+ # Verify implementation file
226
+ desktop_impl_jac = (
227
+ plugin_dir / "src" / "targets" / "impl" / "desktop_target.impl.jac"
228
+ )
229
+ assert desktop_impl_jac.exists(), (
230
+ f"desktop_target.impl.jac not found at {desktop_impl_jac}"
231
+ )
232
+
233
+ # Verify sidecar files
234
+ sidecar_main_py = plugin_dir / "src" / "targets" / "desktop" / "sidecar" / "main.py"
235
+ assert sidecar_main_py.exists(), f"sidecar main.py not found at {sidecar_main_py}"
236
+
237
+ # Read desktop_target.jac and verify it has the expected methods
238
+ desktop_target_content = desktop_target_jac.read_text()
239
+ assert "class DesktopTarget" in desktop_target_content
240
+ assert "def setup" in desktop_target_content
241
+ assert "def build" in desktop_target_content
242
+ assert "def dev" in desktop_target_content
243
+ assert "def start" in desktop_target_content
244
+
245
+ print("[DEBUG] All desktop target files verified!")
246
+
247
+
248
+ # =============================================================================
249
+ # Test: Desktop Setup Command (requires CLI)
250
+ # =============================================================================
251
+
252
+
253
+ @requires_jac_setup_cli
254
+ def test_desktop_setup_creates_directory_structure() -> None:
255
+ """Test that `jac setup desktop` creates the expected Tauri directory structure.
256
+
257
+ This test verifies:
258
+ 1. src-tauri/ directory is created
259
+ 2. tauri.conf.json is generated with valid JSON
260
+ 3. Cargo.toml is generated
261
+ 4. build.rs is generated
262
+ 5. src/main.rs is generated
263
+ 6. icons/ directory is created
264
+ 7. binaries/ directory is created
265
+ 8. jac.toml is updated with [desktop] section
266
+ """
267
+ print("[DEBUG] Starting test_desktop_setup_creates_directory_structure")
268
+
269
+ app_name = "desktop-setup-test"
270
+
271
+ with tempfile.TemporaryDirectory() as temp_dir:
272
+ print(f"[DEBUG] Created temporary directory at {temp_dir}")
273
+ project_dir = Path(temp_dir) / app_name
274
+ project_dir.mkdir(parents=True)
275
+
276
+ # Create minimal project files
277
+ (project_dir / "main.jac").write_text(get_minimal_desktop_jac())
278
+ (project_dir / "jac.toml").write_text(get_minimal_jac_toml(app_name))
279
+
280
+ print(f"[DEBUG] Created project at {project_dir}")
281
+ print(f"[DEBUG] Project files: {list(project_dir.iterdir())}")
282
+
283
+ # Run jac setup desktop
284
+ print("[DEBUG] Running desktop setup")
285
+ returncode, stdout, stderr = run_jac_setup_desktop(project_dir)
286
+
287
+ print(
288
+ f"[DEBUG] Desktop setup completed returncode={returncode}\n"
289
+ f"STDOUT:\n{stdout[:2000]}\n"
290
+ f"STDERR:\n{stderr[:2000]}\n"
291
+ )
292
+
293
+ # Setup should succeed (returncode 0)
294
+ assert returncode == 0, (
295
+ f"jac setup desktop failed\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
296
+ )
297
+
298
+ # Verify src-tauri directory exists
299
+ tauri_dir = project_dir / "src-tauri"
300
+ assert tauri_dir.exists(), "src-tauri/ directory should be created"
301
+ assert tauri_dir.is_dir(), "src-tauri should be a directory"
302
+
303
+ # Verify tauri.conf.json
304
+ tauri_config_path = tauri_dir / "tauri.conf.json"
305
+ assert tauri_config_path.exists(), "tauri.conf.json should be created"
306
+
307
+ # Verify it's valid JSON
308
+ with open(tauri_config_path) as f:
309
+ tauri_config = json.load(f)
310
+
311
+ print(
312
+ f"[DEBUG] tauri.conf.json content: {json.dumps(tauri_config, indent=2)[:500]}"
313
+ )
314
+
315
+ # Verify key config values
316
+ assert "productName" in tauri_config, "tauri.conf.json should have productName"
317
+ assert "version" in tauri_config, "tauri.conf.json should have version"
318
+ assert "identifier" in tauri_config, "tauri.conf.json should have identifier"
319
+ assert "build" in tauri_config, "tauri.conf.json should have build section"
320
+ assert "app" in tauri_config, "tauri.conf.json should have app section"
321
+
322
+ # Verify Cargo.toml
323
+ cargo_path = tauri_dir / "Cargo.toml"
324
+ assert cargo_path.exists(), "Cargo.toml should be created"
325
+ cargo_content = cargo_path.read_text()
326
+ assert "[package]" in cargo_content, "Cargo.toml should have [package] section"
327
+ assert "tauri" in cargo_content.lower(), "Cargo.toml should reference tauri"
328
+
329
+ # Verify build.rs
330
+ build_rs_path = tauri_dir / "build.rs"
331
+ assert build_rs_path.exists(), "build.rs should be created"
332
+ build_rs_content = build_rs_path.read_text()
333
+ assert "tauri_build" in build_rs_content, "build.rs should call tauri_build"
334
+
335
+ # Verify src/main.rs
336
+ main_rs_path = tauri_dir / "src" / "main.rs"
337
+ assert main_rs_path.exists(), "src/main.rs should be created"
338
+
339
+ # Verify icons directory
340
+ icons_dir = tauri_dir / "icons"
341
+ assert icons_dir.exists(), "icons/ directory should be created"
342
+
343
+ # Verify binaries directory
344
+ binaries_dir = tauri_dir / "binaries"
345
+ assert binaries_dir.exists(), "binaries/ directory should be created"
346
+
347
+ # Verify jac.toml was updated with [desktop] section
348
+ jac_toml_content = (project_dir / "jac.toml").read_text()
349
+ assert "[desktop]" in jac_toml_content, "jac.toml should have [desktop] section"
350
+
351
+ print("[DEBUG] All desktop setup verifications passed!")
352
+
353
+
354
+ @requires_jac_setup_cli
355
+ def test_desktop_setup_is_idempotent() -> None:
356
+ """Test that running `jac setup desktop` twice doesn't fail or duplicate files."""
357
+ print("[DEBUG] Starting test_desktop_setup_is_idempotent")
358
+
359
+ app_name = "desktop-idempotent-test"
360
+
361
+ with tempfile.TemporaryDirectory() as temp_dir:
362
+ project_dir = Path(temp_dir) / app_name
363
+ project_dir.mkdir(parents=True)
364
+
365
+ # Create minimal project files
366
+ (project_dir / "main.jac").write_text(get_minimal_desktop_jac())
367
+ (project_dir / "jac.toml").write_text(get_minimal_jac_toml(app_name))
368
+
369
+ # First setup
370
+ print("[DEBUG] Running first desktop setup")
371
+ returncode1, stdout1, stderr1 = run_jac_setup_desktop(project_dir)
372
+ assert returncode1 == 0, f"First setup failed: {stderr1}"
373
+
374
+ # Get state after first setup
375
+ tauri_config_path = project_dir / "src-tauri" / "tauri.conf.json"
376
+ with open(tauri_config_path) as f:
377
+ config_after_first = json.load(f)
378
+
379
+ # Second setup
380
+ print("[DEBUG] Running second desktop setup")
381
+ returncode2, stdout2, stderr2 = run_jac_setup_desktop(project_dir)
382
+
383
+ # Second setup should not fail (it should detect already set up)
384
+ # Note: It may return 0 with a warning, or it may skip silently
385
+ print(
386
+ f"[DEBUG] Second setup returncode={returncode2}\n"
387
+ f"STDOUT:\n{stdout2[:1000]}\n"
388
+ f"STDERR:\n{stderr2[:1000]}"
389
+ )
390
+
391
+ # Config should still be valid
392
+ with open(tauri_config_path) as f:
393
+ config_after_second = json.load(f)
394
+
395
+ # Key values should remain consistent
396
+ assert config_after_first["productName"] == config_after_second["productName"]
397
+ assert config_after_first["version"] == config_after_second["version"]
398
+
399
+ print("[DEBUG] Desktop setup idempotency test passed!")
400
+
401
+
402
+ # =============================================================================
403
+ # Test: Web Build Still Works (Regression Test)
404
+ # =============================================================================
405
+
406
+
407
+ @requires_jac_setup_cli
408
+ def test_web_build_still_works_after_desktop_setup() -> None:
409
+ """Test that web target still works after desktop setup (regression test)."""
410
+ print("[DEBUG] Starting test_web_build_still_works_after_desktop_setup")
411
+
412
+ app_name = "desktop-web-regression-test"
413
+
414
+ with tempfile.TemporaryDirectory() as temp_dir:
415
+ project_dir = Path(temp_dir) / app_name
416
+ project_dir.mkdir(parents=True)
417
+
418
+ # Create minimal project files
419
+ (project_dir / "main.jac").write_text(get_minimal_desktop_jac())
420
+ (project_dir / "jac.toml").write_text(get_minimal_jac_toml(app_name))
421
+
422
+ jac_cmd = get_jac_command()
423
+ env = get_env_with_npm()
424
+
425
+ # Setup desktop first
426
+ print("[DEBUG] Setting up desktop target")
427
+ returncode, _, stderr = run_jac_setup_desktop(project_dir)
428
+ assert returncode == 0, f"Desktop setup failed: {stderr}"
429
+
430
+ # Install npm packages
431
+ print("[DEBUG] Installing npm packages")
432
+ npm_result = run(
433
+ [*jac_cmd, "add", "--npm"],
434
+ cwd=project_dir,
435
+ capture_output=True,
436
+ text=True,
437
+ env=env,
438
+ timeout=180,
439
+ )
440
+ if npm_result.returncode != 0:
441
+ pytest.skip(f"npm install failed: {npm_result.stderr}")
442
+
443
+ # Build web target
444
+ print("[DEBUG] Building web target")
445
+ build_result = run(
446
+ [*jac_cmd, "build", "main.jac", "--client", "web"],
447
+ cwd=project_dir,
448
+ capture_output=True,
449
+ text=True,
450
+ env=env,
451
+ timeout=180,
452
+ )
453
+
454
+ print(
455
+ f"[DEBUG] Web build returncode={build_result.returncode}\n"
456
+ f"STDOUT:\n{build_result.stdout[:2000]}\n"
457
+ f"STDERR:\n{build_result.stderr[:2000]}"
458
+ )
459
+
460
+ assert build_result.returncode == 0, (
461
+ f"Web build failed after desktop setup\n"
462
+ f"STDOUT:\n{build_result.stdout}\n"
463
+ f"STDERR:\n{build_result.stderr}"
464
+ )
465
+
466
+ # Verify web build output
467
+ dist_dir = project_dir / ".jac" / "client" / "dist"
468
+ assert dist_dir.exists(), "Web build should create .jac/client/dist/"
469
+
470
+ # Check for index.html or JS bundle
471
+ dist_files = list(dist_dir.iterdir())
472
+ print(f"[DEBUG] Web dist files: {dist_files}")
473
+ assert len(dist_files) > 0, "Web dist should contain files"
474
+
475
+ print("[DEBUG] Web build regression test passed!")
476
+
477
+
478
+ # =============================================================================
479
+ # Test: Desktop Build (requires full toolchain)
480
+ # =============================================================================
481
+
482
+
483
+ @requires_jac_setup_cli
484
+ @requires_rust
485
+ @requires_tauri
486
+ @requires_pyinstaller
487
+ def test_desktop_build_creates_bundle() -> None:
488
+ """Test that `jac build --client desktop` creates a bundle.
489
+
490
+ This test requires:
491
+ - Rust toolchain installed
492
+ - Tauri CLI installed
493
+ - PyInstaller installed
494
+
495
+ It verifies:
496
+ 1. Web bundle is built first
497
+ 2. Sidecar is bundled (if PyInstaller available)
498
+ 3. Tauri build completes
499
+ 4. Bundle output exists
500
+ """
501
+ print("[DEBUG] Starting test_desktop_build_creates_bundle")
502
+
503
+ app_name = "desktop-build-test"
504
+
505
+ with tempfile.TemporaryDirectory() as temp_dir:
506
+ project_dir = Path(temp_dir) / app_name
507
+ project_dir.mkdir(parents=True)
508
+
509
+ # Create minimal project files
510
+ (project_dir / "main.jac").write_text(get_minimal_desktop_jac())
511
+ (project_dir / "jac.toml").write_text(get_minimal_jac_toml(app_name))
512
+
513
+ jac_cmd = get_jac_command()
514
+ env = get_env_with_npm()
515
+
516
+ # Setup desktop
517
+ print("[DEBUG] Setting up desktop target")
518
+ returncode, _, stderr = run_jac_setup_desktop(project_dir)
519
+ assert returncode == 0, f"Desktop setup failed: {stderr}"
520
+
521
+ # Install npm packages
522
+ print("[DEBUG] Installing npm packages")
523
+ npm_result = run(
524
+ [*jac_cmd, "add", "--npm"],
525
+ cwd=project_dir,
526
+ capture_output=True,
527
+ text=True,
528
+ env=env,
529
+ timeout=180,
530
+ )
531
+ if npm_result.returncode != 0:
532
+ pytest.skip(f"npm install failed: {npm_result.stderr}")
533
+
534
+ # Build desktop (this can take a long time on first run)
535
+ print("[DEBUG] Building desktop target (this may take several minutes)")
536
+ build_result = run(
537
+ [*jac_cmd, "build", "main.jac", "--client", "desktop"],
538
+ cwd=project_dir,
539
+ capture_output=True,
540
+ text=True,
541
+ env=env,
542
+ timeout=900, # 15 minutes for Rust compilation
543
+ )
544
+
545
+ print(
546
+ f"[DEBUG] Desktop build returncode={build_result.returncode}\n"
547
+ f"STDOUT (last 3000 chars):\n{build_result.stdout[-3000:]}\n"
548
+ f"STDERR (last 3000 chars):\n{build_result.stderr[-3000:]}"
549
+ )
550
+
551
+ assert build_result.returncode == 0, (
552
+ f"Desktop build failed\n"
553
+ f"STDOUT:\n{build_result.stdout}\n"
554
+ f"STDERR:\n{build_result.stderr}"
555
+ )
556
+
557
+ # Verify sidecar was created
558
+ binaries_dir = project_dir / "src-tauri" / "binaries"
559
+ sidecar_files = list(binaries_dir.glob("jac-sidecar*"))
560
+ print(f"[DEBUG] Sidecar files: {sidecar_files}")
561
+ assert len(sidecar_files) > 0, (
562
+ "Sidecar should be bundled in src-tauri/binaries/"
563
+ )
564
+
565
+ # Verify Tauri bundle output exists
566
+ bundle_dir = project_dir / "src-tauri" / "target" / "release" / "bundle"
567
+ if bundle_dir.exists():
568
+ bundle_contents = list(bundle_dir.rglob("*"))
569
+ print(f"[DEBUG] Bundle contents: {bundle_contents[:20]}")
570
+ assert len(bundle_contents) > 0, "Bundle directory should contain files"
571
+
572
+ print("[DEBUG] Desktop build test passed!")
573
+
574
+
575
+ # =============================================================================
576
+ # Test: Sidecar Functionality
577
+ # =============================================================================
578
+
579
+
580
+ def test_sidecar_module_runs_directly() -> None:
581
+ """Test that the sidecar module can be run directly.
582
+
583
+ This tests the sidecar's ability to:
584
+ 1. Start an HTTP server
585
+ 2. Compile Jac code
586
+ 3. Serve API endpoints
587
+ """
588
+ print("[DEBUG] Starting test_sidecar_module_runs_directly")
589
+
590
+ app_name = "sidecar-test"
591
+
592
+ # Get the path to the sidecar main.py file
593
+ sidecar_main_py = (
594
+ Path(__file__).parent.parent
595
+ / "plugin"
596
+ / "src"
597
+ / "targets"
598
+ / "desktop"
599
+ / "sidecar"
600
+ / "main.py"
601
+ )
602
+
603
+ if not sidecar_main_py.exists():
604
+ pytest.skip(f"Sidecar main.py not found at {sidecar_main_py}")
605
+
606
+ with tempfile.TemporaryDirectory() as temp_dir:
607
+ project_dir = Path(temp_dir) / app_name
608
+ project_dir.mkdir(parents=True)
609
+
610
+ # Create a simple backend-only Jac file (no client code)
611
+ backend_jac = '''"""Simple backend for sidecar testing."""
612
+
613
+ def:pub greet(name: str) -> str {
614
+ return f"Hello, {name}!";
615
+ }
616
+
617
+ def:pub add(a: int, b: int) -> int {
618
+ return a + b;
619
+ }
620
+
621
+ def:pub get_status() -> dict {
622
+ return {"status": "ok", "version": "1.0.0"};
623
+ }
624
+ '''
625
+ (project_dir / "main.jac").write_text(backend_jac)
626
+
627
+ jac_toml = """[project]
628
+ name = "sidecar-test"
629
+ version = "1.0.0"
630
+ description = "Sidecar test"
631
+ entry-point = "main.jac"
632
+ """
633
+ (project_dir / "jac.toml").write_text(jac_toml)
634
+
635
+ # Start sidecar by running main.py directly
636
+ port = get_free_port()
637
+ sidecar_process: Popen[bytes] | None = None
638
+
639
+ try:
640
+ print(f"[DEBUG] Starting sidecar on port {port}")
641
+ sidecar_process = Popen(
642
+ [
643
+ sys.executable,
644
+ str(sidecar_main_py),
645
+ "--module-path",
646
+ "main.jac",
647
+ "--port",
648
+ str(port),
649
+ ],
650
+ cwd=project_dir,
651
+ stdout=PIPE,
652
+ stderr=PIPE,
653
+ )
654
+
655
+ # Wait for server to start
656
+ print(f"[DEBUG] Waiting for sidecar on 127.0.0.1:{port}")
657
+ try:
658
+ wait_for_port("127.0.0.1", port, timeout=60.0)
659
+ except TimeoutError:
660
+ # Get process output for debugging
661
+ if sidecar_process.poll() is not None:
662
+ stdout, stderr = sidecar_process.communicate(timeout=5)
663
+ pytest.fail(
664
+ f"Sidecar process exited early (code {sidecar_process.returncode})\n"
665
+ f"STDOUT:\n{stdout.decode()}\n"
666
+ f"STDERR:\n{stderr.decode()}"
667
+ )
668
+ raise
669
+
670
+ print(f"[DEBUG] Sidecar is accepting connections on port {port}")
671
+
672
+ # Test root endpoint (may return 404 in some configurations)
673
+ print("[DEBUG] Testing root endpoint /")
674
+ try:
675
+ with urlopen(f"http://127.0.0.1:{port}/", timeout=10) as resp:
676
+ root_body = resp.read().decode("utf-8", errors="ignore")
677
+ print(f"[DEBUG] Root response: {root_body[:500]}")
678
+ assert resp.status == 200, "Root endpoint should return 200"
679
+ except HTTPError as exc:
680
+ if exc.code == 404:
681
+ # Root endpoint might not exist - that's OK, test /functions instead
682
+ print("[DEBUG] Root endpoint returned 404, this is acceptable")
683
+ else:
684
+ pytest.fail(f"Failed to GET root endpoint: {exc}")
685
+ except URLError as exc:
686
+ pytest.fail(f"Failed to GET root endpoint: {exc}")
687
+
688
+ # Test functions endpoint (may return 404 if API structure differs)
689
+ print("[DEBUG] Testing /functions endpoint")
690
+ try:
691
+ with urlopen(f"http://127.0.0.1:{port}/functions", timeout=10) as resp:
692
+ funcs_body = resp.read().decode("utf-8", errors="ignore")
693
+ print(f"[DEBUG] Functions response: {funcs_body[:500]}")
694
+ if "greet" in funcs_body or "add" in funcs_body:
695
+ print("[DEBUG] Functions endpoint lists defined functions")
696
+ else:
697
+ print(
698
+ "[DEBUG] Functions endpoint exists but doesn't list expected functions"
699
+ )
700
+ except HTTPError as exc:
701
+ if exc.code == 404:
702
+ print(
703
+ "[DEBUG] /functions endpoint returned 404, this is acceptable"
704
+ )
705
+ else:
706
+ print(f"[DEBUG] /functions endpoint returned {exc.code}")
707
+ except URLError as exc:
708
+ print(f"[DEBUG] /functions endpoint error: {exc}")
709
+
710
+ # Test function call - greet (may not be available in all API versions)
711
+ print("[DEBUG] Testing POST /function/greet")
712
+ try:
713
+ req = Request(
714
+ f"http://127.0.0.1:{port}/function/greet",
715
+ data=json.dumps({"name": "Sidecar Test"}).encode("utf-8"),
716
+ headers={"Content-Type": "application/json"},
717
+ method="POST",
718
+ )
719
+ with urlopen(req, timeout=10) as resp:
720
+ greet_body = resp.read().decode("utf-8", errors="ignore")
721
+ print(f"[DEBUG] Greet response: {greet_body}")
722
+ response_data = json.loads(greet_body)
723
+ data = response_data.get("data", response_data)
724
+ if isinstance(data, dict) and "error" in data:
725
+ print(f"[DEBUG] Function call returned error: {data['error']}")
726
+ elif isinstance(data, str) and "Hello" in data:
727
+ print("[DEBUG] Greet returned expected greeting")
728
+ else:
729
+ print(f"[DEBUG] Greet response: {data}")
730
+ except HTTPError as exc:
731
+ if exc.code == 404:
732
+ print(
733
+ "[DEBUG] /function/greet returned 404, endpoint may not exist"
734
+ )
735
+ else:
736
+ print(f"[DEBUG] /function/greet returned {exc.code}")
737
+ except URLError as exc:
738
+ print(f"[DEBUG] /function/greet error: {exc}")
739
+
740
+ # Test function call - add (may not be available in all API versions)
741
+ print("[DEBUG] Testing POST /function/add")
742
+ try:
743
+ req = Request(
744
+ f"http://127.0.0.1:{port}/function/add",
745
+ data=json.dumps({"a": 10, "b": 20}).encode("utf-8"),
746
+ headers={"Content-Type": "application/json"},
747
+ method="POST",
748
+ )
749
+ with urlopen(req, timeout=10) as resp:
750
+ add_body = resp.read().decode("utf-8", errors="ignore")
751
+ print(f"[DEBUG] Add response: {add_body}")
752
+ response_data = json.loads(add_body)
753
+ data = response_data.get("data", response_data)
754
+ if isinstance(data, dict) and "error" in data:
755
+ print(f"[DEBUG] Function call returned error: {data['error']}")
756
+ elif data == 30 or "30" in str(data):
757
+ print("[DEBUG] Add returned expected result: 30")
758
+ else:
759
+ print(f"[DEBUG] Add response: {data}")
760
+ except HTTPError as exc:
761
+ if exc.code == 404:
762
+ print("[DEBUG] /function/add returned 404, endpoint may not exist")
763
+ else:
764
+ print(f"[DEBUG] /function/add returned {exc.code}")
765
+ except URLError as exc:
766
+ print(f"[DEBUG] /function/add error: {exc}")
767
+
768
+ print("[DEBUG] All sidecar tests passed!")
769
+
770
+ finally:
771
+ if sidecar_process is not None:
772
+ print("[DEBUG] Terminating sidecar process")
773
+ # Close stdout and stderr pipes first to avoid ResourceWarning
774
+ if sidecar_process.stdout:
775
+ sidecar_process.stdout.close()
776
+ if sidecar_process.stderr:
777
+ sidecar_process.stderr.close()
778
+ sidecar_process.terminate()
779
+ try:
780
+ sidecar_process.wait(timeout=10)
781
+ except subprocess.TimeoutExpired:
782
+ sidecar_process.kill()
783
+ sidecar_process.wait(timeout=5)
784
+ time.sleep(0.5)
785
+ gc.collect()
786
+
787
+
788
+ # =============================================================================
789
+ # Test: Desktop Dev Mode (requires display)
790
+ # =============================================================================
791
+
792
+
793
+ @requires_jac_setup_cli
794
+ @requires_rust
795
+ @requires_tauri
796
+ @requires_display
797
+ def test_desktop_dev_mode_starts() -> None:
798
+ """Test that `jac start --client desktop --dev` starts correctly.
799
+
800
+ This test requires a display and full Tauri toolchain.
801
+ It verifies:
802
+ 1. Vite dev server starts
803
+ 2. Tauri dev window launches
804
+ 3. Server can be stopped cleanly
805
+ """
806
+ print("[DEBUG] Starting test_desktop_dev_mode_starts")
807
+
808
+ app_name = "desktop-dev-test"
809
+
810
+ with tempfile.TemporaryDirectory() as temp_dir:
811
+ project_dir = Path(temp_dir) / app_name
812
+ project_dir.mkdir(parents=True)
813
+
814
+ # Create minimal project files
815
+ (project_dir / "main.jac").write_text(get_minimal_desktop_jac())
816
+ (project_dir / "jac.toml").write_text(get_minimal_jac_toml(app_name))
817
+
818
+ jac_cmd = get_jac_command()
819
+ env = get_env_with_npm()
820
+
821
+ # Setup desktop
822
+ returncode, _, stderr = run_jac_setup_desktop(project_dir)
823
+ assert returncode == 0, f"Desktop setup failed: {stderr}"
824
+
825
+ # Install npm packages
826
+ npm_result = run(
827
+ [*jac_cmd, "add", "--npm"],
828
+ cwd=project_dir,
829
+ capture_output=True,
830
+ text=True,
831
+ env=env,
832
+ timeout=180,
833
+ )
834
+ if npm_result.returncode != 0:
835
+ pytest.skip(f"npm install failed: {npm_result.stderr}")
836
+
837
+ # Start desktop dev mode
838
+ dev_process: Popen[bytes] | None = None
839
+
840
+ try:
841
+ print("[DEBUG] Starting desktop dev mode")
842
+ dev_process = Popen(
843
+ [*jac_cmd, "start", "main.jac", "--client", "desktop", "--dev"],
844
+ cwd=project_dir,
845
+ env=env,
846
+ )
847
+
848
+ # Wait for Vite dev server (port 5173)
849
+ print("[DEBUG] Waiting for Vite dev server on port 5173")
850
+ try:
851
+ wait_for_port("127.0.0.1", 5173, timeout=90.0)
852
+ print("[DEBUG] Vite dev server is running on port 5173")
853
+ except TimeoutError:
854
+ # Dev server might not have started
855
+ if dev_process.poll() is not None:
856
+ pytest.fail(
857
+ f"Desktop dev process exited early (code {dev_process.returncode})"
858
+ )
859
+ raise
860
+
861
+ # Give Tauri a moment to initialize
862
+ time.sleep(3)
863
+
864
+ # Verify dev server is serving content
865
+ try:
866
+ with urlopen("http://127.0.0.1:5173/", timeout=10) as resp:
867
+ body = resp.read().decode("utf-8", errors="ignore")
868
+ print(f"[DEBUG] Dev server response: {body[:500]}")
869
+ assert resp.status == 200, "Dev server should return 200"
870
+ except (URLError, HTTPError) as exc:
871
+ print(f"[DEBUG] Warning: Could not fetch from dev server: {exc}")
872
+ # Not a hard failure - Tauri might intercept
873
+
874
+ print("[DEBUG] Desktop dev mode test passed!")
875
+
876
+ finally:
877
+ if dev_process is not None:
878
+ print("[DEBUG] Terminating desktop dev process")
879
+ # Send SIGINT for clean shutdown
880
+ try:
881
+ dev_process.send_signal(signal.SIGINT)
882
+ dev_process.wait(timeout=15)
883
+ except (subprocess.TimeoutExpired, OSError):
884
+ dev_process.terminate()
885
+ try:
886
+ dev_process.wait(timeout=10)
887
+ except subprocess.TimeoutExpired:
888
+ dev_process.kill()
889
+ dev_process.wait(timeout=5)
890
+ time.sleep(1)
891
+ gc.collect()