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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. jac_client/examples/all-in-one/button.jac +4 -3
  2. jac_client/examples/all-in-one/components/CategoryFilter.jac +36 -24
  3. jac_client/examples/all-in-one/components/Header.jac +12 -8
  4. jac_client/examples/all-in-one/components/ProfitOverview.jac +49 -35
  5. jac_client/examples/all-in-one/components/Summary.jac +59 -36
  6. jac_client/examples/all-in-one/components/TransactionForm.jac +142 -112
  7. jac_client/examples/all-in-one/components/TransactionItem.jac +37 -30
  8. jac_client/examples/all-in-one/components/TransactionList.jac +33 -26
  9. jac_client/examples/all-in-one/components/button.jac +4 -3
  10. jac_client/examples/all-in-one/components/navigation.jac +111 -117
  11. jac_client/examples/all-in-one/constants/categories.jac +23 -24
  12. jac_client/examples/all-in-one/constants/clients.jac +7 -8
  13. jac_client/examples/all-in-one/context/BudgetContext.jac +9 -6
  14. jac_client/examples/all-in-one/hooks/useBudget.jac +18 -12
  15. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +14 -13
  16. jac_client/examples/all-in-one/main.jac +542 -0
  17. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +26 -12
  18. jac_client/examples/all-in-one/pages/FeaturesTest.jac +43 -12
  19. jac_client/examples/all-in-one/pages/LandingPage.jac +113 -90
  20. jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +65 -0
  21. jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +675 -0
  22. jac_client/examples/all-in-one/pages/loginPage.jac +114 -119
  23. jac_client/examples/all-in-one/pages/nestedDemo.jac +44 -51
  24. jac_client/examples/all-in-one/pages/notFound.jac +15 -21
  25. jac_client/examples/all-in-one/pages/signupPage.jac +113 -119
  26. jac_client/examples/all-in-one/utils/formatters.jac +5 -8
  27. jac_client/examples/asset-serving/css-with-image/main.jac +92 -0
  28. jac_client/examples/asset-serving/image-asset/main.jac +56 -0
  29. jac_client/examples/asset-serving/import-alias/main.jac +109 -0
  30. jac_client/examples/basic/main.jac +23 -0
  31. jac_client/examples/basic-auth/main.jac +363 -0
  32. jac_client/examples/basic-auth-with-router/main.jac +451 -0
  33. jac_client/examples/basic-full-stack/main.jac +362 -0
  34. jac_client/examples/css-styling/js-styling/main.jac +63 -0
  35. jac_client/examples/css-styling/material-ui/main.jac +122 -0
  36. jac_client/examples/css-styling/pure-css/main.jac +55 -0
  37. jac_client/examples/css-styling/sass-example/main.jac +55 -0
  38. jac_client/examples/css-styling/styled-components/main.jac +62 -0
  39. jac_client/examples/css-styling/tailwind-example/main.jac +74 -0
  40. jac_client/examples/full-stack-with-auth/main.jac +696 -0
  41. jac_client/examples/little-x/main.jac +681 -0
  42. jac_client/examples/little-x/src/submit-button.jac +15 -14
  43. jac_client/examples/nested-folders/nested-advance/main.jac +26 -0
  44. jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +4 -6
  45. jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +9 -13
  46. jac_client/examples/nested-folders/nested-advance/src/level1/Card.jac +29 -32
  47. jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +12 -18
  48. jac_client/examples/nested-folders/nested-basic/{src/app.jac → main.jac} +7 -5
  49. jac_client/examples/nested-folders/nested-basic/src/button.jac +4 -3
  50. jac_client/examples/nested-folders/nested-basic/src/components/button.jac +4 -3
  51. jac_client/examples/ts-support/main.jac +35 -0
  52. jac_client/examples/with-router/main.jac +286 -0
  53. jac_client/plugin/cli.jac +491 -411
  54. jac_client/plugin/client.jac +25 -0
  55. jac_client/plugin/client_runtime.cl.jac +10 -4
  56. jac_client/plugin/impl/client.impl.jac +96 -55
  57. jac_client/plugin/impl/client_runtime.impl.jac +155 -1
  58. jac_client/plugin/plugin_config.jac +211 -29
  59. jac_client/plugin/src/__init__.jac +0 -2
  60. jac_client/plugin/src/compiler.jac +0 -1
  61. jac_client/plugin/src/config_loader.jac +1 -0
  62. jac_client/plugin/src/desktop_config.jac +31 -0
  63. jac_client/plugin/src/impl/compiler.impl.jac +49 -17
  64. jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
  65. jac_client/plugin/src/impl/desktop_config.impl.jac +191 -0
  66. jac_client/plugin/src/impl/jac_to_js.impl.jac +5 -1
  67. jac_client/plugin/src/impl/package_installer.impl.jac +20 -20
  68. jac_client/plugin/src/impl/vite_bundler.impl.jac +191 -64
  69. jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
  70. jac_client/plugin/src/targets/desktop_target.jac +37 -0
  71. jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2347 -0
  72. jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
  73. jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
  74. jac_client/plugin/src/targets/register.jac +21 -0
  75. jac_client/plugin/src/targets/registry.jac +87 -0
  76. jac_client/plugin/src/targets/web_target.jac +35 -0
  77. jac_client/plugin/src/vite_bundler.jac +6 -0
  78. jac_client/plugin/utils/__init__.jac +3 -0
  79. jac_client/plugin/utils/bun_installer.jac +16 -0
  80. jac_client/plugin/utils/impl/bun_installer.impl.jac +99 -0
  81. jac_client/templates/client.jacpack +72 -0
  82. jac_client/templates/fullstack.jacpack +61 -0
  83. jac_client/tests/conftest.py +103 -47
  84. jac_client/tests/fixtures/spawn_test/app.jac +49 -52
  85. jac_client/tests/fixtures/with-ts/app.jac +27 -27
  86. jac_client/tests/test_cli.py +182 -71
  87. jac_client/tests/test_e2e.py +232 -0
  88. jac_client/tests/test_helpers.py +58 -0
  89. jac_client/tests/test_it.py +91 -135
  90. jac_client/tests/test_it_desktop.py +891 -0
  91. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/METADATA +6 -6
  92. jac_client-0.2.11.dist-info/RECORD +113 -0
  93. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/WHEEL +1 -1
  94. jac_client/examples/all-in-one/app.jac +0 -573
  95. jac_client/examples/all-in-one/pages/BudgetPlanner.cl.jac +0 -70
  96. jac_client/examples/all-in-one/pages/FeaturesTest.cl.jac +0 -552
  97. jac_client/examples/asset-serving/css-with-image/src/app.jac +0 -88
  98. jac_client/examples/asset-serving/image-asset/src/app.jac +0 -55
  99. jac_client/examples/asset-serving/import-alias/src/app.jac +0 -111
  100. jac_client/examples/basic/src/app.jac +0 -21
  101. jac_client/examples/basic-auth/src/app.jac +0 -371
  102. jac_client/examples/basic-auth-with-router/src/app.jac +0 -464
  103. jac_client/examples/basic-full-stack/src/app.jac +0 -359
  104. jac_client/examples/css-styling/js-styling/src/app.jac +0 -84
  105. jac_client/examples/css-styling/material-ui/src/app.jac +0 -122
  106. jac_client/examples/css-styling/pure-css/src/app.jac +0 -64
  107. jac_client/examples/css-styling/sass-example/src/app.jac +0 -64
  108. jac_client/examples/css-styling/styled-components/src/app.jac +0 -71
  109. jac_client/examples/css-styling/tailwind-example/src/app.jac +0 -63
  110. jac_client/examples/full-stack-with-auth/src/app.jac +0 -722
  111. jac_client/examples/little-x/src/app.jac +0 -719
  112. jac_client/examples/nested-folders/nested-advance/src/app.jac +0 -35
  113. jac_client/examples/ts-support/src/app.jac +0 -35
  114. jac_client/examples/with-router/src/app.jac +0 -323
  115. jac_client/plugin/src/babel_processor.jac +0 -18
  116. jac_client/plugin/src/impl/babel_processor.impl.jac +0 -89
  117. jac_client-0.2.8.dist-info/RECORD +0 -97
  118. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/entry_points.txt +0 -0
  119. {jac_client-0.2.8.dist-info → jac_client-0.2.11.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,58 @@
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_bun() -> dict[str, str]:
30
+ """Get environment dict with bun in PATH."""
31
+ env = os.environ.copy()
32
+ bun_path = shutil.which("bun")
33
+ if bun_path:
34
+ bun_dir = str(Path(bun_path).parent)
35
+ current_path = env.get("PATH", "")
36
+ if bun_dir not in current_path:
37
+ env["PATH"] = f"{bun_dir}:{current_path}"
38
+ return env
39
+
40
+
41
+ # Backward compatibility alias
42
+ get_env_with_npm = get_env_with_bun
43
+
44
+
45
+ def wait_for_port(host: str, port: int, timeout: float = 60.0) -> None:
46
+ """Block until a TCP port is accepting connections or timeout."""
47
+ import time
48
+
49
+ deadline = time.time() + timeout
50
+ while time.time() < deadline:
51
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
52
+ sock.settimeout(0.5)
53
+ try:
54
+ sock.connect((host, port))
55
+ return
56
+ except OSError:
57
+ time.sleep(0.5)
58
+ raise TimeoutError(f"Timed out waiting for {host}:{port}")