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.
- jac_client/examples/all-in-one/button.jac +4 -3
- jac_client/examples/all-in-one/components/CategoryFilter.jac +36 -24
- jac_client/examples/all-in-one/components/Header.jac +12 -8
- jac_client/examples/all-in-one/components/ProfitOverview.jac +49 -35
- jac_client/examples/all-in-one/components/Summary.jac +59 -36
- jac_client/examples/all-in-one/components/TransactionForm.jac +142 -112
- jac_client/examples/all-in-one/components/TransactionItem.jac +37 -30
- jac_client/examples/all-in-one/components/TransactionList.jac +33 -26
- jac_client/examples/all-in-one/components/button.jac +4 -3
- jac_client/examples/all-in-one/components/navigation.jac +111 -117
- jac_client/examples/all-in-one/constants/categories.jac +23 -24
- jac_client/examples/all-in-one/constants/clients.jac +7 -8
- jac_client/examples/all-in-one/context/BudgetContext.jac +9 -6
- jac_client/examples/all-in-one/hooks/useBudget.jac +18 -12
- jac_client/examples/all-in-one/hooks/useLocalStorage.jac +14 -13
- jac_client/examples/all-in-one/main.jac +542 -0
- jac_client/examples/all-in-one/pages/BudgetPlanner.jac +26 -12
- jac_client/examples/all-in-one/pages/FeaturesTest.jac +43 -12
- jac_client/examples/all-in-one/pages/LandingPage.jac +113 -90
- jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +65 -0
- jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +675 -0
- jac_client/examples/all-in-one/pages/loginPage.jac +114 -119
- jac_client/examples/all-in-one/pages/nestedDemo.jac +44 -51
- jac_client/examples/all-in-one/pages/notFound.jac +15 -21
- jac_client/examples/all-in-one/pages/signupPage.jac +113 -119
- jac_client/examples/all-in-one/utils/formatters.jac +5 -8
- jac_client/examples/asset-serving/css-with-image/main.jac +92 -0
- jac_client/examples/asset-serving/image-asset/main.jac +56 -0
- jac_client/examples/asset-serving/import-alias/main.jac +109 -0
- jac_client/examples/basic/main.jac +23 -0
- jac_client/examples/basic-auth/main.jac +363 -0
- jac_client/examples/basic-auth-with-router/main.jac +451 -0
- jac_client/examples/basic-full-stack/main.jac +362 -0
- jac_client/examples/css-styling/js-styling/main.jac +63 -0
- jac_client/examples/css-styling/material-ui/main.jac +122 -0
- jac_client/examples/css-styling/pure-css/main.jac +55 -0
- jac_client/examples/css-styling/sass-example/main.jac +55 -0
- jac_client/examples/css-styling/styled-components/main.jac +62 -0
- jac_client/examples/css-styling/tailwind-example/main.jac +74 -0
- jac_client/examples/full-stack-with-auth/main.jac +696 -0
- jac_client/examples/little-x/main.jac +681 -0
- jac_client/examples/little-x/src/submit-button.jac +15 -14
- jac_client/examples/nested-folders/nested-advance/main.jac +26 -0
- jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +4 -6
- jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +9 -13
- jac_client/examples/nested-folders/nested-advance/src/level1/Card.jac +29 -32
- jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +12 -18
- jac_client/examples/nested-folders/nested-basic/{src/app.jac → main.jac} +7 -5
- jac_client/examples/nested-folders/nested-basic/src/button.jac +4 -3
- jac_client/examples/nested-folders/nested-basic/src/components/button.jac +4 -3
- jac_client/examples/ts-support/main.jac +35 -0
- jac_client/examples/with-router/main.jac +286 -0
- jac_client/plugin/cli.jac +491 -411
- jac_client/plugin/client.jac +25 -0
- jac_client/plugin/client_runtime.cl.jac +10 -4
- jac_client/plugin/impl/client.impl.jac +96 -55
- jac_client/plugin/impl/client_runtime.impl.jac +155 -1
- jac_client/plugin/plugin_config.jac +211 -29
- jac_client/plugin/src/__init__.jac +0 -2
- jac_client/plugin/src/compiler.jac +0 -1
- jac_client/plugin/src/config_loader.jac +1 -0
- jac_client/plugin/src/desktop_config.jac +31 -0
- jac_client/plugin/src/impl/compiler.impl.jac +49 -17
- jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
- jac_client/plugin/src/impl/desktop_config.impl.jac +191 -0
- jac_client/plugin/src/impl/jac_to_js.impl.jac +5 -1
- jac_client/plugin/src/impl/package_installer.impl.jac +20 -20
- jac_client/plugin/src/impl/vite_bundler.impl.jac +191 -64
- jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
- jac_client/plugin/src/targets/desktop_target.jac +37 -0
- jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2347 -0
- jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
- jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
- jac_client/plugin/src/targets/register.jac +21 -0
- jac_client/plugin/src/targets/registry.jac +87 -0
- jac_client/plugin/src/targets/web_target.jac +35 -0
- jac_client/plugin/src/vite_bundler.jac +6 -0
- jac_client/plugin/utils/__init__.jac +3 -0
- jac_client/plugin/utils/bun_installer.jac +16 -0
- jac_client/plugin/utils/impl/bun_installer.impl.jac +99 -0
- jac_client/templates/client.jacpack +72 -0
- jac_client/templates/fullstack.jacpack +61 -0
- jac_client/tests/conftest.py +103 -47
- jac_client/tests/fixtures/spawn_test/app.jac +49 -52
- jac_client/tests/fixtures/with-ts/app.jac +27 -27
- jac_client/tests/test_cli.py +182 -71
- jac_client/tests/test_e2e.py +232 -0
- jac_client/tests/test_helpers.py +58 -0
- jac_client/tests/test_it.py +91 -135
- jac_client/tests/test_it_desktop.py +891 -0
- {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/METADATA +6 -6
- jac_client-0.2.11.dist-info/RECORD +113 -0
- {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/WHEEL +1 -1
- jac_client/examples/all-in-one/app.jac +0 -573
- jac_client/examples/all-in-one/pages/BudgetPlanner.cl.jac +0 -70
- jac_client/examples/all-in-one/pages/FeaturesTest.cl.jac +0 -552
- jac_client/examples/asset-serving/css-with-image/src/app.jac +0 -88
- jac_client/examples/asset-serving/image-asset/src/app.jac +0 -55
- jac_client/examples/asset-serving/import-alias/src/app.jac +0 -111
- jac_client/examples/basic/src/app.jac +0 -21
- jac_client/examples/basic-auth/src/app.jac +0 -371
- jac_client/examples/basic-auth-with-router/src/app.jac +0 -464
- jac_client/examples/basic-full-stack/src/app.jac +0 -359
- jac_client/examples/css-styling/js-styling/src/app.jac +0 -84
- jac_client/examples/css-styling/material-ui/src/app.jac +0 -122
- jac_client/examples/css-styling/pure-css/src/app.jac +0 -64
- jac_client/examples/css-styling/sass-example/src/app.jac +0 -64
- jac_client/examples/css-styling/styled-components/src/app.jac +0 -71
- jac_client/examples/css-styling/tailwind-example/src/app.jac +0 -63
- jac_client/examples/full-stack-with-auth/src/app.jac +0 -722
- jac_client/examples/little-x/src/app.jac +0 -719
- jac_client/examples/nested-folders/nested-advance/src/app.jac +0 -35
- jac_client/examples/ts-support/src/app.jac +0 -35
- jac_client/examples/with-router/src/app.jac +0 -323
- jac_client/plugin/src/babel_processor.jac +0 -18
- jac_client/plugin/src/impl/babel_processor.impl.jac +0 -89
- jac_client-0.2.8.dist-info/RECORD +0 -97
- {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/entry_points.txt +0 -0
- {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}")
|