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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. jac_client/examples/all-in-one/{app.jac → main.jac} +5 -5
  2. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +8 -1
  3. jac_client/examples/all-in-one/pages/FeaturesTest.jac +16 -1
  4. jac_client/examples/all-in-one/pages/{FeaturesTest.cl.jac → features_test_ui.cl.jac} +11 -0
  5. jac_client/examples/all-in-one/pages/nestedDemo.jac +1 -1
  6. jac_client/examples/all-in-one/pages/notFound.jac +2 -7
  7. jac_client/plugin/cli.jac +491 -411
  8. jac_client/plugin/client.jac +25 -0
  9. jac_client/plugin/client_runtime.cl.jac +5 -1
  10. jac_client/plugin/impl/client.impl.jac +96 -55
  11. jac_client/plugin/impl/client_runtime.impl.jac +154 -0
  12. jac_client/plugin/plugin_config.jac +243 -15
  13. jac_client/plugin/src/config_loader.jac +1 -0
  14. jac_client/plugin/src/desktop_config.jac +31 -0
  15. jac_client/plugin/src/impl/compiler.impl.jac +1 -1
  16. jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
  17. jac_client/plugin/src/impl/desktop_config.impl.jac +191 -0
  18. jac_client/plugin/src/impl/vite_bundler.impl.jac +97 -16
  19. jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
  20. jac_client/plugin/src/targets/desktop_target.jac +37 -0
  21. jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2334 -0
  22. jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
  23. jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
  24. jac_client/plugin/src/targets/register.jac +21 -0
  25. jac_client/plugin/src/targets/registry.jac +87 -0
  26. jac_client/plugin/src/targets/web_target.jac +35 -0
  27. jac_client/plugin/src/vite_bundler.jac +6 -0
  28. jac_client/plugin/utils/__init__.jac +1 -0
  29. jac_client/plugin/utils/impl/node_installer.impl.jac +249 -0
  30. jac_client/plugin/utils/node_installer.jac +41 -0
  31. jac_client/templates/client.jacpack +72 -0
  32. jac_client/templates/fullstack.jacpack +61 -0
  33. jac_client/tests/conftest.py +48 -7
  34. jac_client/tests/test_cli.py +184 -70
  35. jac_client/tests/test_e2e.py +232 -0
  36. jac_client/tests/test_helpers.py +65 -0
  37. jac_client/tests/test_it.py +91 -135
  38. jac_client/tests/test_it_desktop.py +891 -0
  39. {jac_client-0.2.8.dist-info → jac_client-0.2.10.dist-info}/METADATA +4 -4
  40. jac_client-0.2.10.dist-info/RECORD +115 -0
  41. {jac_client-0.2.8.dist-info → jac_client-0.2.10.dist-info}/WHEEL +1 -1
  42. jac_client-0.2.8.dist-info/RECORD +0 -97
  43. /jac_client/examples/all-in-one/pages/{BudgetPlanner.cl.jac → budget_planner_ui.cl.jac} +0 -0
  44. /jac_client/examples/asset-serving/css-with-image/{src/app.jac → main.jac} +0 -0
  45. /jac_client/examples/asset-serving/image-asset/{src/app.jac → main.jac} +0 -0
  46. /jac_client/examples/asset-serving/import-alias/{src/app.jac → main.jac} +0 -0
  47. /jac_client/examples/basic/{src/app.jac → main.jac} +0 -0
  48. /jac_client/examples/basic-auth/{src/app.jac → main.jac} +0 -0
  49. /jac_client/examples/basic-auth-with-router/{src/app.jac → main.jac} +0 -0
  50. /jac_client/examples/basic-full-stack/{src/app.jac → main.jac} +0 -0
  51. /jac_client/examples/css-styling/js-styling/{src/app.jac → main.jac} +0 -0
  52. /jac_client/examples/css-styling/material-ui/{src/app.jac → main.jac} +0 -0
  53. /jac_client/examples/css-styling/pure-css/{src/app.jac → main.jac} +0 -0
  54. /jac_client/examples/css-styling/sass-example/{src/app.jac → main.jac} +0 -0
  55. /jac_client/examples/css-styling/styled-components/{src/app.jac → main.jac} +0 -0
  56. /jac_client/examples/css-styling/tailwind-example/{src/app.jac → main.jac} +0 -0
  57. /jac_client/examples/full-stack-with-auth/{src/app.jac → main.jac} +0 -0
  58. /jac_client/examples/little-x/{src/app.jac → main.jac} +0 -0
  59. /jac_client/examples/nested-folders/nested-advance/{src/app.jac → main.jac} +0 -0
  60. /jac_client/examples/nested-folders/nested-basic/{src/app.jac → main.jac} +0 -0
  61. /jac_client/examples/ts-support/{src/app.jac → main.jac} +0 -0
  62. /jac_client/examples/with-router/{src/app.jac → main.jac} +0 -0
  63. {jac_client-0.2.8.dist-info → jac_client-0.2.10.dist-info}/entry_points.txt +0 -0
  64. {jac_client-0.2.8.dist-info → jac_client-0.2.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,232 @@
1
+ """Browser-level E2E tests for jac-client authentication flows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gc
6
+ import os
7
+ import shutil
8
+ import tempfile
9
+ import time
10
+ from subprocess import PIPE, Popen, run
11
+
12
+ import pytest
13
+
14
+ pytest.importorskip("playwright")
15
+
16
+ from playwright.sync_api import Browser, Page
17
+
18
+ from .test_helpers import (
19
+ get_env_with_npm,
20
+ get_free_port,
21
+ get_jac_command,
22
+ wait_for_port,
23
+ )
24
+
25
+
26
+ @pytest.fixture(scope="module")
27
+ def running_server():
28
+ """Start the all-in-one jac server for the test module and yield its URL.
29
+
30
+ Yields a dict with keys `port` and `url`.
31
+ """
32
+ tests_dir = os.path.dirname(__file__)
33
+ jac_client_root = os.path.dirname(tests_dir)
34
+ all_in_one_path = os.path.join(jac_client_root, "examples", "all-in-one")
35
+
36
+ if not os.path.isdir(all_in_one_path):
37
+ pytest.skip("all-in-one example directory not found")
38
+
39
+ app_name = "e2e-browser-test-app"
40
+ jac_cmd = get_jac_command()
41
+ env = get_env_with_npm()
42
+
43
+ with tempfile.TemporaryDirectory() as temp_dir:
44
+ original_cwd = os.getcwd()
45
+ try:
46
+ os.chdir(temp_dir)
47
+ process = Popen(
48
+ [*jac_cmd, "create", "--use", "client", app_name],
49
+ stdin=PIPE,
50
+ stdout=PIPE,
51
+ stderr=PIPE,
52
+ text=True,
53
+ env=env,
54
+ )
55
+ stdout, stderr = process.communicate()
56
+ if process.returncode != 0:
57
+ pytest.fail(f"jac create --use client failed: {stderr}")
58
+
59
+ project_path = os.path.join(temp_dir, app_name)
60
+
61
+ for entry in os.listdir(all_in_one_path):
62
+ if entry in {"node_modules", "build", "dist", ".pytest_cache"}:
63
+ continue
64
+ src = os.path.join(all_in_one_path, entry)
65
+ dst = os.path.join(project_path, entry)
66
+ if os.path.isdir(src):
67
+ shutil.copytree(src, dst, dirs_exist_ok=True)
68
+ else:
69
+ shutil.copy2(src, dst)
70
+
71
+ jac_add_result = run(
72
+ [*jac_cmd, "add", "--npm"],
73
+ cwd=project_path,
74
+ capture_output=True,
75
+ text=True,
76
+ env=env,
77
+ )
78
+ if jac_add_result.returncode != 0:
79
+ pytest.fail(f"jac add --npm failed: {jac_add_result.stderr}")
80
+
81
+ server_port = get_free_port()
82
+ server = Popen(
83
+ [*jac_cmd, "start", "main.jac", "-p", str(server_port)],
84
+ cwd=project_path,
85
+ env=env,
86
+ )
87
+
88
+ try:
89
+ wait_for_port("127.0.0.1", server_port, timeout=90.0)
90
+ time.sleep(5)
91
+ yield {"port": server_port, "url": f"http://127.0.0.1:{server_port}"}
92
+
93
+ finally:
94
+ server.terminate()
95
+ try:
96
+ server.wait(timeout=15)
97
+ except Exception:
98
+ server.kill()
99
+ server.wait(timeout=5)
100
+ time.sleep(1)
101
+ gc.collect()
102
+
103
+ finally:
104
+ os.chdir(original_cwd)
105
+ gc.collect()
106
+
107
+
108
+ @pytest.fixture(scope="module")
109
+ def browser():
110
+ """Provide a Playwright browser instance for the module."""
111
+ from playwright.sync_api import sync_playwright
112
+
113
+ with sync_playwright() as p:
114
+ browser = p.chromium.launch(headless=True)
115
+ yield browser
116
+ browser.close()
117
+
118
+
119
+ @pytest.fixture
120
+ def page(browser: Browser):
121
+ """Provide a fresh browser page for each test and close context afterwards."""
122
+ context = browser.new_context()
123
+ page = context.new_page()
124
+ yield page
125
+ context.close()
126
+
127
+
128
+ class TestAuthenticationE2E:
129
+ """E2E tests for auth flows with helper methods to reduce duplication."""
130
+
131
+ @staticmethod
132
+ def _fill_auth_form(page: Page, username: str, password: str) -> None:
133
+ """Fill username and password fields."""
134
+ page.locator('input[type="text"], input[placeholder="Username" i]').first.fill(
135
+ username
136
+ )
137
+ page.locator('input[type="password"]').first.fill(password)
138
+
139
+ @staticmethod
140
+ def _submit_form(page: Page) -> None:
141
+ """Click submit button and wait for navigation."""
142
+ page.locator('button[type="submit"]').first.click()
143
+ page.wait_for_timeout(2000)
144
+
145
+ def _signup(self, page: Page, base_url: str, username: str, password: str) -> None:
146
+ """Navigate to signup, fill form, and submit."""
147
+ page.goto(f"{base_url}#/signup", wait_until="networkidle", timeout=60000)
148
+ page.wait_for_selector('input[type="text"]', timeout=30000)
149
+ self._fill_auth_form(page, username, password)
150
+ self._submit_form(page)
151
+
152
+ def _login(self, page: Page, base_url: str, username: str, password: str) -> None:
153
+ """Navigate to login, fill form, and submit."""
154
+ page.goto(f"{base_url}#/login", wait_until="networkidle", timeout=30000)
155
+ page.wait_for_selector('input[type="text"]', timeout=30000)
156
+ self._fill_auth_form(page, username, password)
157
+ self._submit_form(page)
158
+
159
+ def _logout(self, page: Page) -> None:
160
+ """Click logout button if visible."""
161
+ logout_btn = page.locator('button:has-text("Logout")').first
162
+ if logout_btn.is_visible():
163
+ logout_btn.click()
164
+ page.wait_for_timeout(1000)
165
+
166
+ def test_navigate_without_auth(self, running_server: dict, page: Page) -> None:
167
+ """Visiting protected route without auth should redirect to login."""
168
+ page.goto(
169
+ f"{running_server['url']}#/nested", wait_until="networkidle", timeout=60000
170
+ )
171
+ page.wait_for_timeout(2000)
172
+ assert "#/login" in page.url.lower()
173
+
174
+ def test_signup_form_submission(self, running_server: dict, page: Page) -> None:
175
+ """Signup via UI should redirect away from signup page on success."""
176
+ self._signup(
177
+ page,
178
+ running_server["url"],
179
+ f"e2e_signup_{int(time.time())}",
180
+ "test_pass_123",
181
+ )
182
+ assert "#/signup" not in page.url.lower()
183
+
184
+ def test_login_with_valid_credentials(
185
+ self, running_server: dict, page: Page
186
+ ) -> None:
187
+ """Verify login succeeds for valid credentials."""
188
+ base_url = running_server["url"]
189
+ username, password = f"e2e_login_{int(time.time())}", "valid_pass_123"
190
+
191
+ self._signup(page, base_url, username, password)
192
+ self._logout(page)
193
+ self._login(page, base_url, username, password)
194
+
195
+ assert "#/login" not in page.url.lower() and "#/signup" not in page.url.lower()
196
+
197
+ def test_login_with_invalid_credentials(
198
+ self, running_server: dict, page: Page
199
+ ) -> None:
200
+ """Verify login fails for invalid credentials (stays or shows error)."""
201
+ self._login(page, running_server["url"], "nonexistent_999", "wrong_pass")
202
+
203
+ assert "#/login" in page.url.lower()
204
+ assert page.locator("text=/Invalid credentials/i").first.is_visible()
205
+
206
+ def test_logout_functionality(self, running_server: dict, page: Page) -> None:
207
+ """Signup then logout should redirect to login."""
208
+ base_url = running_server["url"]
209
+ self._signup(
210
+ page, base_url, f"e2e_logout_{int(time.time())}", "logout_pass_123"
211
+ )
212
+
213
+ logout_btn = page.locator('button:has-text("Logout")').first
214
+ logout_btn.click()
215
+ page.wait_for_timeout(1500)
216
+
217
+ assert "#/login" in page.url.lower() and not logout_btn.is_visible(timeout=5000)
218
+
219
+ def test_complete_auth_flow(self, running_server: dict, page: Page) -> None:
220
+ """Integration: signup -> logout -> login -> access protected route."""
221
+ base_url = running_server["url"]
222
+ username, password = f"e2e_complete_{int(time.time())}", "complete_pass_123"
223
+
224
+ self._signup(page, base_url, username, password)
225
+ assert "#/signup" not in page.url.lower()
226
+
227
+ self._logout(page)
228
+ self._login(page, base_url, username, password)
229
+ assert "#/login" not in page.url.lower()
230
+
231
+ page.goto(f"{base_url}#/nested", wait_until="networkidle", timeout=30000)
232
+ assert "#/nested" in page.url.lower()
@@ -0,0 +1,65 @@
1
+ """Shared test utilities for jac-client tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import socket
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def get_free_port() -> int:
13
+ """Get a free port by binding to port 0 and releasing it."""
14
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
15
+ s.bind(("", 0))
16
+ s.listen(1)
17
+ port = s.getsockname()[1]
18
+ return port
19
+
20
+
21
+ def get_jac_command() -> list[str]:
22
+ """Get the jac command with proper path handling."""
23
+ jac_path = shutil.which("jac")
24
+ if jac_path:
25
+ return [jac_path]
26
+ return [sys.executable, "-m", "jaclang"]
27
+
28
+
29
+ def get_env_with_npm() -> dict[str, str]:
30
+ """Get environment dict with npm in PATH."""
31
+ env = os.environ.copy()
32
+ npm_path = shutil.which("npm")
33
+ if npm_path:
34
+ npm_dir = str(Path(npm_path).parent)
35
+ current_path = env.get("PATH", "")
36
+ if npm_dir not in current_path:
37
+ env["PATH"] = f"{npm_dir}:{current_path}"
38
+ # Also check common nvm locations
39
+ nvm_dir = os.environ.get("NVM_DIR", os.path.expanduser("~/.nvm"))
40
+ nvm_node_bin = Path(nvm_dir) / "versions" / "node"
41
+ if nvm_node_bin.exists():
42
+ for version_dir in nvm_node_bin.iterdir():
43
+ bin_dir = version_dir / "bin"
44
+ if bin_dir.exists() and (bin_dir / "npm").exists():
45
+ current_path = env.get("PATH", "")
46
+ if str(bin_dir) not in current_path:
47
+ env["PATH"] = f"{bin_dir}:{current_path}"
48
+ break
49
+ return env
50
+
51
+
52
+ def wait_for_port(host: str, port: int, timeout: float = 60.0) -> None:
53
+ """Block until a TCP port is accepting connections or timeout."""
54
+ import time
55
+
56
+ deadline = time.time() + timeout
57
+ while time.time() < deadline:
58
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
59
+ sock.settimeout(0.5)
60
+ try:
61
+ sock.connect((host, port))
62
+ return
63
+ except OSError:
64
+ time.sleep(0.5)
65
+ raise TimeoutError(f"Timed out waiting for {host}:{port}")