jac-client 0.2.6__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/{src/button.jac → button.jac} +4 -3
  2. jac_client/examples/all-in-one/components/CategoryFilter.jac +47 -0
  3. jac_client/examples/all-in-one/components/Header.jac +17 -0
  4. jac_client/examples/all-in-one/components/ProfitOverview.jac +64 -0
  5. jac_client/examples/all-in-one/components/Summary.jac +76 -0
  6. jac_client/examples/all-in-one/components/TransactionForm.jac +188 -0
  7. jac_client/examples/all-in-one/components/TransactionItem.jac +62 -0
  8. jac_client/examples/all-in-one/components/TransactionList.jac +44 -0
  9. jac_client/examples/all-in-one/components/button.jac +8 -0
  10. jac_client/examples/all-in-one/components/navigation.jac +126 -0
  11. jac_client/examples/all-in-one/constants/categories.jac +36 -0
  12. jac_client/examples/all-in-one/constants/clients.jac +12 -0
  13. jac_client/examples/all-in-one/context/BudgetContext.jac +31 -0
  14. jac_client/examples/all-in-one/hooks/useBudget.jac +122 -0
  15. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +37 -0
  16. jac_client/examples/all-in-one/main.jac +542 -0
  17. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +140 -0
  18. jac_client/examples/all-in-one/pages/FeaturesTest.jac +157 -0
  19. jac_client/examples/all-in-one/pages/LandingPage.jac +124 -0
  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 +127 -0
  23. jac_client/examples/all-in-one/pages/nestedDemo.jac +54 -0
  24. jac_client/examples/all-in-one/pages/notFound.jac +18 -0
  25. jac_client/examples/all-in-one/pages/signupPage.jac +127 -0
  26. jac_client/examples/all-in-one/utils/formatters.jac +49 -0
  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 +507 -470
  54. jac_client/plugin/client.jac +30 -12
  55. jac_client/plugin/client_runtime.cl.jac +25 -15
  56. jac_client/plugin/impl/client.impl.jac +126 -26
  57. jac_client/plugin/impl/client_runtime.impl.jac +182 -10
  58. jac_client/plugin/plugin_config.jac +216 -34
  59. jac_client/plugin/src/__init__.jac +0 -2
  60. jac_client/plugin/src/compiler.jac +2 -2
  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 +99 -30
  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 +384 -144
  69. jac_client/plugin/src/package_installer.jac +1 -1
  70. jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
  71. jac_client/plugin/src/targets/desktop_target.jac +37 -0
  72. jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2347 -0
  73. jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
  74. jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
  75. jac_client/plugin/src/targets/register.jac +21 -0
  76. jac_client/plugin/src/targets/registry.jac +87 -0
  77. jac_client/plugin/src/targets/web_target.jac +35 -0
  78. jac_client/plugin/src/vite_bundler.jac +15 -1
  79. jac_client/plugin/utils/__init__.jac +3 -0
  80. jac_client/plugin/utils/bun_installer.jac +16 -0
  81. jac_client/plugin/utils/impl/bun_installer.impl.jac +99 -0
  82. jac_client/templates/client.jacpack +72 -0
  83. jac_client/templates/fullstack.jacpack +61 -0
  84. jac_client/tests/conftest.py +110 -52
  85. jac_client/tests/fixtures/spawn_test/app.jac +64 -70
  86. jac_client/tests/fixtures/with-ts/app.jac +28 -28
  87. jac_client/tests/test_cli.py +280 -113
  88. jac_client/tests/test_e2e.py +232 -0
  89. jac_client/tests/test_helpers.py +58 -0
  90. jac_client/tests/test_it.py +325 -154
  91. jac_client/tests/test_it_desktop.py +891 -0
  92. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/METADATA +20 -11
  93. jac_client-0.2.11.dist-info/RECORD +113 -0
  94. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/WHEEL +1 -1
  95. jac_client/examples/all-in-one/src/app.jac +0 -841
  96. jac_client/examples/all-in-one/src/components/button.jac +0 -7
  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 -377
  102. jac_client/examples/basic-auth-with-router/src/app.jac +0 -464
  103. jac_client/examples/basic-full-stack/src/app.jac +0 -365
  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 -84
  117. jac_client-0.2.6.dist-info/RECORD +0 -74
  118. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/entry_points.txt +0 -0
  119. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- """End-to-end tests for `jac serve` HTTP endpoints."""
