jac-client 0.2.3__py3-none-any.whl → 0.2.8__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 (224) hide show
  1. jac_client/examples/all-in-one/app.jac +494 -347
  2. jac_client/examples/all-in-one/assets/workers/worker.py +5 -0
  3. jac_client/examples/all-in-one/button.jac +1 -1
  4. jac_client/examples/all-in-one/components/CategoryFilter.jac +35 -0
  5. jac_client/examples/all-in-one/components/Header.jac +13 -0
  6. jac_client/examples/all-in-one/components/ProfitOverview.jac +50 -0
  7. jac_client/examples/all-in-one/components/Summary.jac +53 -0
  8. jac_client/examples/all-in-one/components/TransactionForm.jac +158 -0
  9. jac_client/examples/all-in-one/components/TransactionItem.jac +55 -0
  10. jac_client/examples/all-in-one/components/TransactionList.jac +37 -0
  11. jac_client/examples/all-in-one/components/button.jac +1 -1
  12. jac_client/examples/all-in-one/components/navigation.jac +132 -0
  13. jac_client/examples/all-in-one/constants/categories.jac +37 -0
  14. jac_client/examples/all-in-one/constants/clients.jac +13 -0
  15. jac_client/examples/all-in-one/context/BudgetContext.jac +28 -0
  16. jac_client/examples/all-in-one/hooks/useBudget.jac +116 -0
  17. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +36 -0
  18. jac_client/examples/all-in-one/pages/BudgetPlanner.cl.jac +70 -0
  19. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +126 -0
  20. jac_client/examples/all-in-one/pages/FeaturesTest.cl.jac +552 -0
  21. jac_client/examples/all-in-one/pages/FeaturesTest.jac +126 -0
  22. jac_client/examples/all-in-one/pages/LandingPage.jac +101 -0
  23. jac_client/examples/all-in-one/pages/loginPage.jac +132 -0
  24. jac_client/examples/all-in-one/pages/nestedDemo.jac +61 -0
  25. jac_client/examples/all-in-one/pages/notFound.jac +24 -0
  26. jac_client/examples/all-in-one/pages/signupPage.jac +133 -0
  27. jac_client/examples/all-in-one/utils/formatters.jac +52 -0
  28. jac_client/examples/asset-serving/css-with-image/{app.jac → src/app.jac} +4 -4
  29. jac_client/examples/asset-serving/image-asset/{app.jac → src/app.jac} +4 -4
  30. jac_client/examples/asset-serving/import-alias/{app.jac → src/app.jac} +5 -5
  31. jac_client/examples/basic/{app.jac → src/app.jac} +4 -4
  32. jac_client/examples/basic-auth/src/app.jac +371 -0
  33. jac_client/examples/basic-auth-with-router/{app.jac → src/app.jac} +28 -28
  34. jac_client/examples/basic-full-stack/{app.jac → src/app.jac} +166 -127
  35. jac_client/examples/css-styling/js-styling/{app.jac → src/app.jac} +7 -7
  36. jac_client/examples/css-styling/material-ui/{app.jac → src/app.jac} +6 -6
  37. jac_client/examples/css-styling/pure-css/{app.jac → src/app.jac} +7 -7
  38. jac_client/examples/css-styling/sass-example/{app.jac → src/app.jac} +7 -7
  39. jac_client/examples/css-styling/styled-components/{app.jac → src/app.jac} +6 -6
  40. jac_client/examples/css-styling/tailwind-example/{app.jac → src/app.jac} +7 -7
  41. jac_client/examples/full-stack-with-auth/{app.jac → src/app.jac} +47 -47
  42. jac_client/examples/little-x/{app.jac → src/app.jac} +27 -32
  43. jac_client/examples/little-x/src/submit-button.jac +16 -0
  44. jac_client/examples/nested-folders/nested-advance/{ButtonRoot.jac → src/ButtonRoot.jac} +1 -1
  45. jac_client/examples/nested-folders/nested-advance/{app.jac → src/app.jac} +1 -1
  46. jac_client/examples/nested-folders/nested-advance/{level1 → src/level1}/ButtonSecondL.jac +1 -1
  47. jac_client/examples/nested-folders/nested-advance/{level1 → src/level1}/Card.jac +1 -1
  48. jac_client/examples/nested-folders/nested-advance/{level1 → src/level1}/level2/ButtonThirdL.jac +1 -1
  49. jac_client/examples/nested-folders/nested-basic/{app.jac → src/app.jac} +2 -2
  50. jac_client/examples/nested-folders/nested-basic/{button.jac → src/button.jac} +1 -1
  51. jac_client/examples/nested-folders/nested-basic/{components → src/components}/button.jac +1 -1
  52. jac_client/examples/ts-support/src/app.jac +35 -0
  53. jac_client/examples/with-router/{app.jac → src/app.jac} +15 -15
  54. jac_client/plugin/cli.jac +504 -0
  55. jac_client/plugin/client.jac +45 -0
  56. jac_client/plugin/client_runtime.cl.jac +42 -0
  57. jac_client/plugin/impl/client.impl.jac +193 -0
  58. jac_client/plugin/impl/client_runtime.impl.jac +195 -0
  59. jac_client/plugin/impl/vite_client_bundle.impl.jac +72 -0
  60. jac_client/plugin/plugin_config.jac +195 -0
  61. jac_client/plugin/src/__init__.jac +20 -0
  62. jac_client/plugin/src/asset_processor.jac +33 -0
  63. jac_client/plugin/src/babel_processor.jac +18 -0
  64. jac_client/plugin/src/compiler.jac +67 -0
  65. jac_client/plugin/src/config_loader.jac +32 -0
  66. jac_client/plugin/src/impl/asset_processor.impl.jac +127 -0
  67. jac_client/plugin/src/impl/babel_processor.impl.jac +89 -0
  68. jac_client/plugin/src/impl/compiler.impl.jac +288 -0
  69. jac_client/plugin/src/impl/config_loader.impl.jac +119 -0
  70. jac_client/plugin/src/impl/import_processor.impl.jac +33 -0
  71. jac_client/plugin/src/impl/jac_to_js.impl.jac +41 -0
  72. jac_client/plugin/src/impl/package_installer.impl.jac +105 -0
  73. jac_client/plugin/src/impl/vite_bundler.impl.jac +626 -0
  74. jac_client/plugin/src/import_processor.jac +19 -0
  75. jac_client/plugin/src/jac_to_js.jac +35 -0
  76. jac_client/plugin/src/package_installer.jac +26 -0
  77. jac_client/plugin/src/vite_bundler.jac +44 -0
  78. jac_client/plugin/vite_client_bundle.jac +31 -0
  79. jac_client/tests/conftest.py +283 -0
  80. jac_client/tests/fixtures/basic-app/app.jac +2 -2
  81. jac_client/tests/fixtures/cl_file/app.cl.jac +2 -2
  82. jac_client/tests/fixtures/client_app_with_antd/app.jac +1 -1
  83. jac_client/tests/fixtures/js_import/app.jac +5 -5
  84. jac_client/tests/fixtures/spawn_test/app.jac +15 -18
  85. jac_client/tests/fixtures/with-ts/app.jac +35 -0
  86. jac_client/tests/test_cli.py +811 -0
  87. jac_client/tests/test_it.py +592 -97
  88. {jac_client-0.2.3.dist-info → jac_client-0.2.8.dist-info}/METADATA +41 -34
  89. jac_client-0.2.8.dist-info/RECORD +97 -0
  90. {jac_client-0.2.3.dist-info → jac_client-0.2.8.dist-info}/WHEEL +2 -1
  91. jac_client-0.2.8.dist-info/entry_points.txt +4 -0
  92. jac_client-0.2.8.dist-info/top_level.txt +1 -0
  93. jac_client/docs/README.md +0 -689
  94. jac_client/docs/advanced-state.md +0 -1265
  95. jac_client/docs/asset-serving/intro.md +0 -209
  96. jac_client/docs/assets/pipe_line-v2.svg +0 -32
  97. jac_client/docs/assets/pipe_line.png +0 -0
  98. jac_client/docs/file-system/app.jac.md +0 -121
  99. jac_client/docs/file-system/backend-frontend.md +0 -217
  100. jac_client/docs/file-system/intro.md +0 -72
  101. jac_client/docs/file-system/nested-imports.md +0 -348
  102. jac_client/docs/guide-example/intro.md +0 -115
  103. jac_client/docs/guide-example/step-01-setup.md +0 -270
  104. jac_client/docs/guide-example/step-02-components.md +0 -416
  105. jac_client/docs/guide-example/step-03-styling.md +0 -478
  106. jac_client/docs/guide-example/step-04-todo-ui.md +0 -477
  107. jac_client/docs/guide-example/step-05-local-state.md +0 -530
  108. jac_client/docs/guide-example/step-06-events.md +0 -749
  109. jac_client/docs/guide-example/step-07-effects.md +0 -468
  110. jac_client/docs/guide-example/step-08-walkers.md +0 -534
  111. jac_client/docs/guide-example/step-09-authentication.md +0 -586
  112. jac_client/docs/guide-example/step-10-routing.md +0 -539
  113. jac_client/docs/guide-example/step-11-final.md +0 -963
  114. jac_client/docs/imports.md +0 -1141
  115. jac_client/docs/lifecycle-hooks.md +0 -773
  116. jac_client/docs/routing.md +0 -659
  117. jac_client/docs/styling/intro.md +0 -249
  118. jac_client/docs/styling/js-styling.md +0 -367
  119. jac_client/docs/styling/material-ui.md +0 -341
  120. jac_client/docs/styling/pure-css.md +0 -299
  121. jac_client/docs/styling/sass.md +0 -403
  122. jac_client/docs/styling/styled-components.md +0 -395
  123. jac_client/docs/styling/tailwind.md +0 -298
  124. jac_client/examples/all-in-one/.babelrc +0 -9
  125. jac_client/examples/all-in-one/README.md +0 -16
  126. jac_client/examples/all-in-one/assets/burger.png +0 -0
  127. jac_client/examples/all-in-one/package.json +0 -29
  128. jac_client/examples/all-in-one/styles.css +0 -26
  129. jac_client/examples/all-in-one/vite.config.js +0 -28
  130. jac_client/examples/asset-serving/css-with-image/.babelrc +0 -9
  131. jac_client/examples/asset-serving/css-with-image/README.md +0 -91
  132. jac_client/examples/asset-serving/css-with-image/assets/burger.png +0 -0
  133. jac_client/examples/asset-serving/css-with-image/package.json +0 -28
  134. jac_client/examples/asset-serving/css-with-image/styles.css +0 -26
  135. jac_client/examples/asset-serving/css-with-image/vite.config.js +0 -28
  136. jac_client/examples/asset-serving/image-asset/.babelrc +0 -9
  137. jac_client/examples/asset-serving/image-asset/README.md +0 -119
  138. jac_client/examples/asset-serving/image-asset/assets/burger.png +0 -0
  139. jac_client/examples/asset-serving/image-asset/package.json +0 -28
  140. jac_client/examples/asset-serving/image-asset/styles.css +0 -26
  141. jac_client/examples/asset-serving/image-asset/vite.config.js +0 -28
  142. jac_client/examples/asset-serving/import-alias/.babelrc +0 -9
  143. jac_client/examples/asset-serving/import-alias/README.md +0 -83
  144. jac_client/examples/asset-serving/import-alias/assets/burger.png +0 -0
  145. jac_client/examples/asset-serving/import-alias/package.json +0 -28
  146. jac_client/examples/asset-serving/import-alias/vite.config.js +0 -28
  147. jac_client/examples/basic/.babelrc +0 -9
  148. jac_client/examples/basic/README.md +0 -16
  149. jac_client/examples/basic/package.json +0 -27
  150. jac_client/examples/basic/vite.config.js +0 -27
  151. jac_client/examples/basic-auth/.babelrc +0 -9
  152. jac_client/examples/basic-auth/README.md +0 -16
  153. jac_client/examples/basic-auth/app.jac +0 -308
  154. jac_client/examples/basic-auth/package.json +0 -27
  155. jac_client/examples/basic-auth/vite.config.js +0 -27
  156. jac_client/examples/basic-auth-with-router/.babelrc +0 -9
  157. jac_client/examples/basic-auth-with-router/README.md +0 -60
  158. jac_client/examples/basic-auth-with-router/package.json +0 -28
  159. jac_client/examples/basic-auth-with-router/vite.config.js +0 -27
  160. jac_client/examples/basic-full-stack/.babelrc +0 -9
  161. jac_client/examples/basic-full-stack/README.md +0 -18
  162. jac_client/examples/basic-full-stack/package.json +0 -28
  163. jac_client/examples/basic-full-stack/vite.config.js +0 -27
  164. jac_client/examples/css-styling/js-styling/.babelrc +0 -9
  165. jac_client/examples/css-styling/js-styling/README.md +0 -183
  166. jac_client/examples/css-styling/js-styling/package.json +0 -28
  167. jac_client/examples/css-styling/js-styling/styles.js +0 -100
  168. jac_client/examples/css-styling/js-styling/vite.config.js +0 -27
  169. jac_client/examples/css-styling/material-ui/.babelrc +0 -9
  170. jac_client/examples/css-styling/material-ui/README.md +0 -16
  171. jac_client/examples/css-styling/material-ui/package.json +0 -32
  172. jac_client/examples/css-styling/material-ui/vite.config.js +0 -27
  173. jac_client/examples/css-styling/pure-css/.babelrc +0 -9
  174. jac_client/examples/css-styling/pure-css/README.md +0 -16
  175. jac_client/examples/css-styling/pure-css/package.json +0 -28
  176. jac_client/examples/css-styling/pure-css/styles.css +0 -111
  177. jac_client/examples/css-styling/pure-css/vite.config.js +0 -27
  178. jac_client/examples/css-styling/sass-example/.babelrc +0 -9
  179. jac_client/examples/css-styling/sass-example/README.md +0 -16
  180. jac_client/examples/css-styling/sass-example/package.json +0 -29
  181. jac_client/examples/css-styling/sass-example/styles.scss +0 -153
  182. jac_client/examples/css-styling/sass-example/vite.config.js +0 -27
  183. jac_client/examples/css-styling/styled-components/.babelrc +0 -9
  184. jac_client/examples/css-styling/styled-components/README.md +0 -16
  185. jac_client/examples/css-styling/styled-components/package.json +0 -29
  186. jac_client/examples/css-styling/styled-components/styled.js +0 -90
  187. jac_client/examples/css-styling/styled-components/vite.config.js +0 -27
  188. jac_client/examples/css-styling/tailwind-example/.babelrc +0 -9
  189. jac_client/examples/css-styling/tailwind-example/README.md +0 -16
  190. jac_client/examples/css-styling/tailwind-example/global.css +0 -1
  191. jac_client/examples/css-styling/tailwind-example/package.json +0 -30
  192. jac_client/examples/css-styling/tailwind-example/vite.config.js +0 -29
  193. jac_client/examples/full-stack-with-auth/.babelrc +0 -9
  194. jac_client/examples/full-stack-with-auth/README.md +0 -16
  195. jac_client/examples/full-stack-with-auth/package.json +0 -28
  196. jac_client/examples/full-stack-with-auth/vite.config.js +0 -29
  197. jac_client/examples/little-x/package.json +0 -23
  198. jac_client/examples/little-x/submit-button.jac +0 -8
  199. jac_client/examples/nested-folders/nested-advance/.babelrc +0 -9
  200. jac_client/examples/nested-folders/nested-advance/README.md +0 -77
  201. jac_client/examples/nested-folders/nested-advance/package.json +0 -29
  202. jac_client/examples/nested-folders/nested-advance/vite.config.js +0 -28
  203. jac_client/examples/nested-folders/nested-basic/.babelrc +0 -9
  204. jac_client/examples/nested-folders/nested-basic/README.md +0 -183
  205. jac_client/examples/nested-folders/nested-basic/app.js +0 -7
  206. jac_client/examples/nested-folders/nested-basic/package.json +0 -28
  207. jac_client/examples/nested-folders/nested-basic/vite.config.js +0 -27
  208. jac_client/examples/with-router/.babelrc +0 -9
  209. jac_client/examples/with-router/README.md +0 -17
  210. jac_client/examples/with-router/package.json +0 -28
  211. jac_client/examples/with-router/vite.config.js +0 -27
  212. jac_client/plugin/cli.py +0 -244
  213. jac_client/plugin/client.py +0 -152
  214. jac_client/plugin/client_runtime.jac +0 -234
  215. jac_client/plugin/vite_client_bundle.py +0 -503
  216. jac_client/tests/fixtures/js_import/utils.js +0 -21
  217. jac_client/tests/fixtures/package-lock.json +0 -329
  218. jac_client/tests/fixtures/package.json +0 -11
  219. jac_client/tests/test_asset_examples.py +0 -322
  220. jac_client/tests/test_cl.py +0 -530
  221. jac_client/tests/test_create_jac_app.py +0 -131
  222. jac_client/tests/test_nested_file.py +0 -374
  223. jac_client-0.2.3.dist-info/RECORD +0 -171
  224. jac_client-0.2.3.dist-info/entry_points.txt +0 -4
