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.
- jac_client/examples/all-in-one/app.jac +494 -347
- jac_client/examples/all-in-one/assets/workers/worker.py +5 -0
- jac_client/examples/all-in-one/button.jac +1 -1
- jac_client/examples/all-in-one/components/CategoryFilter.jac +35 -0
- jac_client/examples/all-in-one/components/Header.jac +13 -0
- jac_client/examples/all-in-one/components/ProfitOverview.jac +50 -0
- jac_client/examples/all-in-one/components/Summary.jac +53 -0
- jac_client/examples/all-in-one/components/TransactionForm.jac +158 -0
- jac_client/examples/all-in-one/components/TransactionItem.jac +55 -0
- jac_client/examples/all-in-one/components/TransactionList.jac +37 -0
- jac_client/examples/all-in-one/components/button.jac +1 -1
- jac_client/examples/all-in-one/components/navigation.jac +132 -0
- jac_client/examples/all-in-one/constants/categories.jac +37 -0
- jac_client/examples/all-in-one/constants/clients.jac +13 -0
- jac_client/examples/all-in-one/context/BudgetContext.jac +28 -0
- jac_client/examples/all-in-one/hooks/useBudget.jac +116 -0
- jac_client/examples/all-in-one/hooks/useLocalStorage.jac +36 -0
- jac_client/examples/all-in-one/pages/BudgetPlanner.cl.jac +70 -0
- jac_client/examples/all-in-one/pages/BudgetPlanner.jac +126 -0
- jac_client/examples/all-in-one/pages/FeaturesTest.cl.jac +552 -0
- jac_client/examples/all-in-one/pages/FeaturesTest.jac +126 -0
- jac_client/examples/all-in-one/pages/LandingPage.jac +101 -0
- jac_client/examples/all-in-one/pages/loginPage.jac +132 -0
- jac_client/examples/all-in-one/pages/nestedDemo.jac +61 -0
- jac_client/examples/all-in-one/pages/notFound.jac +24 -0
- jac_client/examples/all-in-one/pages/signupPage.jac +133 -0
- jac_client/examples/all-in-one/utils/formatters.jac +52 -0
- jac_client/examples/asset-serving/css-with-image/{app.jac → src/app.jac} +4 -4
- jac_client/examples/asset-serving/image-asset/{app.jac → src/app.jac} +4 -4
- jac_client/examples/asset-serving/import-alias/{app.jac → src/app.jac} +5 -5
- jac_client/examples/basic/{app.jac → src/app.jac} +4 -4
- jac_client/examples/basic-auth/src/app.jac +371 -0
- jac_client/examples/basic-auth-with-router/{app.jac → src/app.jac} +28 -28
- jac_client/examples/basic-full-stack/{app.jac → src/app.jac} +166 -127
- jac_client/examples/css-styling/js-styling/{app.jac → src/app.jac} +7 -7
- jac_client/examples/css-styling/material-ui/{app.jac → src/app.jac} +6 -6
- jac_client/examples/css-styling/pure-css/{app.jac → src/app.jac} +7 -7
- jac_client/examples/css-styling/sass-example/{app.jac → src/app.jac} +7 -7
- jac_client/examples/css-styling/styled-components/{app.jac → src/app.jac} +6 -6
- jac_client/examples/css-styling/tailwind-example/{app.jac → src/app.jac} +7 -7
- jac_client/examples/full-stack-with-auth/{app.jac → src/app.jac} +47 -47
- jac_client/examples/little-x/{app.jac → src/app.jac} +27 -32
- jac_client/examples/little-x/src/submit-button.jac +16 -0
- jac_client/examples/nested-folders/nested-advance/{ButtonRoot.jac → src/ButtonRoot.jac} +1 -1
- jac_client/examples/nested-folders/nested-advance/{app.jac → src/app.jac} +1 -1
- jac_client/examples/nested-folders/nested-advance/{level1 → src/level1}/ButtonSecondL.jac +1 -1
- jac_client/examples/nested-folders/nested-advance/{level1 → src/level1}/Card.jac +1 -1
- jac_client/examples/nested-folders/nested-advance/{level1 → src/level1}/level2/ButtonThirdL.jac +1 -1
- jac_client/examples/nested-folders/nested-basic/{app.jac → src/app.jac} +2 -2
- jac_client/examples/nested-folders/nested-basic/{button.jac → src/button.jac} +1 -1
- jac_client/examples/nested-folders/nested-basic/{components → src/components}/button.jac +1 -1
- jac_client/examples/ts-support/src/app.jac +35 -0
- jac_client/examples/with-router/{app.jac → src/app.jac} +15 -15
- jac_client/plugin/cli.jac +504 -0
- jac_client/plugin/client.jac +45 -0
- jac_client/plugin/client_runtime.cl.jac +42 -0
- jac_client/plugin/impl/client.impl.jac +193 -0
- jac_client/plugin/impl/client_runtime.impl.jac +195 -0
- jac_client/plugin/impl/vite_client_bundle.impl.jac +72 -0
- jac_client/plugin/plugin_config.jac +195 -0
- jac_client/plugin/src/__init__.jac +20 -0
- jac_client/plugin/src/asset_processor.jac +33 -0
- jac_client/plugin/src/babel_processor.jac +18 -0
- jac_client/plugin/src/compiler.jac +67 -0
- jac_client/plugin/src/config_loader.jac +32 -0
- jac_client/plugin/src/impl/asset_processor.impl.jac +127 -0
- jac_client/plugin/src/impl/babel_processor.impl.jac +89 -0
- jac_client/plugin/src/impl/compiler.impl.jac +288 -0
- jac_client/plugin/src/impl/config_loader.impl.jac +119 -0
- jac_client/plugin/src/impl/import_processor.impl.jac +33 -0
- jac_client/plugin/src/impl/jac_to_js.impl.jac +41 -0
- jac_client/plugin/src/impl/package_installer.impl.jac +105 -0
- jac_client/plugin/src/impl/vite_bundler.impl.jac +626 -0
- jac_client/plugin/src/import_processor.jac +19 -0
- jac_client/plugin/src/jac_to_js.jac +35 -0
- jac_client/plugin/src/package_installer.jac +26 -0
- jac_client/plugin/src/vite_bundler.jac +44 -0
- jac_client/plugin/vite_client_bundle.jac +31 -0
- jac_client/tests/conftest.py +283 -0
- jac_client/tests/fixtures/basic-app/app.jac +2 -2
- jac_client/tests/fixtures/cl_file/app.cl.jac +2 -2
- jac_client/tests/fixtures/client_app_with_antd/app.jac +1 -1
- jac_client/tests/fixtures/js_import/app.jac +5 -5
- jac_client/tests/fixtures/spawn_test/app.jac +15 -18
- jac_client/tests/fixtures/with-ts/app.jac +35 -0
- jac_client/tests/test_cli.py +811 -0
- jac_client/tests/test_it.py +592 -97
- {jac_client-0.2.3.dist-info → jac_client-0.2.8.dist-info}/METADATA +41 -34
- jac_client-0.2.8.dist-info/RECORD +97 -0
- {jac_client-0.2.3.dist-info → jac_client-0.2.8.dist-info}/WHEEL +2 -1
- jac_client-0.2.8.dist-info/entry_points.txt +4 -0
- jac_client-0.2.8.dist-info/top_level.txt +1 -0
- jac_client/docs/README.md +0 -689
- jac_client/docs/advanced-state.md +0 -1265
- jac_client/docs/asset-serving/intro.md +0 -209
- jac_client/docs/assets/pipe_line-v2.svg +0 -32
- jac_client/docs/assets/pipe_line.png +0 -0
- jac_client/docs/file-system/app.jac.md +0 -121
- jac_client/docs/file-system/backend-frontend.md +0 -217
- jac_client/docs/file-system/intro.md +0 -72
- jac_client/docs/file-system/nested-imports.md +0 -348
- jac_client/docs/guide-example/intro.md +0 -115
- jac_client/docs/guide-example/step-01-setup.md +0 -270
- jac_client/docs/guide-example/step-02-components.md +0 -416
- jac_client/docs/guide-example/step-03-styling.md +0 -478
- jac_client/docs/guide-example/step-04-todo-ui.md +0 -477
- jac_client/docs/guide-example/step-05-local-state.md +0 -530
- jac_client/docs/guide-example/step-06-events.md +0 -749
- jac_client/docs/guide-example/step-07-effects.md +0 -468
- jac_client/docs/guide-example/step-08-walkers.md +0 -534
- jac_client/docs/guide-example/step-09-authentication.md +0 -586
- jac_client/docs/guide-example/step-10-routing.md +0 -539
- jac_client/docs/guide-example/step-11-final.md +0 -963
- jac_client/docs/imports.md +0 -1141
- jac_client/docs/lifecycle-hooks.md +0 -773
- jac_client/docs/routing.md +0 -659
- jac_client/docs/styling/intro.md +0 -249
- jac_client/docs/styling/js-styling.md +0 -367
- jac_client/docs/styling/material-ui.md +0 -341
- jac_client/docs/styling/pure-css.md +0 -299
- jac_client/docs/styling/sass.md +0 -403
- jac_client/docs/styling/styled-components.md +0 -395
- jac_client/docs/styling/tailwind.md +0 -298
- jac_client/examples/all-in-one/.babelrc +0 -9
- jac_client/examples/all-in-one/README.md +0 -16
- jac_client/examples/all-in-one/assets/burger.png +0 -0
- jac_client/examples/all-in-one/package.json +0 -29
- jac_client/examples/all-in-one/styles.css +0 -26
- jac_client/examples/all-in-one/vite.config.js +0 -28
- jac_client/examples/asset-serving/css-with-image/.babelrc +0 -9
- jac_client/examples/asset-serving/css-with-image/README.md +0 -91
- jac_client/examples/asset-serving/css-with-image/assets/burger.png +0 -0
- jac_client/examples/asset-serving/css-with-image/package.json +0 -28
- jac_client/examples/asset-serving/css-with-image/styles.css +0 -26
- jac_client/examples/asset-serving/css-with-image/vite.config.js +0 -28
- jac_client/examples/asset-serving/image-asset/.babelrc +0 -9
- jac_client/examples/asset-serving/image-asset/README.md +0 -119
- jac_client/examples/asset-serving/image-asset/assets/burger.png +0 -0
- jac_client/examples/asset-serving/image-asset/package.json +0 -28
- jac_client/examples/asset-serving/image-asset/styles.css +0 -26
- jac_client/examples/asset-serving/image-asset/vite.config.js +0 -28
- jac_client/examples/asset-serving/import-alias/.babelrc +0 -9
- jac_client/examples/asset-serving/import-alias/README.md +0 -83
- jac_client/examples/asset-serving/import-alias/assets/burger.png +0 -0
- jac_client/examples/asset-serving/import-alias/package.json +0 -28
- jac_client/examples/asset-serving/import-alias/vite.config.js +0 -28
- jac_client/examples/basic/.babelrc +0 -9
- jac_client/examples/basic/README.md +0 -16
- jac_client/examples/basic/package.json +0 -27
- jac_client/examples/basic/vite.config.js +0 -27
- jac_client/examples/basic-auth/.babelrc +0 -9
- jac_client/examples/basic-auth/README.md +0 -16
- jac_client/examples/basic-auth/app.jac +0 -308
- jac_client/examples/basic-auth/package.json +0 -27
- jac_client/examples/basic-auth/vite.config.js +0 -27
- jac_client/examples/basic-auth-with-router/.babelrc +0 -9
- jac_client/examples/basic-auth-with-router/README.md +0 -60
- jac_client/examples/basic-auth-with-router/package.json +0 -28
- jac_client/examples/basic-auth-with-router/vite.config.js +0 -27
- jac_client/examples/basic-full-stack/.babelrc +0 -9
- jac_client/examples/basic-full-stack/README.md +0 -18
- jac_client/examples/basic-full-stack/package.json +0 -28
- jac_client/examples/basic-full-stack/vite.config.js +0 -27
- jac_client/examples/css-styling/js-styling/.babelrc +0 -9
- jac_client/examples/css-styling/js-styling/README.md +0 -183
- jac_client/examples/css-styling/js-styling/package.json +0 -28
- jac_client/examples/css-styling/js-styling/styles.js +0 -100
- jac_client/examples/css-styling/js-styling/vite.config.js +0 -27
- jac_client/examples/css-styling/material-ui/.babelrc +0 -9
- jac_client/examples/css-styling/material-ui/README.md +0 -16
- jac_client/examples/css-styling/material-ui/package.json +0 -32
- jac_client/examples/css-styling/material-ui/vite.config.js +0 -27
- jac_client/examples/css-styling/pure-css/.babelrc +0 -9
- jac_client/examples/css-styling/pure-css/README.md +0 -16
- jac_client/examples/css-styling/pure-css/package.json +0 -28
- jac_client/examples/css-styling/pure-css/styles.css +0 -111
- jac_client/examples/css-styling/pure-css/vite.config.js +0 -27
- jac_client/examples/css-styling/sass-example/.babelrc +0 -9
- jac_client/examples/css-styling/sass-example/README.md +0 -16
- jac_client/examples/css-styling/sass-example/package.json +0 -29
- jac_client/examples/css-styling/sass-example/styles.scss +0 -153
- jac_client/examples/css-styling/sass-example/vite.config.js +0 -27
- jac_client/examples/css-styling/styled-components/.babelrc +0 -9
- jac_client/examples/css-styling/styled-components/README.md +0 -16
- jac_client/examples/css-styling/styled-components/package.json +0 -29
- jac_client/examples/css-styling/styled-components/styled.js +0 -90
- jac_client/examples/css-styling/styled-components/vite.config.js +0 -27
- jac_client/examples/css-styling/tailwind-example/.babelrc +0 -9
- jac_client/examples/css-styling/tailwind-example/README.md +0 -16
- jac_client/examples/css-styling/tailwind-example/global.css +0 -1
- jac_client/examples/css-styling/tailwind-example/package.json +0 -30
- jac_client/examples/css-styling/tailwind-example/vite.config.js +0 -29
- jac_client/examples/full-stack-with-auth/.babelrc +0 -9
- jac_client/examples/full-stack-with-auth/README.md +0 -16
- jac_client/examples/full-stack-with-auth/package.json +0 -28
- jac_client/examples/full-stack-with-auth/vite.config.js +0 -29
- jac_client/examples/little-x/package.json +0 -23
- jac_client/examples/little-x/submit-button.jac +0 -8
- jac_client/examples/nested-folders/nested-advance/.babelrc +0 -9
- jac_client/examples/nested-folders/nested-advance/README.md +0 -77
- jac_client/examples/nested-folders/nested-advance/package.json +0 -29
- jac_client/examples/nested-folders/nested-advance/vite.config.js +0 -28
- jac_client/examples/nested-folders/nested-basic/.babelrc +0 -9
- jac_client/examples/nested-folders/nested-basic/README.md +0 -183
- jac_client/examples/nested-folders/nested-basic/app.js +0 -7
- jac_client/examples/nested-folders/nested-basic/package.json +0 -28
- jac_client/examples/nested-folders/nested-basic/vite.config.js +0 -27
- jac_client/examples/with-router/.babelrc +0 -9
- jac_client/examples/with-router/README.md +0 -17
- jac_client/examples/with-router/package.json +0 -28
- jac_client/examples/with-router/vite.config.js +0 -27
- jac_client/plugin/cli.py +0 -244
- jac_client/plugin/client.py +0 -152
- jac_client/plugin/client_runtime.jac +0 -234
- jac_client/plugin/vite_client_bundle.py +0 -503
- jac_client/tests/fixtures/js_import/utils.js +0 -21
- jac_client/tests/fixtures/package-lock.json +0 -329
- jac_client/tests/fixtures/package.json +0 -11
- jac_client/tests/test_asset_examples.py +0 -322
- jac_client/tests/test_cl.py +0 -530
- jac_client/tests/test_create_jac_app.py +0 -131
- jac_client/tests/test_nested_file.py +0 -374
- jac_client-0.2.3.dist-info/RECORD +0 -171
- jac_client-0.2.3.dist-info/entry_points.txt +0 -4
jac_client/tests/test_it.py
CHANGED
|
@@ -1,19 +1,64 @@
|
|
|
1
|
-
"""End-to-end tests for `jac
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
72
|
-
|
|
73
|
-
["jac", "
|
|
74
|
-
|
|
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
|
|
79
|
-
f"returncode={
|
|
80
|
-
f"STDOUT:\n{
|
|
81
|
-
f"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 `
|
|
85
|
-
#
|
|
86
|
-
if
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
95
|
-
"jac
|
|
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.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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] '
|
|
127
|
-
f"returncode={
|
|
128
|
-
f"STDOUT (truncated to 2000 chars):\n{
|
|
129
|
-
f"STDERR (truncated to 2000 chars):\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
|
|
133
|
-
pytest.
|
|
134
|
-
"
|
|
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
|
|
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
|
|
251
|
+
print("[DEBUG] Starting server with 'jac start src/app.jac'")
|
|
146
252
|
server = Popen(
|
|
147
|
-
["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
|
-
|
|
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=
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
# "/
|
|
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(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
# "/
|
|
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 /
|
|
311
|
+
print("[DEBUG] Sending GET request to /cl/app#/nested endpoint")
|
|
199
312
|
with urlopen(
|
|
200
|
-
"http://127.0.0.1:8000/
|
|
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 /
|
|
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 /
|
|
328
|
+
f"[DEBUG] Error while requesting /cl/app#/nested endpoint: {exc}"
|
|
216
329
|
)
|
|
217
|
-
pytest.fail("Failed to GET /
|
|
330
|
+
pytest.fail("Failed to GET /cl/app#/nested endpoint")
|
|
218
331
|
|
|
219
|
-
#
|
|
220
|
-
|
|
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()
|