1
+ """End-to-end tests for `jac start` HTTP endpoints."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -6,7 +6,6 @@ import gc
6
6
  import json
7
7
  import os
8
8
  import shutil
9
- import socket
10
9
  import tempfile
11
10
  import time
12
11
  from http.client import RemoteDisconnected
@@ -16,44 +15,12 @@ from urllib.request import Request, urlopen
16
15
 
17
16
  import pytest
18
17
 
19
- from jaclang.pycore.runtime import JacRuntime as Jac
20
-
21
-
22
- @pytest.fixture(autouse=True)
23
- def reset_jac_machine():
24
- """Reset Jac machine before and after each test."""
25
- Jac.reset_machine()
26
- yield
27
- Jac.reset_machine()
28
-
29
-
30
- def _wait_for_port(
31
- host: str,
32
- port: int,
33
- timeout: float = 60.0,
34
- poll_interval: float = 0.5,
35
- ) -> None:
36
- """Block until a TCP port is accepting connections or timeout.
37
-
38
- Raises:
39
- TimeoutError: if the port is not accepting connections within timeout.
40
- """
41
- deadline = time.time() + timeout
42
- last_err: Exception | None = None
43
-
44
- while time.time() < deadline:
45
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
46
- sock.settimeout(poll_interval)
47
- try:
48
- sock.connect((host, port))
49
- return
50
- except OSError as exc: # Connection refused / timeout
51
- last_err = exc
52
- time.sleep(poll_interval)
53
-
54
- raise TimeoutError(
55
- f"Timed out waiting for {host}:{port} to become available. Last error: {last_err}"
56
- )
18
+ from .test_helpers import (
19
+ get_env_with_npm, # Backward compat alias
20
+ get_free_port,
21
+ get_jac_command,
22
+ wait_for_port,
23
+ )
57
24
 
58
25
 
59
26
  def _wait_for_endpoint(
@@ -117,7 +84,7 @@ def _wait_for_endpoint(
117
84
  def test_all_in_one_app_endpoints() -> None:
118
85
  """Create a Jac app, copy @all-in-one into it, install packages from jac.toml, then verify endpoints."""
119
86
  print(
120
- "[DEBUG] Starting test_all_in_one_app_endpoints using jac create --cl + @all-in-one"
87
+ "[DEBUG] Starting test_all_in_one_app_endpoints using jac create --use client + @all-in-one"
121
88
  )
122
89
 
123
90
  # Resolve the path to jac_client/examples/all-in-one relative to this test file.
@@ -138,9 +105,9 @@ def test_all_in_one_app_endpoints() -> None:
138
105
  print(f"[DEBUG] Changed working directory to {temp_dir}")
139
106
 
140
107
  # 1. Create a new Jac app via CLI (requires jac + jac-client plugin installed)
141
- print(f"[DEBUG] Running 'jac create --cl {app_name}'")
108
+ print(f"[DEBUG] Running 'jac create --use client {app_name}'")
142
109
  process = Popen(
143
- ["jac", "create", "--cl", app_name],
110
+ ["jac", "create", "--use", "client", app_name],
144
111
  stdin=PIPE,
145
112
  stdout=PIPE,
146
113
  stderr=PIPE,
@@ -150,21 +117,21 @@ def test_all_in_one_app_endpoints() -> None:
150
117
  returncode = process.returncode
151
118
 
152
119
  print(
153
- "[DEBUG] 'jac create --cl' completed "
120
+ "[DEBUG] 'jac create --use client' completed "
154
121
  f"returncode={returncode}\n"
155
122
  f"STDOUT:\n{stdout}\n"
156
123
  f"STDERR:\n{stderr}\n"
157
124
  )
158
125
 
159
- # If the currently installed `jac` CLI does not support `create --cl`,
126
+ # If the currently installed `jac` CLI does not support `create --use client`,
160
127
  # fail the test instead of skipping it.
161
- if returncode != 0 and "unrecognized arguments: --cl" in stderr:
128
+ if returncode != 0 and "unrecognized arguments: --use" in stderr:
162
129
  pytest.fail(
163
- "Test failed: installed `jac` CLI does not support `create --cl`."
130
+ "Test failed: installed `jac` CLI does not support `create --use client`."
164
131
  )
165
132
 
166
133
  assert returncode == 0, (
167
- f"jac create --cl failed\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}\n"
134
+ f"jac create --use client failed\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}\n"
168
135
  )
169
136
 
170
137
  project_path = os.path.join(temp_dir, app_name)
@@ -184,17 +151,17 @@ def test_all_in_one_app_endpoints() -> None:
184
151
  else:
185
152
  shutil.copy2(src, dst)
186
153
 
187
- # 3. Install packages from jac.toml using `jac add --cl`
154
+ # 3. Install packages from jac.toml using `jac add --npm`
188
155
  # This reads packages from jac.toml, generates package.json, and runs npm install
189
- print("[DEBUG] Running 'jac add --cl' to install packages from jac.toml")
156
+ print("[DEBUG] Running 'jac add --npm' to install packages from jac.toml")
190
157
  jac_add_result = run(
191
- ["jac", "add", "--cl"],
158
+ ["jac", "add", "--npm"],
192
159
  cwd=project_path,
193
160
  capture_output=True,
194
161
  text=True,
195
162
  )
196
163
  print(
197
- "[DEBUG] 'jac add --cl' completed "
164
+ "[DEBUG] 'jac add --npm' completed "
198
165
  f"returncode={jac_add_result.returncode}\n"
199
166
  f"STDOUT (truncated to 2000 chars):\n{jac_add_result.stdout[:2000]}\n"
200
167
  f"STDERR (truncated to 2000 chars):\n{jac_add_result.stderr[:2000]}\n"
@@ -202,104 +169,97 @@ def test_all_in_one_app_endpoints() -> None:
202
169
 
203
170
  if jac_add_result.returncode != 0:
204
171
  pytest.fail(
205
- f"Test failed: jac add --cl failed or npm is not available in PATH.\n"
172
+ f"Test failed: jac add --npm failed or npm is not available in PATH.\n"
206
173
  f"STDOUT:\n{jac_add_result.stdout}\n"
207
174
  f"STDERR:\n{jac_add_result.stderr}\n"
208
175
  )
209
176
 
210
- app_jac_path = os.path.join(project_path, "src", "app.jac")
211
- assert os.path.isfile(app_jac_path), "all-in-one src/app.jac file missing"
177
+ app_jac_path = os.path.join(project_path, "main.jac")
178
+ assert os.path.isfile(app_jac_path), "all-in-one main.jac file missing"
212
179
 
213
- # 4. Start the server: `jac serve src/app.jac`
180
+ # 4. Start the server: `jac start main.jac`
214
181
  # NOTE: We don't use text mode here, so `Popen` defaults to bytes.
215
182
  # Use `Popen[bytes]` in the type annotation to keep mypy happy.
216
183
  server: Popen[bytes] | None = None
184
+ # Use dynamic port allocation to avoid conflicts when running tests in parallel
185
+ server_port = get_free_port()
217
186
  try:
218
- print("[DEBUG] Starting server with 'jac serve src/app.jac'")
187
+ print(
188
+ f"[DEBUG] Starting server with 'jac start main.jac -p {server_port}'"
189
+ )
219
190
  server = Popen(
220
- ["jac", "serve", "src/app.jac"],
191
+ ["jac", "start", "main.jac", "-p", str(server_port)],
221
192
  cwd=project_path,
222
193
  )
223
-
224
194
  # Wait for localhost:8000 to become available
225
- print("[DEBUG] Waiting for server to be available on 127.0.0.1:8000")
226
- _wait_for_port("127.0.0.1", 8000, timeout=90.0)
227
- print("[DEBUG] Server is now accepting connections on 127.0.0.1:8000")
195
+ print(
196
+ f"[DEBUG] Waiting for server to be available on 127.0.0.1:{server_port}"
197
+ )
198
+ wait_for_port("127.0.0.1", server_port, timeout=90.0)
199
+ print(
200
+ f"[DEBUG] Server is now accepting connections on 127.0.0.1:{server_port}"
201
+ )
228
202
 
229
- # "/" – server up
203
+ # "/" – server up (serves client app HTML due to base_route_app="app")
204
+ # Note: The root endpoint may return 503 while the client bundle is building.
205
+ # We use _wait_for_endpoint to retry on 503 until it's ready.
230
206
  try:
231
- print("[DEBUG] Sending GET request to root endpoint /")
232
- with urlopen(
233
- "http://127.0.0.1:8000",
234
- timeout=10,
235
- ) as resp_root:
236
- root_body = resp_root.read().decode("utf-8", errors="ignore")
237
- print(
238
- "[DEBUG] Received response from root endpoint /\n"
239
- f"Status: {resp_root.status}\n"
240
- f"Body (truncated to 500 chars):\n{root_body[:500]}"
241
- )
242
- assert resp_root.status == 200
243
- assert '"Jac API Server"' in root_body
244
- assert '"endpoints"' in root_body
245
-
246
- # Verify custom headers from jac.toml are present
247
- assert (
248
- resp_root.headers.get("Cross-Origin-Opener-Policy")
249
- == "same-origin"
250
- ), (
251
- "Expected Cross-Origin-Opener-Policy header to be 'same-origin'"
252
- )
253
- assert (
254
- resp_root.headers.get("Cross-Origin-Embedder-Policy")
255
- == "require-corp"
256
- ), (
257
- "Expected Cross-Origin-Embedder-Policy header to be 'require-corp'"
258
- )
259
- print(
260
- "[DEBUG] Custom headers verified: COOP and COEP are present"
261
- )
262
- except (URLError, HTTPError) as exc:
207
+ print("[DEBUG] Sending GET request to root endpoint / (with retry)")
208
+ root_bytes = _wait_for_endpoint(
209
+ f"http://127.0.0.1:{server_port}",
210
+ timeout=120.0,
211
+ poll_interval=2.0,
212
+ request_timeout=30.0,
213
+ )
214
+ root_body = root_bytes.decode("utf-8", errors="ignore")
215
+ print(
216
+ "[DEBUG] Received response from root endpoint /\n"
217
+ f"Body (truncated to 500 chars):\n{root_body[:500]}"
218
+ )
219
+ # With base_route_app="app", root serves client HTML
220
+ assert "<!DOCTYPE html>" in root_body or "<html" in root_body
221
+ assert '<div id="root">' in root_body
222
+ except (URLError, HTTPError, TimeoutError) as exc:
263
223
  print(f"[DEBUG] Error while requesting root endpoint: {exc}")
264
224
  pytest.fail(f"Failed to GET root endpoint: {exc}")
265
225
 
266
- # "/page/app" – main page is loading
226
+ # "/cl/app" – main page is loading
267
227
  # Note: This endpoint may return 503 (temporary) while the page is being compiled,
268
228
  # or 500 (permanent) if there's a compilation error. We use _wait_for_endpoint
269
229
  # to retry on 503 until it's ready, but it will fail immediately on 500.
270
230
  try:
271
231
  print(
272
- "[DEBUG] Sending GET request to /page/app endpoint (with retry)"
232
+ "[DEBUG] Sending GET request to /cl/app endpoint (with retry)"
273
233
  )
274
234
  page_bytes = _wait_for_endpoint(
275
- "http://127.0.0.1:8000/page/app",
235
+ f"http://127.0.0.1:{server_port}/cl/app",
276
236
  timeout=120.0,
277
237
  poll_interval=2.0,
278
238
  request_timeout=30.0,
279
239
  )
280
240
  page_body = page_bytes.decode("utf-8", errors="ignore")
281
241
  print(
282
- "[DEBUG] Received response from /page/app endpoint\n"
242
+ "[DEBUG] Received response from /cl/app endpoint\n"
283
243
  f"Body (truncated to 500 chars):\n{page_body[:500]}"
284
244
  )
285
245
  assert "<html" in page_body.lower()
286
246
  except (URLError, HTTPError, TimeoutError, RemoteDisconnected) as exc:
287
- print(f"[DEBUG] Error while requesting /page/app endpoint: {exc}")
288
- pytest.fail(f"Failed to GET /page/app endpoint: {exc}")
247
+ print(f"[DEBUG] Error while requesting /cl/app endpoint: {exc}")
248
+ pytest.fail(f"Failed to GET /cl/app endpoint: {exc}")
289
249
 
290
- # "/page/app#/nested" – relative paths / nested route
250
+ # "/cl/app#/nested" – relative paths / nested route
291
251
  # (hash fragment is client-side only but server should still serve the app shell)
292
252
  try:
293
- print("[DEBUG] Sending GET request to /page/app#/nested endpoint")
253
+ print("[DEBUG] Sending GET request to /cl/app#/nested endpoint")
294
254
  with urlopen(
295
- "http://127.0.0.1:8000/page/app#/nested",
255
+ f"http://127.0.0.1:{server_port}/cl/app#/nested",
296
256
  timeout=200,
297
257
  ) as resp_nested:
298
258
  nested_body = resp_nested.read().decode(
299
259
  "utf-8", errors="ignore"
300
260
  )
301
261
  print(
302
- "[DEBUG] Received response from /page/app#/nested endpoint\n"
262
+ "[DEBUG] Received response from /cl/app#/nested endpoint\n"
303
263
  f"Status: {resp_nested.status}\n"
304
264
  f"Body (truncated to 500 chars):\n{nested_body[:500]}"
305
265
  )
@@ -307,37 +267,18 @@ def test_all_in_one_app_endpoints() -> None:
307
267
  assert "<html" in nested_body.lower()
308
268
  except (URLError, HTTPError) as exc:
309
269
  print(
310
- f"[DEBUG] Error while requesting /page/app#/nested endpoint: {exc}"
270
+ f"[DEBUG] Error while requesting /cl/app#/nested endpoint: {exc}"
311
271
  )
312
- pytest.fail("Failed to GET /page/app#/nested endpoint")
272
+ pytest.fail("Failed to GET /cl/app#/nested endpoint")
313
273
 
314
- # "/static/main.css" CSS compiled and serving
315
- # Note: CSS may be compiled asynchronously, so we retry if it's not ready
316
- try:
317
- print(
318
- "[DEBUG] Sending GET request to /static/main.css (with retry)"
319
- )
320
- css_bytes = _wait_for_endpoint(
321
- "http://127.0.0.1:8000/static/main.css",
322
- timeout=60.0,
323
- poll_interval=2.0,
324
- request_timeout=20.0,
325
- )
326
- css_body = css_bytes.decode("utf-8", errors="ignore")
327
- print(
328
- "[DEBUG] Received response from /static/main.css\n"
329
- f"Body (truncated to 500 chars):\n{css_body[:500]}"
330
- )
331
- assert len(css_body.strip()) > 0, "CSS file should not be empty"
332
- except (URLError, HTTPError, TimeoutError, RemoteDisconnected) as exc:
333
- print(f"[DEBUG] Error while requesting /static/main.css: {exc}")
334
- pytest.fail(f"Failed to GET /static/main.css after retries: {exc}")
274
+ # Note: CSS serving is tested separately in test_css_with_image
275
+ # The CSS is bundled into client.js so no separate /static/styles.css endpoint
335
276
 
336
277
  # "/static/assets/burger.png" – static files are loading
337
278
  try:
338
279
  print("[DEBUG] Sending GET request to /static/assets/burger.png")
339
280
  with urlopen(
340
- "http://127.0.0.1:8000/static/assets/burger.png",
281
+ f"http://127.0.0.1:{server_port}/static/assets/burger.png",
341
282
  timeout=20,
342
283
  ) as resp_png:
343
284
  png_bytes = resp_png.read()
@@ -363,7 +304,7 @@ def test_all_in_one_app_endpoints() -> None:
363
304
  "[DEBUG] Sending GET request to /workers/worker.js (with retry)"
364
305
  )
365
306
  worker_js_bytes = _wait_for_endpoint(
366
- "http://127.0.0.1:8000/workers/worker.js",
307
+ f"http://127.0.0.1:{server_port}/workers/worker.js",
367
308
  timeout=60.0,
368
309
  poll_interval=2.0,
369
310
  request_timeout=20.0,
@@ -385,13 +326,18 @@ def test_all_in_one_app_endpoints() -> None:
385
326
  f"Failed to GET /workers/worker.js after retries: {exc}"
386
327
  )
387
328
 
388
- # "/walker/get_server_message" – walkers are integrated and up and running
329
+ # POST /walker/get_server_message – walkers are integrated and up and running
389
330
  try:
390
- print("[DEBUG] Sending GET request to /walker/get_server_message")
391
- with urlopen(
392
- "http://127.0.0.1:8000/walker/get_server_message",
393
- timeout=20,
394
- ) as resp_walker:
331
+ print(
332
+ "[DEBUG] Sending POST request to /walker/get_server_message endpoint"
333
+ )
334
+ req = Request(
335
+ f"http://127.0.0.1:{server_port}/walker/get_server_message",
336
+ data=json.dumps({}).encode("utf-8"),
337
+ headers={"Content-Type": "application/json"},
338
+ method="POST",
339
+ )
340
+ with urlopen(req, timeout=20) as resp_walker:
395
341
  walker_body = resp_walker.read().decode(
396
342
  "utf-8", errors="ignore"
397
343
  )
@@ -401,12 +347,13 @@ def test_all_in_one_app_endpoints() -> None:
401
347
  f"Body (truncated to 500 chars):\n{walker_body[:500]}"
402
348
  )
403
349
  assert resp_walker.status == 200
404
- assert "get_server_message" in walker_body
405
- except (URLError, HTTPError) as exc:
350
+ # The walker reports "hello from a basic walker!"
351
+ assert "hello from a basic walker" in walker_body.lower()
352
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
406
353
  print(
407
354
  f"[DEBUG] Error while requesting /walker/get_server_message: {exc}"
408
355
  )
409
- pytest.fail("Failed to GET /walker/get_server_message")
356
+ pytest.fail("Failed to POST /walker/get_server_message")
410
357
 
411
358
  # POST /walker/create_todo – create a Todo via walker HTTP API
412
359
  try:
@@ -417,7 +364,7 @@ def test_all_in_one_app_endpoints() -> None:
417
364
  "text": "Sample todo from all-in-one app",
418
365
  }
419
366
  req = Request(
420
- "http://127.0.0.1:8000/walker/create_todo",
367
+ f"http://127.0.0.1:{server_port}/walker/create_todo",
421
368
  data=json.dumps(payload).encode("utf-8"),
422
369
  headers={"Content-Type": "application/json"},
423
370
  method="POST",
@@ -434,7 +381,7 @@ def test_all_in_one_app_endpoints() -> None:
434
381
  assert resp_create.status == 200
435
382
  # Basic sanity check: created Todo text should appear in the response payload.
436
383
  assert "Sample todo from all-in-one app" in create_body
437
- except (URLError, HTTPError) as exc:
384
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
438
385
  print(f"[DEBUG] Error while requesting /walker/create_todo: {exc}")
439
386
  pytest.fail("Failed to POST /walker/create_todo")
440
387
 
@@ -448,7 +395,7 @@ def test_all_in_one_app_endpoints() -> None:
448
395
  "password": test_password,
449
396
  }
450
397
  req_register = Request(
451
- "http://127.0.0.1:8000/user/register",
398
+ f"http://127.0.0.1:{server_port}/user/register",
452
399
  data=json.dumps(register_payload).encode("utf-8"),
453
400
  headers={"Content-Type": "application/json"},
454
401
  method="POST",
@@ -463,7 +410,9 @@ def test_all_in_one_app_endpoints() -> None:
463
410
  f"Body (truncated to 500 chars):\n{register_body[:500]}"
464
411
  )
465
412
  assert resp_register.status == 201
466
- register_data = json.loads(register_body)
413
+ register_response = json.loads(register_body)
414
+ # Handle new TransportResponse envelope format
415
+ register_data = register_response.get("data", register_response)
467
416
  assert "username" in register_data
468
417
  assert "token" in register_data
469
418
  assert "root_id" in register_data
@@ -475,7 +424,7 @@ def test_all_in_one_app_endpoints() -> None:
475
424
  f"Token: {register_data['token'][:20]}...\n"
476
425
  f"Root ID: {register_data['root_id']}"
477
426
  )
478
- except (URLError, HTTPError) as exc:
427
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
479
428
  print(f"[DEBUG] Error while requesting /user/register: {exc}")
480
429
  pytest.fail("Failed to POST /user/register")
481
430
 
@@ -487,7 +436,7 @@ def test_all_in_one_app_endpoints() -> None:
487
436
  "password": test_password,
488
437
  }
489
438
  req_login = Request(
490
- "http://127.0.0.1:8000/user/login",
439
+ f"http://127.0.0.1:{server_port}/user/login",
491
440
  data=json.dumps(login_payload).encode("utf-8"),
492
441
  headers={"Content-Type": "application/json"},
493
442
  method="POST",
@@ -500,14 +449,16 @@ def test_all_in_one_app_endpoints() -> None:
500
449
  f"Body (truncated to 500 chars):\n{login_body[:500]}"
501
450
  )
502
451
  assert resp_login.status == 200
503
- login_data = json.loads(login_body)
452
+ login_response = json.loads(login_body)
453
+ # Handle new TransportResponse envelope format
454
+ login_data = login_response.get("data", login_response)
504
455
  assert "token" in login_data
505
456
  assert len(login_data["token"]) > 0
506
457
  print(
507
458
  f"[DEBUG] Successfully logged in user: {test_username}\n"
508
459
  f"Token: {login_data['token'][:20]}..."
509
460
  )
510
- except (URLError, HTTPError) as exc:
461
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
511
462
  print(f"[DEBUG] Error while requesting /user/login: {exc}")
512
463
  pytest.fail("Failed to POST /user/login")
513
464
 
@@ -521,7 +472,7 @@ def test_all_in_one_app_endpoints() -> None:
521
472
  "password": "wrong_password",
522
473
  }
523
474
  req_invalid_login = Request(
524
- "http://127.0.0.1:8000/user/login",
475
+ f"http://127.0.0.1:{server_port}/user/login",
525
476
  data=json.dumps(invalid_login_payload).encode("utf-8"),
526
477
  headers={"Content-Type": "application/json"},
527
478
  method="POST",
@@ -552,14 +503,14 @@ def test_all_in_one_app_endpoints() -> None:
552
503
  assert http_err.code in (400, 401, 403), (
553
504
  f"Expected 400/401/403 for invalid login, got {http_err.code}"
554
505
  )
555
- except URLError as exc:
506
+ except (URLError, RemoteDisconnected) as exc:
556
507
  print(
557
508
  f"[DEBUG] Unexpected error while testing invalid login: {exc}"
558
509
  )
559
510
  pytest.fail("Unexpected error testing invalid login")
560
511
 
561
512
  # Verify TypeScript component is working - check that page loads with TS component
562
- # The /page/app endpoint should serve the app which includes the TypeScript Card component
513
+ # The /cl/app endpoint should serve the app which includes the TypeScript Card component
563
514
  try:
564
515
  print("[DEBUG] Verifying TypeScript component integration")
565
516
  # The page should load successfully (already tested above)
@@ -570,12 +521,10 @@ def test_all_in_one_app_endpoints() -> None:
570
521
  print(f"[DEBUG] Error verifying TypeScript component: {exc}")
571
522
  pytest.fail("Failed to verify TypeScript component integration")
572
523
 
573
- # Verify nested folder imports are working - /page/app#/nested route
524
+ # Verify nested folder imports are working - /cl/app#/nested route
574
525
  # This route uses nested folder imports (components.button and button)
575
526
  try:
576
- print(
577
- "[DEBUG] Verifying nested folder imports via /page/app#/nested"
578
- )
527
+ print("[DEBUG] Verifying nested folder imports via /cl/app#/nested")
579
528
  # The nested route should load successfully (already tested above)
580
529
  # Nested imports are compiled and included in the bundle
581
530
  assert "<html" in nested_body.lower(), (
@@ -607,3 +556,225 @@ def test_all_in_one_app_endpoints() -> None:
607
556
  os.chdir(original_cwd)
608
557
  # Final garbage collection to ensure all resources are released
609
558
  gc.collect()
559
+
560
+
561
+ def test_default_client_app_renders() -> None:
562
+ """Test that a default `jac create --use client` app renders correctly when served.
563
+
564
+ This test validates the out-of-the-box experience:
565
+ 1. Creates a new client app using `jac create --use client`
566
+ 2. Installs packages
567
+ 3. Starts the server
568
+ 4. Validates that the default app renders with expected content
569
+ """
570
+ print("[DEBUG] Starting test_default_client_app_renders")
571
+
572
+ app_name = "e2e-default-app"
573
+
574
+ with tempfile.TemporaryDirectory() as temp_dir:
575
+ print(f"[DEBUG] Created temporary directory at {temp_dir}")
576
+ original_cwd = os.getcwd()
577
+ try:
578
+ os.chdir(temp_dir)
579
+ print(f"[DEBUG] Changed working directory to {temp_dir}")
580
+
581
+ # 1. Create a new default Jac client app
582
+ jac_cmd = get_jac_command()
583
+ env = get_env_with_npm()
584
+ print(
585
+ f"[DEBUG] Running '{' '.join(jac_cmd)} create --use client {app_name}'"
586
+ )
587
+ process = Popen(
588
+ [*jac_cmd, "create", "--use", "client", app_name],
589
+ stdin=PIPE,
590
+ stdout=PIPE,
591
+ stderr=PIPE,
592
+ text=True,
593
+ env=env,
594
+ )
595
+ stdout, stderr = process.communicate()
596
+ returncode = process.returncode
597
+
598
+ print(
599
+ f"[DEBUG] 'jac create --use client' completed returncode={returncode}\n"
600
+ f"STDOUT:\n{stdout}\n"
601
+ f"STDERR:\n{stderr}\n"
602
+ )
603
+
604
+ if returncode != 0 and "unrecognized arguments: --use" in stderr:
605
+ pytest.fail(
606
+ "Test failed: installed `jac` CLI does not support `create --use client`."
607
+ )
608
+
609
+ assert returncode == 0, (
610
+ f"jac create --use client failed\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}\n"
611
+ )
612
+
613
+ project_path = os.path.join(temp_dir, app_name)
614
+ print(f"[DEBUG] Created default Jac client app at {project_path}")
615
+ assert os.path.isdir(project_path)
616
+
617
+ # Verify expected files were created (new structure: main.jac at root)
618
+ main_jac_path = os.path.join(project_path, "main.jac")
619
+ assert os.path.isfile(main_jac_path), (
620
+ "main.jac should exist at project root"
621
+ )
622
+
623
+ # Components are now at root level (not src/components)
624
+ button_jac_path = os.path.join(project_path, "components", "Button.cl.jac")
625
+ assert os.path.isfile(button_jac_path), (
626
+ "components/Button.cl.jac should exist"
627
+ )
628
+
629
+ jac_toml_path = os.path.join(project_path, "jac.toml")
630
+ assert os.path.isfile(jac_toml_path), "jac.toml should exist"
631
+
632
+ # 2. Ensure packages are installed (jac create --use client should have done this)
633
+ # If node_modules doesn't exist, run jac add --npm
634
+ node_modules_path = os.path.join(
635
+ project_path, ".jac", "client", "node_modules"
636
+ )
637
+ if not os.path.isdir(node_modules_path):
638
+ print("[DEBUG] node_modules not found, running 'jac add --npm'")
639
+ jac_add_result = run(
640
+ [*jac_cmd, "add", "--npm"],
641
+ cwd=project_path,
642
+ capture_output=True,
643
+ text=True,
644
+ env=env,
645
+ )
646
+ print(
647
+ f"[DEBUG] 'jac add --npm' completed returncode={jac_add_result.returncode}\n"
648
+ f"STDOUT (truncated):\n{jac_add_result.stdout[:1000]}\n"
649
+ f"STDERR (truncated):\n{jac_add_result.stderr[:1000]}\n"
650
+ )
651
+ if jac_add_result.returncode != 0:
652
+ pytest.fail(
653
+ f"jac add --npm failed\n"
654
+ f"STDOUT:\n{jac_add_result.stdout}\n"
655
+ f"STDERR:\n{jac_add_result.stderr}\n"
656
+ )
657
+
658
+ # 3. Start the server (now uses main.jac at project root)
659
+ server: Popen[bytes] | None = None
660
+ # Use dynamic port allocation to avoid conflicts when running tests in parallel
661
+ server_port = get_free_port()
662
+ try:
663
+ print(
664
+ f"[DEBUG] Starting server with 'jac start main.jac -p {server_port}'"
665
+ )
666
+ server = Popen(
667
+ [*jac_cmd, "start", "main.jac", "-p", str(server_port)],
668
+ cwd=project_path,
669
+ env=env,
670
+ )
671
+
672
+ # Wait for server to be ready
673
+ print(f"[DEBUG] Waiting for server on 127.0.0.1:{server_port}")
674
+ wait_for_port("127.0.0.1", server_port, timeout=90.0)
675
+ print(
676
+ f"[DEBUG] Server is accepting connections on 127.0.0.1:{server_port}"
677
+ )
678
+
679
+ # 4. Test root endpoint - for client-only apps, root serves the HTML app
680
+ # Note: The root endpoint may return 503 while the client bundle is building.
681
+ # We use _wait_for_endpoint to retry on 503 until it's ready.
682
+ try:
683
+ print("[DEBUG] Testing root endpoint / (with retry)")
684
+ root_bytes = _wait_for_endpoint(
685
+ f"http://127.0.0.1:{server_port}",
686
+ timeout=120.0,
687
+ poll_interval=2.0,
688
+ request_timeout=30.0,
689
+ )
690
+ root_body = root_bytes.decode("utf-8", errors="ignore")
691
+ print(
692
+ f"[DEBUG] Root response:\nBody (truncated):\n{root_body[:500]}"
693
+ )
694
+ # For client-only apps, root returns HTML with the React app
695
+ assert "<html" in root_body.lower(), (
696
+ "Root should return HTML for client-only app"
697
+ )
698
+ assert "<script" in root_body.lower(), (
699
+ "Root should include script tag for client bundle"
700
+ )
701
+ except (URLError, HTTPError, TimeoutError) as exc:
702
+ print(f"[DEBUG] Error at root endpoint: {exc}")
703
+ pytest.fail(f"Failed to GET root endpoint: {exc}")
704
+
705
+ # 5. Test client app endpoint - the rendered React app
706
+ try:
707
+ print("[DEBUG] Testing client app endpoint /cl/app")
708
+ page_bytes = _wait_for_endpoint(
709
+ f"http://127.0.0.1:{server_port}/cl/app",
710
+ timeout=120.0,
711
+ poll_interval=2.0,
712
+ request_timeout=30.0,
713
+ )
714
+ page_body = page_bytes.decode("utf-8", errors="ignore")
715
+ print(
716
+ f"[DEBUG] Client app response:\n"
717
+ f"Body (truncated):\n{page_body[:1000]}"
718
+ )
719
+
720
+ # Validate HTML structure
721
+ assert "<html" in page_body.lower(), "Response should contain HTML"
722
+ assert "<body" in page_body.lower(), "Response should contain body"
723
+
724
+ # The page should include the bundled JavaScript
725
+ # that will render "Hello, World!" client-side
726
+ assert (
727
+ "<script" in page_body.lower() or "src=" in page_body.lower()
728
+ ), "Response should include script tags for React app"
729
+
730
+ except (URLError, HTTPError, TimeoutError) as exc:
731
+ print(f"[DEBUG] Error at /cl/app endpoint: {exc}")
732
+ pytest.fail(f"Failed to GET /cl/app endpoint: {exc}")
733
+
734
+ # 6. Test that static JS bundle is being served
735
+ try:
736
+ print("[DEBUG] Testing that client.js bundle is served")
737
+ # Extract the client.js path from the HTML
738
+ import re
739
+
740
+ script_match = re.search(
741
+ r'src="(/static/client\.js[^"]*)"', root_body
742
+ )
743
+ if script_match:
744
+ js_path = script_match.group(1)
745
+ js_url = f"http://127.0.0.1:{server_port}{js_path}"
746
+ print(f"[DEBUG] Fetching JS bundle from {js_url}")
747
+ with urlopen(js_url, timeout=30) as resp:
748
+ js_body = resp.read().decode("utf-8", errors="ignore")
749
+ assert resp.status == 200, "JS bundle should return 200"
750
+ assert len(js_body) > 0, "JS bundle should not be empty"
751
+ print(
752
+ f"[DEBUG] JS bundle fetched successfully "
753
+ f"({len(js_body)} bytes)"
754
+ )
755
+ else:
756
+ print("[DEBUG] Warning: Could not find client.js in HTML")
757
+ except (URLError, HTTPError) as exc:
758
+ print(f"[DEBUG] Warning: Could not verify static assets: {exc}")
759
+ # Not a hard failure - the main page test is sufficient
760
+
761
+ print("[DEBUG] All default app tests passed!")
762
+
763
+ finally:
764
+ if server is not None:
765
+ print("[DEBUG] Terminating server process")
766
+ server.terminate()
767
+ try:
768
+ server.wait(timeout=15)
769
+ print("[DEBUG] Server terminated cleanly")
770
+ except Exception:
771
+ print("[DEBUG] Server did not terminate cleanly, killing")
772
+ server.kill()
773
+ server.wait(timeout=5)
774
+ time.sleep(1)
775
+ gc.collect()
776
+
777
+ finally:
778
+ print(f"[DEBUG] Restoring working directory to {original_cwd}")
779
+ os.chdir(original_cwd)
780
+ gc.collect()