@@ -1,19 +1,64 @@
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
 
5
+ import gc
5
6
  import json
6
7
  import os
7
8
  import shutil
8
9
  import socket
10
+ import sys
9
11
  import tempfile
10
12
  import time
11
- from subprocess import Popen, run
13
+ from http.client import RemoteDisconnected
14
+ from pathlib import Path
15
+ from subprocess import PIPE, Popen, run
12
16
  from urllib.error import HTTPError, URLError
13
17
  from urllib.request import Request, urlopen
14
18
 
15
19
  import pytest
16
20
 
21
+ from jaclang.pycore.runtime import JacRuntime as Jac
22
+
23
+
24
+ def _get_jac_command() -> list[str]:
25
+ """Get the jac command with proper path handling."""
26
+ jac_path = shutil.which("jac")
27
+ if jac_path:
28
+ return [jac_path]
29
+ return [sys.executable, "-m", "jaclang"]
30
+
31
+
32
+ def _get_env_with_npm() -> dict[str, str]:
33
+ """Get environment dict with npm in PATH."""
34
+ env = os.environ.copy()
35
+ npm_path = shutil.which("npm")
36
+ if npm_path:
37
+ npm_dir = str(Path(npm_path).parent)
38
+ current_path = env.get("PATH", "")
39
+ if npm_dir not in current_path:
40
+ env["PATH"] = f"{npm_dir}:{current_path}"
41
+ # Also check common nvm locations
42
+ nvm_dir = os.environ.get("NVM_DIR", os.path.expanduser("~/.nvm"))
43
+ nvm_node_bin = Path(nvm_dir) / "versions" / "node"
44
+ if nvm_node_bin.exists():
45
+ for version_dir in nvm_node_bin.iterdir():
46
+ bin_dir = version_dir / "bin"
47
+ if bin_dir.exists() and (bin_dir / "npm").exists():
48
+ current_path = env.get("PATH", "")
49
+ if str(bin_dir) not in current_path:
50
+ env["PATH"] = f"{bin_dir}:{current_path}"
51
+ break
52
+ return env
53
+
54
+
55
+ @pytest.fixture(autouse=True)
56
+ def reset_jac_machine():
57
+ """Reset Jac machine before and after each test."""
58
+ Jac.reset_machine()
59
+ yield
60
+ Jac.reset_machine()
61
+
17
62
 
