jac-client 0.2.8__py3-none-any.whl → 0.2.10__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.
- jac_client/examples/all-in-one/{app.jac → main.jac} +5 -5
- jac_client/examples/all-in-one/pages/BudgetPlanner.jac +8 -1
- jac_client/examples/all-in-one/pages/FeaturesTest.jac +16 -1
- jac_client/examples/all-in-one/pages/{FeaturesTest.cl.jac → features_test_ui.cl.jac} +11 -0
- jac_client/examples/all-in-one/pages/nestedDemo.jac +1 -1
- jac_client/examples/all-in-one/pages/notFound.jac +2 -7
- jac_client/plugin/cli.jac +491 -411
- jac_client/plugin/client.jac +25 -0
- jac_client/plugin/client_runtime.cl.jac +5 -1
- jac_client/plugin/impl/client.impl.jac +96 -55
- jac_client/plugin/impl/client_runtime.impl.jac +154 -0
- jac_client/plugin/plugin_config.jac +243 -15
- jac_client/plugin/src/config_loader.jac +1 -0
- jac_client/plugin/src/desktop_config.jac +31 -0
- jac_client/plugin/src/impl/compiler.impl.jac +1 -1
- jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
- jac_client/plugin/src/impl/desktop_config.impl.jac +191 -0
- jac_client/plugin/src/impl/vite_bundler.impl.jac +97 -16
- jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
- jac_client/plugin/src/targets/desktop_target.jac +37 -0
- jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2334 -0
- jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
- jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
- jac_client/plugin/src/targets/register.jac +21 -0
- jac_client/plugin/src/targets/registry.jac +87 -0
- jac_client/plugin/src/targets/web_target.jac +35 -0
- jac_client/plugin/src/vite_bundler.jac +6 -0
- jac_client/plugin/utils/__init__.jac +1 -0
- jac_client/plugin/utils/impl/node_installer.impl.jac +249 -0
- jac_client/plugin/utils/node_installer.jac +41 -0
- jac_client/templates/client.jacpack +72 -0
- jac_client/templates/fullstack.jacpack +61 -0
- jac_client/tests/conftest.py +48 -7
- jac_client/tests/test_cli.py +184 -70
- jac_client/tests/test_e2e.py +232 -0
- jac_client/tests/test_helpers.py +65 -0
- jac_client/tests/test_it.py +91 -135
- jac_client/tests/test_it_desktop.py +891 -0
- {jac_client-0.2.8.dist-info → jac_client-0.2.10.dist-info}/METADATA +4 -4
- jac_client-0.2.10.dist-info/RECORD +115 -0
- {jac_client-0.2.8.dist-info → jac_client-0.2.10.dist-info}/WHEEL +1 -1
- jac_client-0.2.8.dist-info/RECORD +0 -97
- /jac_client/examples/all-in-one/pages/{BudgetPlanner.cl.jac → budget_planner_ui.cl.jac} +0 -0
- /jac_client/examples/asset-serving/css-with-image/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/asset-serving/image-asset/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/asset-serving/import-alias/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/basic/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/basic-auth/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/basic-auth-with-router/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/basic-full-stack/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/css-styling/js-styling/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/css-styling/material-ui/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/css-styling/pure-css/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/css-styling/sass-example/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/css-styling/styled-components/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/css-styling/tailwind-example/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/full-stack-with-auth/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/little-x/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/nested-folders/nested-advance/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/nested-folders/nested-basic/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/ts-support/{src/app.jac → main.jac} +0 -0
- /jac_client/examples/with-router/{src/app.jac → main.jac} +0 -0
- {jac_client-0.2.8.dist-info → jac_client-0.2.10.dist-info}/entry_points.txt +0 -0
- {jac_client-0.2.8.dist-info → jac_client-0.2.10.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()
|