18
63
  def _wait_for_port(
19
64
  host: str,
@@ -44,10 +89,68 @@ def _wait_for_port(
44
89
  )
45
90
 
46
91
 
92
+ def _wait_for_endpoint(
93
+ url: str,
94
+ timeout: float = 120.0,
95
+ poll_interval: float = 2.0,
96
+ request_timeout: float = 30.0,
97
+ ) -> bytes:
98
+ """Block until an HTTP endpoint returns a successful response or timeout.
99
+
100
+ Retries on 503 Service Unavailable (temporary) and connection errors.
101
+ Fails immediately on 500 Internal Server Error (permanent errors like compilation failures).
102
+
103
+ Returns:
104
+ The response body as bytes.
105
+
106
+ Raises:
107
+ TimeoutError: if the endpoint does not return success within timeout.
108
+ HTTPError: if the endpoint returns a non-retryable error (e.g., 500).
109
+ """
110
+ deadline = time.time() + timeout
111
+ last_err: Exception | None = None
112
+
113
+ while time.time() < deadline:
114
+ try:
115
+ with urlopen(url, timeout=request_timeout) as resp:
116
+ return resp.read()
117
+ except HTTPError as exc:
118
+ if exc.code == 503:
119
+ # Service Unavailable - retry (temporary condition, e.g., compilation in progress)
120
+ # Close the underlying response to release the socket
121
+ exc.close()
122
+ last_err = exc
123
+ print(f"[DEBUG] Endpoint {url} returned 503, retrying...")
124
+ time.sleep(poll_interval)
125
+ elif exc.code == 500:
126
+ # Internal Server Error - do not retry (permanent error, e.g., compilation failure)
127
+ # Close the underlying response to release the socket
128
+ exc.close()
129
+ # Re-raise immediately - 500 indicates a permanent error that won't resolve by retrying
130
+ raise
131
+ else:
132
+ # Other HTTP errors should not be retried
133
+ raise
134
+ except URLError as exc:
135
+ # Connection errors - retry
136
+ last_err = exc
137
+ print(f"[DEBUG] Endpoint {url} connection error: {exc}, retrying...")
138
+ time.sleep(poll_interval)
139
+ except RemoteDisconnected as exc:
140
+ # Server closed connection - retry
141
+ last_err = exc
142
+ print(f"[DEBUG] Endpoint {url} remote disconnected: {exc}, retrying...")
143
+ time.sleep(poll_interval)
144
+
145
+ raise TimeoutError(
146
+ f"Timed out waiting for {url} to become available. Last error: {last_err}"
147
+ )
148
+
149
+
47
150
  def test_all_in_one_app_endpoints() -> None:
48
- """Create a Jac app, copy @all-in-one into it, run npm install, then verify endpoints."""
151
+ """Create a Jac app, copy @all-in-one into it, install packages from jac.toml, then verify endpoints."""
49
152
  print(
50
- "[DEBUG] Starting test_all_in_one_app_endpoints using jac create_jac_app + @all-in-one"
153
+ "[DEBUG] Starting test_all_in_one_app_endpoints using jac create --cl + @all-in-one"
51
154
  )
52
155
 
53
156
  # Resolve the path to jac_client/examples/all-in-one relative to this test file.
@@ -68,33 +171,33 @@ def test_all_in_one_app_endpoints() -> None:
68
171
  print(f"[DEBUG] Changed working directory to {temp_dir}")
69
172
 
70
173
  # 1. Create a new Jac app via CLI (requires jac + jac-client plugin installed)
71
- print(f"[DEBUG] Running 'jac create_jac_app {app_name}'")
72
- create_result = run(
73
- ["jac", "create_jac_app", app_name],
74
- capture_output=True,
174
+ print(f"[DEBUG] Running 'jac create --cl {app_name}'")
175
+ process = Popen(
176
+ ["jac", "create", "--cl", app_name],
177
+ stdin=PIPE,
178
+ stdout=PIPE,
179
+ stderr=PIPE,
75
180
  text=True,
76
181
  )
182
+ stdout, stderr = process.communicate()
183
+ returncode = process.returncode
184
+
77
185
  print(
78
- "[DEBUG] 'jac create_jac_app' completed "
79
- f"returncode={create_result.returncode}\n"
80
- f"STDOUT:\n{create_result.stdout}\n"
81
- f"STDERR:\n{create_result.stderr}\n"
186
+ "[DEBUG] 'jac create --cl' completed "
187
+ f"returncode={returncode}\n"
188
+ f"STDOUT:\n{stdout}\n"
189
+ f"STDERR:\n{stderr}\n"
82
190
  )
83
191
 
84
- # If the currently installed `jac` CLI does not support `create_jac_app`,
85
- # skip this integration test instead of failing the whole suite.
86
- if (
87
- create_result.returncode != 0
88
- and "invalid choice: 'create_jac_app'" in create_result.stderr
89
- ):
90
- pytest.skip(
91
- "Skipping: installed `jac` CLI does not support `create_jac_app`."
192
+ # If the currently installed `jac` CLI does not support `create --cl`,
193
+ # fail the test instead of skipping it.
194
+ if returncode != 0 and "unrecognized arguments: --cl" in stderr:
195
+ pytest.fail(
196
+ "Test failed: installed `jac` CLI does not support `create --cl`."
92
197
  )
93
198
 
94
- assert create_result.returncode == 0, (
95
- "jac create_jac_app failed\n"
96
- f"STDOUT:\n{create_result.stdout}\n"
97
- f"STDERR:\n{create_result.stderr}\n"
199
+ assert returncode == 0, (
200
+ f"jac create --cl failed\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}\n"
98
201
  )
99
202
 
100
203
  project_path = os.path.join(temp_dir, app_name)
@@ -114,97 +217,107 @@ def test_all_in_one_app_endpoints() -> None:
114
217
  else:
115
218
  shutil.copy2(src, dst)
116
219
 
117
- # 3. Run `npm install` inside the project directory so the frontend can build.
118
- print("[DEBUG] Running 'npm install' in created Jac app")
119
- npm_result = run(
120
- ["npm", "install"],
220
+ # 3. Install packages from jac.toml using `jac add --cl`
221
+ # This reads packages from jac.toml, generates package.json, and runs npm install
222
+ print("[DEBUG] Running 'jac add --cl' to install packages from jac.toml")
223
+ jac_add_result = run(
224
+ ["jac", "add", "--cl"],
121
225
  cwd=project_path,
122
226
  capture_output=True,
123
227
  text=True,
124
228
  )
125
229
  print(
126
- "[DEBUG] 'npm install' completed "
127
- f"returncode={npm_result.returncode}\n"
128
- f"STDOUT (truncated to 2000 chars):\n{npm_result.stdout[:2000]}\n"
129
- f"STDERR (truncated to 2000 chars):\n{npm_result.stderr[:2000]}\n"
230
+ "[DEBUG] 'jac add --cl' completed "
231
+ f"returncode={jac_add_result.returncode}\n"
232
+ f"STDOUT (truncated to 2000 chars):\n{jac_add_result.stdout[:2000]}\n"
233
+ f"STDERR (truncated to 2000 chars):\n{jac_add_result.stderr[:2000]}\n"
130
234
  )
131
235
 
132
- if npm_result.returncode != 0:
133
- pytest.skip(
134
- "Skipping: npm install failed or npm is not available in PATH."
236
+ if jac_add_result.returncode != 0:
237
+ pytest.fail(
238
+ f"Test failed: jac add --cl failed or npm is not available in PATH.\n"
239
+ f"STDOUT:\n{jac_add_result.stdout}\n"
240
+ f"STDERR:\n{jac_add_result.stderr}\n"
135
241
  )
136
242
 
137
243
  app_jac_path = os.path.join(project_path, "app.jac")
138
- assert os.path.isfile(app_jac_path), "all-in-one app.jac file missing"
244
+ assert os.path.isfile(app_jac_path), "all-in-one src/app.jac file missing"
139
245
 
140
- # 4. Start the server: `jac serve app.jac`
246
+ # 4. Start the server: `jac start src/app.jac`
141
247
  # NOTE: We don't use text mode here, so `Popen` defaults to bytes.
142
248
  # Use `Popen[bytes]` in the type annotation to keep mypy happy.
143
249
  server: Popen[bytes] | None = None
144
250
  try:
145
- print("[DEBUG] Starting server with 'jac serve app.jac'")
251
+ print("[DEBUG] Starting server with 'jac start src/app.jac'")
146
252
  server = Popen(
147
- ["jac", "serve", "app.jac"],
253
+ ["jac", "start", "src/app.jac"],
148
254
  cwd=project_path,
149
255
  )
150
-
151
256
  # Wait for localhost:8000 to become available
152
257
  print("[DEBUG] Waiting for server to be available on 127.0.0.1:8000")
153
258
  _wait_for_port("127.0.0.1", 8000, timeout=90.0)
154
259
  print("[DEBUG] Server is now accepting connections on 127.0.0.1:8000")
155
260
 
156
- # "/" – server up
261
+ # "/" – server up (serves client app HTML due to base_route_app="app")
262
+ # Note: The root endpoint may return 503 while the client bundle is building.
263
+ # We use _wait_for_endpoint to retry on 503 until it's ready.
157
264
  try:
158
- print("[DEBUG] Sending GET request to root endpoint /")
159
- with urlopen(
265
+ print("[DEBUG] Sending GET request to root endpoint / (with retry)")
266
+ root_bytes = _wait_for_endpoint(
160
267
  "http://127.0.0.1:8000",
161
- timeout=10,
162
- ) as resp_root:
163
- root_body = resp_root.read().decode("utf-8", errors="ignore")
164
- print(
165
- "[DEBUG] Received response from root endpoint /\n"
166
- f"Status: {resp_root.status}\n"
167
- f"Body (truncated to 500 chars):\n{root_body[:500]}"
168
- )
169
- assert resp_root.status == 200
170
- assert '"Jac API Server"' in root_body
171
- assert '"endpoints"' in root_body
172
- except (URLError, HTTPError) as exc:
268
+ timeout=120.0,
269
+ poll_interval=2.0,
270
+ request_timeout=30.0,
271
+ )
272
+ root_body = root_bytes.decode("utf-8", errors="ignore")
273
+ print(
274
+ "[DEBUG] Received response from root endpoint /\n"
275
+ f"Body (truncated to 500 chars):\n{root_body[:500]}"
276
+ )
277
+ # With base_route_app="app", root serves client HTML
278
+ assert "<!DOCTYPE html>" in root_body or "<html" in root_body
279
+ assert '<div id="root">' in root_body
280
+ except (URLError, HTTPError, TimeoutError) as exc:
173
281
  print(f"[DEBUG] Error while requesting root endpoint: {exc}")
174
282
  pytest.fail(f"Failed to GET root endpoint: {exc}")
175
283
 
176
- # "/page/app" – main page is loading
284
+ # "/cl/app" – main page is loading
285
+ # Note: This endpoint may return 503 (temporary) while the page is being compiled,
286
+ # or 500 (permanent) if there's a compilation error. We use _wait_for_endpoint
287
+ # to retry on 503 until it's ready, but it will fail immediately on 500.
177
288
  try:
178
- print("[DEBUG] Sending GET request to /page/app endpoint")
179
- with urlopen(
180
- "http://127.0.0.1:8000/page/app",
181
- timeout=200,
182
- ) as resp_page:
183
- page_body = resp_page.read().decode("utf-8", errors="ignore")
184
- print(
185
- "[DEBUG] Received response from /page/app endpoint\n"
186
- f"Status: {resp_page.status}\n"
187
- f"Body (truncated to 500 chars):\n{page_body[:500]}"
188
- )
189
- assert resp_page.status == 200
190
- assert "<html" in page_body.lower()
191
- except (URLError, HTTPError) as exc:
192
- print(f"[DEBUG] Error while requesting /page/app endpoint: {exc}")
193
- pytest.fail("Failed to GET /page/app endpoint")
289
+ print(
290
+ "[DEBUG] Sending GET request to /cl/app endpoint (with retry)"
291
+ )
292
+ page_bytes = _wait_for_endpoint(
293
+ "http://127.0.0.1:8000/cl/app",
294
+ timeout=120.0,
295
+ poll_interval=2.0,
296
+ request_timeout=30.0,
297
+ )
298
+ page_body = page_bytes.decode("utf-8", errors="ignore")
299
+ print(
300
+ "[DEBUG] Received response from /cl/app endpoint\n"
301
+ f"Body (truncated to 500 chars):\n{page_body[:500]}"
302
+ )
303
+ assert "<html" in page_body.lower()
304
+ except (URLError, HTTPError, TimeoutError, RemoteDisconnected) as exc:
305
+ print(f"[DEBUG] Error while requesting /cl/app endpoint: {exc}")
306
+ pytest.fail(f"Failed to GET /cl/app endpoint: {exc}")
194
307
 
195
- # "/page/app#/nested" – relative paths / nested route
308
+ # "/cl/app#/nested" – relative paths / nested route
196
309
  # (hash fragment is client-side only but server should still serve the app shell)
197
310
  try:
198
- print("[DEBUG] Sending GET request to /page/app#/nested endpoint")
311
+ print("[DEBUG] Sending GET request to /cl/app#/nested endpoint")
199
312
  with urlopen(
200
- "http://127.0.0.1:8000/page/app#/nested",
313
+ "http://127.0.0.1:8000/cl/app#/nested",
201
314
  timeout=200,
202
315
  ) as resp_nested:
203
316
  nested_body = resp_nested.read().decode(
204
317
  "utf-8", errors="ignore"
205
318
  )
206
319
  print(
207
- "[DEBUG] Received response from /page/app#/nested endpoint\n"
320
+ "[DEBUG] Received response from /cl/app#/nested endpoint\n"
208
321
  f"Status: {resp_nested.status}\n"
209
322
  f"Body (truncated to 500 chars):\n{nested_body[:500]}"
210
323
  )
@@ -212,28 +325,12 @@ def test_all_in_one_app_endpoints() -> None:
212
325
  assert "<html" in nested_body.lower()
213
326
  except (URLError, HTTPError) as exc:
214
327
  print(
215
- f"[DEBUG] Error while requesting /page/app#/nested endpoint: {exc}"
328
+ f"[DEBUG] Error while requesting /cl/app#/nested endpoint: {exc}"
216
329
  )
217
- pytest.fail("Failed to GET /page/app#/nested endpoint")
330
+ pytest.fail("Failed to GET /cl/app#/nested endpoint")
218
331
 
219
- # "/static/main.css" CSS compiled and serving
220
- try:
221
- print("[DEBUG] Sending GET request to /static/main.css")
222
- with urlopen(
223
- "http://127.0.0.1:8000/static/main.css",
224
- timeout=20,
225
- ) as resp_css:
226
- css_body = resp_css.read().decode("utf-8", errors="ignore")
227
- print(
228
- "[DEBUG] Received response from /static/main.css\n"
229
- f"Status: {resp_css.status}\n"
230
- f"Body (truncated to 500 chars):\n{css_body[:500]}"
231
- )
232
- assert resp_css.status == 200
233
- assert len(css_body.strip()) > 0
234
- except (URLError, HTTPError) as exc:
235
- print(f"[DEBUG] Error while requesting /static/main.css: {exc}")
236
- pytest.fail("Failed to GET /static/main.css")
332
+ # Note: CSS serving is tested separately in test_css_with_image
333
+ # The CSS is bundled into client.js so no separate /static/styles.css endpoint
237
334
 
238
335
  # "/static/assets/burger.png" – static files are loading
239
336
  try:
@@ -259,6 +356,34 @@ def test_all_in_one_app_endpoints() -> None:
259
356
  )
260
357
  pytest.fail("Failed to GET /static/assets/burger.png")
261
358
 
359
+ # "/workers/worker.js" – worker script is served
360
+ try:
361
+ print(
362
+ "[DEBUG] Sending GET request to /workers/worker.js (with retry)"
363
+ )
364
+ worker_js_bytes = _wait_for_endpoint(
365
+ "http://127.0.0.1:8000/workers/worker.js",
366
+ timeout=60.0,
367
+ poll_interval=2.0,
368
+ request_timeout=20.0,
369
+ )
370
+ worker_js_body = worker_js_bytes.decode("utf-8", errors="ignore")
371
+ print(
372
+ "[DEBUG] Received response from /workers/worker.js\n"
373
+ f"Body (truncated to 500 chars):\n{worker_js_body[:500]}"
374
+ )
375
+ assert len(worker_js_body.strip()) > 0, (
376
+ "Worker JS should not be empty"
377
+ )
378
+ assert (
379
+ "postMessage" in worker_js_body or "onmessage" in worker_js_body
380
+ ), "Worker JS should contain a message handler"
381
+ except (URLError, HTTPError, TimeoutError, RemoteDisconnected) as exc:
382
+ print(f"[DEBUG] Error while requesting /workers/worker.js: {exc}")
383
+ pytest.fail(
384
+ f"Failed to GET /workers/worker.js after retries: {exc}"
385
+ )
386
+
262
387
  # "/walker/get_server_message" – walkers are integrated and up and running
263
388
  try:
264
389
  print("[DEBUG] Sending GET request to /walker/get_server_message")
@@ -308,10 +433,159 @@ def test_all_in_one_app_endpoints() -> None:
308
433
  assert resp_create.status == 200
309
434
  # Basic sanity check: created Todo text should appear in the response payload.
310
435
  assert "Sample todo from all-in-one app" in create_body
311
- except (URLError, HTTPError) as exc:
436
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
312
437
  print(f"[DEBUG] Error while requesting /walker/create_todo: {exc}")
313
438
  pytest.fail("Failed to POST /walker/create_todo")
314
439
 
440
+ # POST /user/register – register a new user
441
+ test_username = "test_user"
442
+ test_password = "test_password_123"
443
+ try:
444
+ print("[DEBUG] Sending POST request to /user/register endpoint")
445
+ register_payload = {
446
+ "username": test_username,
447
+ "password": test_password,
448
+ }
449
+ req_register = Request(
450
+ "http://127.0.0.1:8000/user/register",
451
+ data=json.dumps(register_payload).encode("utf-8"),
452
+ headers={"Content-Type": "application/json"},
453
+ method="POST",
454
+ )
455
+ with urlopen(req_register, timeout=20) as resp_register:
456
+ register_body = resp_register.read().decode(
457
+ "utf-8", errors="ignore"
458
+ )
459
+ print(
460
+ "[DEBUG] Received response from /user/register\n"
461
+ f"Status: {resp_register.status}\n"
462
+ f"Body (truncated to 500 chars):\n{register_body[:500]}"
463
+ )
464
+ assert resp_register.status == 201
465
+ register_response = json.loads(register_body)
466
+ # Handle new TransportResponse envelope format
467
+ register_data = register_response.get("data", register_response)
468
+ assert "username" in register_data
469
+ assert "token" in register_data
470
+ assert "root_id" in register_data
471
+ assert register_data["username"] == test_username
472
+ assert len(register_data["token"]) > 0
473
+ assert len(register_data["root_id"]) > 0
474
+ print(
475
+ f"[DEBUG] Successfully registered user: {test_username}\n"
476
+ f"Token: {register_data['token'][:20]}...\n"
477
+ f"Root ID: {register_data['root_id']}"
478
+ )
479
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
480
+ print(f"[DEBUG] Error while requesting /user/register: {exc}")
481
+ pytest.fail("Failed to POST /user/register")
482
+
483
+ # POST /user/login – login with registered credentials
484
+ try:
485
+ print("[DEBUG] Sending POST request to /user/login endpoint")
486
+ login_payload = {
487
+ "username": test_username,
488
+ "password": test_password,
489
+ }
490
+ req_login = Request(
491
+ "http://127.0.0.1:8000/user/login",
492
+ data=json.dumps(login_payload).encode("utf-8"),
493
+ headers={"Content-Type": "application/json"},
494
+ method="POST",
495
+ )
496
+ with urlopen(req_login, timeout=20) as resp_login:
497
+ login_body = resp_login.read().decode("utf-8", errors="ignore")
498
+ print(
499
+ "[DEBUG] Received response from /user/login\n"
500
+ f"Status: {resp_login.status}\n"
501
+ f"Body (truncated to 500 chars):\n{login_body[:500]}"
502
+ )
503
+ assert resp_login.status == 200
504
+ login_response = json.loads(login_body)
505
+ # Handle new TransportResponse envelope format
506
+ login_data = login_response.get("data", login_response)
507
+ assert "token" in login_data
508
+ assert len(login_data["token"]) > 0
509
+ print(
510
+ f"[DEBUG] Successfully logged in user: {test_username}\n"
511
+ f"Token: {login_data['token'][:20]}..."
512
+ )
513
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
514
+ print(f"[DEBUG] Error while requesting /user/login: {exc}")
515
+ pytest.fail("Failed to POST /user/login")
516
+
517
+ # POST /user/login – test login with invalid credentials
518
+ try:
519
+ print(
520
+ "[DEBUG] Sending POST request to /user/login with invalid credentials"
521
+ )
522
+ invalid_login_payload = {
523
+ "username": "nonexistent_user",
524
+ "password": "wrong_password",
525
+ }
526
+ req_invalid_login = Request(
527
+ "http://127.0.0.1:8000/user/login",
528
+ data=json.dumps(invalid_login_payload).encode("utf-8"),
529
+ headers={"Content-Type": "application/json"},
530
+ method="POST",
531
+ )
532
+ try:
533
+ with urlopen(req_invalid_login, timeout=20) as resp_invalid:
534
+ # If we get here, the request succeeded but should have failed
535
+ invalid_body = resp_invalid.read().decode(
536
+ "utf-8", errors="ignore"
537
+ )
538
+ print(
539
+ "[DEBUG] Received response from /user/login (invalid creds)\n"
540
+ f"Status: {resp_invalid.status}\n"
541
+ f"Body: {invalid_body}"
542
+ )
543
+ # Login should fail with invalid credentials
544
+ assert (
545
+ resp_invalid.status != 200
546
+ or "error" in invalid_body.lower()
547
+ )
548
+ except HTTPError as http_err:
549
+ # Expected: login should fail with 401 or similar
550
+ print(
551
+ f"[DEBUG] Expected error for invalid login: {http_err.code} {http_err.reason}"
552
+ )
553
+ # Close the underlying response to release the socket
554
+ http_err.close()
555
+ assert http_err.code in (400, 401, 403), (
556
+ f"Expected 400/401/403 for invalid login, got {http_err.code}"
557
+ )
558
+ except (URLError, RemoteDisconnected) as exc:
559
+ print(
560
+ f"[DEBUG] Unexpected error while testing invalid login: {exc}"
561
+ )
562
+ pytest.fail("Unexpected error testing invalid login")
563
+
564
+ # Verify TypeScript component is working - check that page loads with TS component
565
+ # The /cl/app endpoint should serve the app which includes the TypeScript Card component
566
+ try:
567
+ print("[DEBUG] Verifying TypeScript component integration")
568
+ # The page should load successfully (already tested above)
569
+ # TypeScript components are compiled and included in the bundle
570
+ # We verify this by checking the page loads without errors
571
+ assert "<html" in page_body.lower(), "Page should contain HTML"
572
+ except Exception as exc:
573
+ print(f"[DEBUG] Error verifying TypeScript component: {exc}")
574
+ pytest.fail("Failed to verify TypeScript component integration")
575
+
576
+ # Verify nested folder imports are working - /cl/app#/nested route
577
+ # This route uses nested folder imports (components.button and button)
578
+ try:
579
+ print("[DEBUG] Verifying nested folder imports via /cl/app#/nested")
580
+ # The nested route should load successfully (already tested above)
581
+ # Nested imports are compiled and included in the bundle
582
+ assert "<html" in nested_body.lower(), (
583
+ "Nested route should contain HTML"
584
+ )
585
+ except Exception as exc:
586
+ print(f"[DEBUG] Error verifying nested folder imports: {exc}")
587
+ pytest.fail("Failed to verify nested folder imports")
588
+
315
589
  finally:
316
590
  if server is not None:
317
591
  print("[DEBUG] Terminating server process")
@@ -324,6 +598,227 @@ def test_all_in_one_app_endpoints() -> None:
324
598
  "[DEBUG] Server did not terminate cleanly, killing process"
325
599
  )
326
600
  server.kill()
601
+ server.wait(timeout=5)
602
+ # Allow time for sockets to fully close and run garbage collection
603
+ # to clean up any lingering socket objects before temp dir cleanup
604
+ time.sleep(1)
605
+ gc.collect()
327
606
  finally:
328
607
  print(f"[DEBUG] Restoring original working directory to {original_cwd}")
329
608
  os.chdir(original_cwd)
609
+ # Final garbage collection to ensure all resources are released
610
+ gc.collect()
611
+
612
+
613
+ def test_default_client_app_renders() -> None:
614
+ """Test that a default `jac create --cl` app renders correctly when served.
615
+
616
+ This test validates the out-of-the-box experience:
617
+ 1. Creates a new client app using `jac create --cl`
618
+ 2. Installs packages
619
+ 3. Starts the server
620
+ 4. Validates that the default app renders with expected content
621
+ """
622
+ print("[DEBUG] Starting test_default_client_app_renders")
623
+
624
+ app_name = "e2e-default-app"
625
+
626
+ with tempfile.TemporaryDirectory() as temp_dir:
627
+ print(f"[DEBUG] Created temporary directory at {temp_dir}")
628
+ original_cwd = os.getcwd()
629
+ try:
630
+ os.chdir(temp_dir)
631
+ print(f"[DEBUG] Changed working directory to {temp_dir}")
632
+
633
+ # 1. Create a new default Jac client app
634
+ jac_cmd = _get_jac_command()
635
+ env = _get_env_with_npm()
636
+ print(f"[DEBUG] Running '{' '.join(jac_cmd)} create --cl {app_name}'")
637
+ process = Popen(
638
+ [*jac_cmd, "create", "--cl", app_name],
639
+ stdin=PIPE,
640
+ stdout=PIPE,
641
+ stderr=PIPE,
642
+ text=True,
643
+ env=env,
644
+ )
645
+ stdout, stderr = process.communicate()
646
+ returncode = process.returncode
647
+
648
+ print(
649
+ f"[DEBUG] 'jac create --cl' completed returncode={returncode}\n"
650
+ f"STDOUT:\n{stdout}\n"
651
+ f"STDERR:\n{stderr}\n"
652
+ )
653
+
654
+ if returncode != 0 and "unrecognized arguments: --cl" in stderr:
655
+ pytest.fail(
656
+ "Test failed: installed `jac` CLI does not support `create --cl`."
657
+ )
658
+
659
+ assert returncode == 0, (
660
+ f"jac create --cl failed\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}\n"
661
+ )
662
+
663
+ project_path = os.path.join(temp_dir, app_name)
664
+ print(f"[DEBUG] Created default Jac client app at {project_path}")
665
+ assert os.path.isdir(project_path)
666
+
667
+ # Verify expected files were created (new structure: main.jac at root)
668
+ main_jac_path = os.path.join(project_path, "main.jac")
669
+ assert os.path.isfile(main_jac_path), (
670
+ "main.jac should exist at project root"
671
+ )
672
+
673
+ # Components are now at root level (not src/components)
674
+ button_jac_path = os.path.join(project_path, "components", "Button.cl.jac")
675
+ assert os.path.isfile(button_jac_path), (
676
+ "components/Button.cl.jac should exist"
677
+ )
678
+
679
+ jac_toml_path = os.path.join(project_path, "jac.toml")
680
+ assert os.path.isfile(jac_toml_path), "jac.toml should exist"
681
+
682
+ # 2. Ensure packages are installed (jac create --cl should have done this)
683
+ # If node_modules doesn't exist, run jac add --cl
684
+ node_modules_path = os.path.join(
685
+ project_path, ".jac", "client", "node_modules"
686
+ )
687
+ if not os.path.isdir(node_modules_path):
688
+ print("[DEBUG] node_modules not found, running 'jac add --cl'")
689
+ jac_add_result = run(
690
+ [*jac_cmd, "add", "--cl"],
691
+ cwd=project_path,
692
+ capture_output=True,
693
+ text=True,
694
+ env=env,
695
+ )
696
+ print(
697
+ f"[DEBUG] 'jac add --cl' completed returncode={jac_add_result.returncode}\n"
698
+ f"STDOUT (truncated):\n{jac_add_result.stdout[:1000]}\n"
699
+ f"STDERR (truncated):\n{jac_add_result.stderr[:1000]}\n"
700
+ )
701
+ if jac_add_result.returncode != 0:
702
+ pytest.fail(
703
+ f"jac add --cl failed\n"
704
+ f"STDOUT:\n{jac_add_result.stdout}\n"
705
+ f"STDERR:\n{jac_add_result.stderr}\n"
706
+ )
707
+
708
+ # 3. Start the server (now uses main.jac at project root)
709
+ server: Popen[bytes] | None = None
710
+ try:
711
+ print("[DEBUG] Starting server with 'jac start main.jac'")
712
+ server = Popen(
713
+ [*jac_cmd, "start", "main.jac"],
714
+ cwd=project_path,
715
+ env=env,
716
+ )
717
+
718
+ # Wait for server to be ready
719
+ print("[DEBUG] Waiting for server on 127.0.0.1:8000")
720
+ _wait_for_port("127.0.0.1", 8000, timeout=90.0)
721
+ print("[DEBUG] Server is accepting connections")
722
+
723
+ # 4. Test root endpoint - for client-only apps, root serves the HTML app
724
+ # Note: The root endpoint may return 503 while the client bundle is building.
725
+ # We use _wait_for_endpoint to retry on 503 until it's ready.
726
+ try:
727
+ print("[DEBUG] Testing root endpoint / (with retry)")
728
+ root_bytes = _wait_for_endpoint(
729
+ "http://127.0.0.1:8000",
730
+ timeout=120.0,
731
+ poll_interval=2.0,
732
+ request_timeout=30.0,
733
+ )
734
+ root_body = root_bytes.decode("utf-8", errors="ignore")
735
+ print(
736
+ f"[DEBUG] Root response:\nBody (truncated):\n{root_body[:500]}"
737
+ )
738
+ # For client-only apps, root returns HTML with the React app
739
+ assert "<html" in root_body.lower(), (
740
+ "Root should return HTML for client-only app"
741
+ )
742
+ assert "<script" in root_body.lower(), (
743
+ "Root should include script tag for client bundle"
744
+ )
745
+ except (URLError, HTTPError, TimeoutError) as exc:
746
+ print(f"[DEBUG] Error at root endpoint: {exc}")
747
+ pytest.fail(f"Failed to GET root endpoint: {exc}")
748
+
749
+ # 5. Test client app endpoint - the rendered React app
750
+ try:
751
+ print("[DEBUG] Testing client app endpoint /cl/app")
752
+ page_bytes = _wait_for_endpoint(
753
+ "http://127.0.0.1:8000/cl/app",
754
+ timeout=120.0,
755
+ poll_interval=2.0,
756
+ request_timeout=30.0,
757
+ )
758
+ page_body = page_bytes.decode("utf-8", errors="ignore")
759
+ print(
760
+ f"[DEBUG] Client app response:\n"
761
+ f"Body (truncated):\n{page_body[:1000]}"
762
+ )
763
+
764
+ # Validate HTML structure
765
+ assert "<html" in page_body.lower(), "Response should contain HTML"
766
+ assert "<body" in page_body.lower(), "Response should contain body"
767
+
768
+ # The page should include the bundled JavaScript
769
+ # that will render "Hello, World!" client-side
770
+ assert (
771
+ "<script" in page_body.lower() or "src=" in page_body.lower()
772
+ ), "Response should include script tags for React app"
773
+
774
+ except (URLError, HTTPError, TimeoutError) as exc:
775
+ print(f"[DEBUG] Error at /cl/app endpoint: {exc}")
776
+ pytest.fail(f"Failed to GET /cl/app endpoint: {exc}")
777
+
778
+ # 6. Test that static JS bundle is being served
779
+ try:
780
+ print("[DEBUG] Testing that client.js bundle is served")
781
+ # Extract the client.js path from the HTML
782
+ import re
783
+
784
+ script_match = re.search(
785
+ r'src="(/static/client\.js[^"]*)"', root_body
786
+ )
787
+ if script_match:
788
+ js_path = script_match.group(1)
789
+ js_url = f"http://127.0.0.1:8000{js_path}"
790
+ print(f"[DEBUG] Fetching JS bundle from {js_url}")
791
+ with urlopen(js_url, timeout=30) as resp:
792
+ js_body = resp.read().decode("utf-8", errors="ignore")
793
+ assert resp.status == 200, "JS bundle should return 200"
794
+ assert len(js_body) > 0, "JS bundle should not be empty"
795
+ print(
796
+ f"[DEBUG] JS bundle fetched successfully "
797
+ f"({len(js_body)} bytes)"
798
+ )
799
+ else:
800
+ print("[DEBUG] Warning: Could not find client.js in HTML")
801
+ except (URLError, HTTPError) as exc:
802
+ print(f"[DEBUG] Warning: Could not verify static assets: {exc}")
803
+ # Not a hard failure - the main page test is sufficient
804
+
805
+ print("[DEBUG] All default app tests passed!")
806
+
807
+ finally:
808
+ if server is not None:
809
+ print("[DEBUG] Terminating server process")
810
+ server.terminate()
811
+ try:
812
+ server.wait(timeout=15)
813
+ print("[DEBUG] Server terminated cleanly")
814
+ except Exception:
815
+ print("[DEBUG] Server did not terminate cleanly, killing")
816
+ server.kill()
817
+ server.wait(timeout=5)
818
+ time.sleep(1)
819
+ gc.collect()
820
+
821
+ finally:
822
+ print(f"[DEBUG] Restoring working directory to {original_cwd}")
823
+ os.chdir(original_cwd)
824
+ gc.collect()