jac-client 0.2.0__py3-none-any.whl → 0.2.2__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 (154) hide show
  1. jac_client/docs/README.md +50 -20
  2. jac_client/docs/advanced-state.md +13 -14
  3. jac_client/docs/asset-serving/intro.md +209 -0
  4. jac_client/docs/assets/pipe_line-v2.svg +32 -0
  5. jac_client/docs/file-system/app.jac.md +121 -0
  6. jac_client/docs/file-system/backend-frontend.md +217 -0
  7. jac_client/docs/file-system/intro.md +72 -0
  8. jac_client/docs/file-system/nested-imports.md +348 -0
  9. jac_client/docs/guide-example/intro.md +11 -13
  10. jac_client/docs/guide-example/step-01-setup.md +30 -20
  11. jac_client/docs/guide-example/step-02-components.md +24 -24
  12. jac_client/docs/guide-example/step-03-styling.md +24 -24
  13. jac_client/docs/guide-example/step-04-todo-ui.md +17 -17
  14. jac_client/docs/guide-example/step-05-local-state.md +23 -23
  15. jac_client/docs/guide-example/step-06-events.md +23 -24
  16. jac_client/docs/guide-example/step-07-effects.md +27 -28
  17. jac_client/docs/guide-example/step-08-walkers.md +23 -23
  18. jac_client/docs/guide-example/step-09-authentication.md +18 -18
  19. jac_client/docs/guide-example/step-10-routing.md +20 -21
  20. jac_client/docs/guide-example/step-11-final.md +34 -35
  21. jac_client/docs/imports.md +4 -5
  22. jac_client/docs/lifecycle-hooks.md +12 -13
  23. jac_client/docs/routing.md +21 -22
  24. jac_client/docs/styling/intro.md +249 -0
  25. jac_client/docs/styling/js-styling.md +367 -0
  26. jac_client/docs/styling/material-ui.md +341 -0
  27. jac_client/docs/styling/pure-css.md +299 -0
  28. jac_client/docs/styling/sass.md +403 -0
  29. jac_client/docs/styling/styled-components.md +395 -0
  30. jac_client/docs/styling/tailwind.md +298 -0
  31. jac_client/examples/all-in-one/.babelrc +9 -0
  32. jac_client/examples/all-in-one/README.md +16 -0
  33. jac_client/examples/all-in-one/app.jac +426 -0
  34. jac_client/examples/all-in-one/assets/burger.png +0 -0
  35. jac_client/examples/all-in-one/button.jac +7 -0
  36. jac_client/examples/all-in-one/components/button.jac +7 -0
  37. jac_client/examples/all-in-one/package.json +29 -0
  38. jac_client/examples/all-in-one/styles.css +26 -0
  39. jac_client/examples/all-in-one/vite.config.js +28 -0
  40. jac_client/examples/asset-serving/css-with-image/.babelrc +9 -0
  41. jac_client/examples/asset-serving/css-with-image/README.md +91 -0
  42. jac_client/examples/asset-serving/css-with-image/app.jac +88 -0
  43. jac_client/examples/asset-serving/css-with-image/assets/burger.png +0 -0
  44. jac_client/examples/asset-serving/css-with-image/package.json +28 -0
  45. jac_client/examples/asset-serving/css-with-image/styles.css +26 -0
  46. jac_client/examples/asset-serving/css-with-image/vite.config.js +28 -0
  47. jac_client/examples/asset-serving/image-asset/.babelrc +9 -0
  48. jac_client/examples/asset-serving/image-asset/README.md +119 -0
  49. jac_client/examples/asset-serving/image-asset/app.jac +55 -0
  50. jac_client/examples/asset-serving/image-asset/assets/burger.png +0 -0
  51. jac_client/examples/asset-serving/image-asset/package.json +28 -0
  52. jac_client/examples/asset-serving/image-asset/styles.css +26 -0
  53. jac_client/examples/asset-serving/image-asset/vite.config.js +28 -0
  54. jac_client/examples/asset-serving/import-alias/.babelrc +9 -0
  55. jac_client/examples/asset-serving/import-alias/README.md +83 -0
  56. jac_client/examples/asset-serving/import-alias/app.jac +111 -0
  57. jac_client/examples/asset-serving/import-alias/assets/burger.png +0 -0
  58. jac_client/examples/asset-serving/import-alias/package.json +28 -0
  59. jac_client/examples/asset-serving/import-alias/vite.config.js +28 -0
  60. jac_client/examples/basic/app.jac +14 -9
  61. jac_client/examples/basic/package.json +1 -1
  62. jac_client/examples/basic/vite.config.js +0 -1
  63. jac_client/examples/basic-auth/package.json +1 -1
  64. jac_client/examples/basic-auth/vite.config.js +0 -1
  65. jac_client/examples/basic-auth-with-router/package.json +1 -1
  66. jac_client/examples/basic-auth-with-router/vite.config.js +0 -1
  67. jac_client/examples/basic-full-stack/package.json +1 -1
  68. jac_client/examples/basic-full-stack/vite.config.js +0 -1
  69. jac_client/examples/css-styling/js-styling/.babelrc +9 -0
  70. jac_client/examples/css-styling/js-styling/README.md +183 -0
  71. jac_client/examples/css-styling/js-styling/app.jac +84 -0
  72. jac_client/examples/css-styling/js-styling/package.json +28 -0
  73. jac_client/examples/css-styling/js-styling/styles.js +100 -0
  74. jac_client/examples/css-styling/js-styling/vite.config.js +27 -0
  75. jac_client/examples/css-styling/material-ui/.babelrc +9 -0
  76. jac_client/examples/css-styling/material-ui/README.md +16 -0
  77. jac_client/examples/css-styling/material-ui/app.jac +122 -0
  78. jac_client/examples/css-styling/material-ui/package.json +32 -0
  79. jac_client/examples/css-styling/material-ui/vite.config.js +27 -0
  80. jac_client/examples/css-styling/pure-css/.babelrc +9 -0
  81. jac_client/examples/css-styling/pure-css/README.md +16 -0
  82. jac_client/examples/css-styling/pure-css/app.jac +64 -0
  83. jac_client/examples/css-styling/pure-css/package.json +28 -0
  84. jac_client/examples/css-styling/pure-css/styles.css +111 -0
  85. jac_client/examples/css-styling/pure-css/vite.config.js +27 -0
  86. jac_client/examples/css-styling/sass-example/.babelrc +9 -0
  87. jac_client/examples/css-styling/sass-example/README.md +16 -0
  88. jac_client/examples/css-styling/sass-example/app.jac +64 -0
  89. jac_client/examples/css-styling/sass-example/package.json +29 -0
  90. jac_client/examples/css-styling/sass-example/styles.scss +153 -0
  91. jac_client/examples/css-styling/sass-example/vite.config.js +27 -0
  92. jac_client/examples/css-styling/styled-components/.babelrc +9 -0
  93. jac_client/examples/css-styling/styled-components/README.md +16 -0
  94. jac_client/examples/css-styling/styled-components/app.jac +71 -0
  95. jac_client/examples/css-styling/styled-components/package.json +29 -0
  96. jac_client/examples/css-styling/styled-components/styled.js +90 -0
  97. jac_client/examples/css-styling/styled-components/vite.config.js +27 -0
  98. jac_client/examples/css-styling/tailwind-example/.babelrc +9 -0
  99. jac_client/examples/css-styling/tailwind-example/README.md +16 -0
  100. jac_client/examples/css-styling/tailwind-example/app.jac +63 -0
  101. jac_client/examples/css-styling/tailwind-example/global.css +1 -0
  102. jac_client/examples/css-styling/tailwind-example/package.json +30 -0
  103. jac_client/examples/css-styling/tailwind-example/vite.config.js +29 -0
  104. jac_client/examples/full-stack-with-auth/app.jac +20 -33
  105. jac_client/examples/full-stack-with-auth/package.json +1 -1
  106. jac_client/examples/full-stack-with-auth/vite.config.js +0 -1
  107. jac_client/examples/little-x/app.jac +327 -218
  108. jac_client/examples/little-x/submit-button.jac +1 -1
  109. jac_client/examples/nested-folders/nested-advance/.babelrc +9 -0
  110. jac_client/examples/nested-folders/nested-advance/ButtonRoot.jac +11 -0
  111. jac_client/examples/nested-folders/nested-advance/README.md +77 -0
  112. jac_client/examples/nested-folders/nested-advance/app.jac +35 -0
  113. jac_client/examples/nested-folders/nested-advance/level1/ButtonSecondL.jac +19 -0
  114. jac_client/examples/nested-folders/nested-advance/level1/Card.jac +43 -0
  115. jac_client/examples/nested-folders/nested-advance/level1/level2/ButtonThirdL.jac +25 -0
  116. jac_client/examples/nested-folders/nested-advance/package.json +29 -0
  117. jac_client/examples/nested-folders/nested-advance/vite.config.js +28 -0
  118. jac_client/examples/nested-folders/nested-basic/.babelrc +9 -0
  119. jac_client/examples/nested-folders/nested-basic/README.md +183 -0
  120. jac_client/examples/nested-folders/nested-basic/app.jac +13 -0
  121. jac_client/examples/nested-folders/nested-basic/app.js +7 -0
  122. jac_client/examples/nested-folders/nested-basic/button.jac +7 -0
  123. jac_client/examples/nested-folders/nested-basic/components/button.jac +7 -0
  124. jac_client/examples/nested-folders/nested-basic/package.json +28 -0
  125. jac_client/examples/nested-folders/nested-basic/vite.config.js +27 -0
  126. jac_client/examples/with-router/app.jac +1 -1
  127. jac_client/examples/with-router/package.json +1 -1
  128. jac_client/examples/with-router/vite.config.js +0 -1
  129. jac_client/plugin/cli.py +7 -2
  130. jac_client/plugin/client.py +68 -5
  131. jac_client/plugin/client_runtime.jac +1 -1
  132. jac_client/plugin/vite_client_bundle.py +162 -14
  133. jac_client/tests/__init__.py +0 -1
  134. jac_client/tests/fixtures/basic-app/app.jac +7 -2
  135. jac_client/tests/fixtures/cl_file/app.cl.jac +48 -0
  136. jac_client/tests/fixtures/cl_file/app.jac +15 -0
  137. jac_client/tests/fixtures/client_app_with_antd/app.jac +14 -8
  138. jac_client/tests/fixtures/js_import/app.jac +19 -15
  139. jac_client/tests/fixtures/js_import/utils.js +0 -1
  140. jac_client/tests/fixtures/package.json +1 -1
  141. jac_client/tests/fixtures/relative_import/app.jac +4 -6
  142. jac_client/tests/fixtures/relative_import/button.jac +7 -6
  143. jac_client/tests/fixtures/spawn_test/app.jac +1 -5
  144. jac_client/tests/fixtures/test_fragments_spread/app.jac +24 -10
  145. jac_client/tests/test_asset_examples.py +322 -0
  146. jac_client/tests/test_cl.py +480 -426
  147. jac_client/tests/test_create_jac_app.py +125 -133
  148. jac_client/tests/test_it.py +329 -0
  149. jac_client/tests/test_nested_file.py +374 -0
  150. {jac_client-0.2.0.dist-info → jac_client-0.2.2.dist-info}/METADATA +2 -2
  151. jac_client-0.2.2.dist-info/RECORD +171 -0
  152. jac_client-0.2.0.dist-info/RECORD +0 -72
  153. {jac_client-0.2.0.dist-info → jac_client-0.2.2.dist-info}/WHEEL +0 -0
  154. {jac_client-0.2.0.dist-info → jac_client-0.2.2.dist-info}/entry_points.txt +0 -0
@@ -4,136 +4,128 @@ import json
4
4
  import os
5
5
  import tempfile
6
6
  from subprocess import run
7
- from unittest import TestCase
8
-
9
-
10
- class TestCreateJacApp(TestCase):
11
- """Test create-jac-app command functionality."""
12
-
13
- def test_create_jac_app(self) -> None:
14
- """Test create-jac-app command."""
15
- test_project_name = "test-jac-app"
16
-
17
- # Create a temporary directory for testing
18
- with tempfile.TemporaryDirectory() as temp_dir:
19
- original_cwd = os.getcwd()
20
- try:
21
- # Change to temp directory
22
- os.chdir(temp_dir)
23
-
24
- # Run create-jac-app command
25
- result = run(
26
- [
27
- "jac",
28
- "create_jac_app",
29
- test_project_name
30
- ],
31
- capture_output=True,
32
- text=True,
33
- check=True
34
- )
35
-
36
- # Check that command succeeded
37
- self.assertEqual(result.returncode, 0)
38
- self.assertIn(f"Successfully created Jac application '{test_project_name}'!", result.stdout)
39
-
40
- # Verify project directory was created
41
- project_path = os.path.join(temp_dir, test_project_name)
42
- self.assertTrue(os.path.exists(project_path))
43
- self.assertTrue(os.path.isdir(project_path))
44
-
45
- # Verify package.json was created and has correct content
46
- package_json_path = os.path.join(project_path, "package.json")
47
- self.assertTrue(os.path.exists(package_json_path))
48
-
49
- with open(package_json_path, "r") as f:
50
- package_data = json.load(f)
51
-
52
- self.assertEqual(package_data["name"], test_project_name)
53
- self.assertEqual(package_data["type"], "module")
54
- self.assertIn("vite", package_data["devDependencies"])
55
- self.assertIn("build", package_data["scripts"])
56
- self.assertIn("dev", package_data["scripts"])
57
- self.assertIn("preview", package_data["scripts"])
58
-
59
- # Verify app.jac file was created
60
- app_jac_path = os.path.join(project_path, "app.jac")
61
- self.assertTrue(os.path.exists(app_jac_path))
62
-
63
- with open(app_jac_path, "r") as f:
64
- app_jac_content = f.read()
65
-
66
- self.assertIn("app()", app_jac_content)
67
-
68
- # Verify README.md was created
69
- readme_path = os.path.join(project_path, "README.md")
70
- self.assertTrue(os.path.exists(readme_path))
71
-
72
- with open(readme_path, "r") as f:
73
- readme_content = f.read()
74
-
75
- self.assertIn(f"# {test_project_name}", readme_content)
76
- self.assertIn("jac serve app.jac", readme_content)
77
-
78
- # Verify node_modules was created (npm install ran)
79
- node_modules_path = os.path.join(project_path, "node_modules")
80
- self.assertTrue(os.path.exists(node_modules_path))
81
-
82
- finally:
83
- # Return to original directory
84
- os.chdir(original_cwd)
85
-
86
- def test_create_jac_app_invalid_name(self) -> None:
87
- """Test create-jac-app command with invalid project name."""
88
- with tempfile.TemporaryDirectory() as temp_dir:
89
- original_cwd = os.getcwd()
90
- try:
91
- os.chdir(temp_dir)
92
-
93
- # Test with invalid name containing spaces
94
- result = run(
95
- [
96
- "jac",
97
- "create_jac_app",
98
- "invalid name with spaces"
99
- ],
100
- capture_output=True,
101
- text=True
102
- )
103
-
104
- # Should fail with non-zero exit code
105
- self.assertNotEqual(result.returncode, 0)
106
- self.assertIn("Project name must contain only letters, numbers, hyphens, and underscores", result.stderr)
107
-
108
- finally:
109
- os.chdir(original_cwd)
110
-
111
- def test_create_jac_app_existing_directory(self) -> None:
112
- """Test create-jac-app command when directory already exists."""
113
- test_project_name = "existing-test-app"
114
-
115
- with tempfile.TemporaryDirectory() as temp_dir:
116
- original_cwd = os.getcwd()
117
- try:
118
- os.chdir(temp_dir)
119
-
120
- # Create the directory first
121
- os.makedirs(test_project_name)
122
-
123
- # Try to create app with same name
124
- result = run(
125
- [
126
- "jac",
127
- "create_jac_app",
128
- test_project_name
129
- ],
130
- capture_output=True,
131
- text=True
132
- )
133
-
134
- # Should fail with non-zero exit code
135
- self.assertNotEqual(result.returncode, 0)
136
- self.assertIn(f"Directory '{test_project_name}' already exists", result.stderr)
137
-
138
- finally:
139
- os.chdir(original_cwd)
7
+
8
+
9
+ def test_create_jac_app() -> None:
10
+ """Test create-jac-app command."""
11
+ test_project_name = "test-jac-app"
12
+
13
+ # Create a temporary directory for testing
14
+ with tempfile.TemporaryDirectory() as temp_dir:
15
+ original_cwd = os.getcwd()
16
+ try:
17
+ # Change to temp directory
18
+ os.chdir(temp_dir)
19
+
20
+ # Run create-jac-app command
21
+ result = run(
22
+ ["jac", "create_jac_app", test_project_name],
23
+ capture_output=True,
24
+ text=True,
25
+ check=True,
26
+ )
27
+
28
+ # Check that command succeeded
29
+ assert result.returncode == 0
30
+ assert (
31
+ f"Successfully created Jac application '{test_project_name}'!"
32
+ in result.stdout
33
+ )
34
+
35
+ # Verify project directory was created
36
+ project_path = os.path.join(temp_dir, test_project_name)
37
+ assert os.path.exists(project_path)
38
+ assert os.path.isdir(project_path)
39
+
40
+ # Verify package.json was created and has correct content
41
+ package_json_path = os.path.join(project_path, "package.json")
42
+ assert os.path.exists(package_json_path)
43
+
44
+ with open(package_json_path) as f:
45
+ package_data = json.load(f)
46
+
47
+ assert package_data["name"] == test_project_name
48
+ assert package_data["type"] == "module"
49
+ assert "vite" in package_data["devDependencies"]
50
+ assert "build" in package_data["scripts"]
51
+ assert "dev" in package_data["scripts"]
52
+ assert "preview" in package_data["scripts"]
53
+
54
+ # Verify app.jac file was created
55
+ app_jac_path = os.path.join(project_path, "app.jac")
56
+ assert os.path.exists(app_jac_path)
57
+
58
+ with open(app_jac_path) as f:
59
+ app_jac_content = f.read()
60
+
61
+ assert "app()" in app_jac_content
62
+
63
+ # Verify README.md was created
64
+ readme_path = os.path.join(project_path, "README.md")
65
+ assert os.path.exists(readme_path)
66
+
67
+ with open(readme_path) as f:
68
+ readme_content = f.read()
69
+
70
+ assert f"# {test_project_name}" in readme_content
71
+ assert "jac serve app.jac" in readme_content
72
+
73
+ # Verify node_modules was created (npm install ran)
74
+ node_modules_path = os.path.join(project_path, "node_modules")
75
+ assert os.path.exists(node_modules_path)
76
+
77
+ finally:
78
+ # Return to original directory
79
+ os.chdir(original_cwd)
80
+
81
+
82
+ def test_create_jac_app_invalid_name() -> None:
83
+ """Test create-jac-app command with invalid project name."""
84
+ with tempfile.TemporaryDirectory() as temp_dir:
85
+ original_cwd = os.getcwd()
86
+ try:
87
+ os.chdir(temp_dir)
88
+
89
+ # Test with invalid name containing spaces
90
+ result = run(
91
+ ["jac", "create_jac_app", "invalid name with spaces"],
92
+ capture_output=True,
93
+ text=True,
94
+ )
95
+
96
+ # Should fail with non-zero exit code
97
+ assert result.returncode != 0
98
+ assert (
99
+ "Project name must contain only letters, numbers, hyphens, and underscores"
100
+ in result.stderr
101
+ )
102
+
103
+ finally:
104
+ os.chdir(original_cwd)
105
+
106
+
107
+ def test_create_jac_app_existing_directory() -> None:
108
+ """Test create-jac-app command when directory already exists."""
109
+ test_project_name = "existing-test-app"
110
+
111
+ with tempfile.TemporaryDirectory() as temp_dir:
112
+ original_cwd = os.getcwd()
113
+ try:
114
+ os.chdir(temp_dir)
115
+
116
+ # Create the directory first
117
+ os.makedirs(test_project_name)
118
+
119
+ # Try to create app with same name
120
+ result = run(
121
+ ["jac", "create_jac_app", test_project_name],
122
+ capture_output=True,
123
+ text=True,
124
+ )
125
+
126
+ # Should fail with non-zero exit code
127
+ assert result.returncode != 0
128
+ assert f"Directory '{test_project_name}' already exists" in result.stderr
129
+
130
+ finally:
131
+ os.chdir(original_cwd)
@@ -0,0 +1,329 @@
1
+ """End-to-end tests for `jac serve` HTTP endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import socket
9
+ import tempfile
10
+ import time
11
+ from subprocess import Popen, run
12
+ from urllib.error import HTTPError, URLError
13
+ from urllib.request import Request, urlopen
14
+
15
+ import pytest
16
+
17
+
18
+ def _wait_for_port(
19
+ host: str,
20
+ port: int,
21
+ timeout: float = 60.0,
22
+ poll_interval: float = 0.5,
23
+ ) -> None:
24
+ """Block until a TCP port is accepting connections or timeout.
25
+
26
+ Raises:
27
+ TimeoutError: if the port is not accepting connections within timeout.
28
+ """
29
+ deadline = time.time() + timeout
30
+ last_err: Exception | None = None
31
+
32
+ while time.time() < deadline:
33
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
34
+ sock.settimeout(poll_interval)
35
+ try:
36
+ sock.connect((host, port))
37
+ return
38
+ except OSError as exc: # Connection refused / timeout
39
+ last_err = exc
40
+ time.sleep(poll_interval)
41
+
42
+ raise TimeoutError(
43
+ f"Timed out waiting for {host}:{port} to become available. Last error: {last_err}"
44
+ )
45
+
46
+
47
+ def test_all_in_one_app_endpoints() -> None:
48
+ """Create a Jac app, copy @all-in-one into it, run npm install, then verify endpoints."""
49
+ print(
50
+ "[DEBUG] Starting test_all_in_one_app_endpoints using jac create_jac_app + @all-in-one"
51
+ )
52
+
53
+ # Resolve the path to jac_client/examples/all-in-one relative to this test file.
54
+ tests_dir = os.path.dirname(__file__)
55
+ jac_client_root = os.path.dirname(tests_dir)
56
+ all_in_one_path = os.path.join(jac_client_root, "examples", "all-in-one")
57
+
58
+ print(f"[DEBUG] Resolved all-in-one source path: {all_in_one_path}")
59
+ assert os.path.isdir(all_in_one_path), "all-in-one example directory missing"
60
+
61
+ app_name = "e2e-all-in-one-app"
62
+
63
+ with tempfile.TemporaryDirectory() as temp_dir:
64
+ print(f"[DEBUG] Created temporary directory at {temp_dir}")
65
+ original_cwd = os.getcwd()
66
+ try:
67
+ os.chdir(temp_dir)
68
+ print(f"[DEBUG] Changed working directory to {temp_dir}")
69
+
70
+ # 1. Create a new Jac app via CLI (requires jac + jac-client plugin installed)
71
+ print(f"[DEBUG] Running 'jac create_jac_app {app_name}'")
72
+ create_result = run(
73
+ ["jac", "create_jac_app", app_name],
74
+ capture_output=True,
75
+ text=True,
76
+ )
77
+ print(
78
+ "[DEBUG] 'jac create_jac_app' completed "
79
+ f"returncode={create_result.returncode}\n"
80
+ f"STDOUT:\n{create_result.stdout}\n"
81
+ f"STDERR:\n{create_result.stderr}\n"
82
+ )
83
+
84
+ # If the currently installed `jac` CLI does not support `create_jac_app`,
85
+ # skip this integration test instead of failing the whole suite.
86
+ if (
87
+ create_result.returncode != 0
88
+ and "invalid choice: 'create_jac_app'" in create_result.stderr
89
+ ):
90
+ pytest.skip(
91
+ "Skipping: installed `jac` CLI does not support `create_jac_app`."
92
+ )
93
+
94
+ assert create_result.returncode == 0, (
95
+ "jac create_jac_app failed\n"
96
+ f"STDOUT:\n{create_result.stdout}\n"
97
+ f"STDERR:\n{create_result.stderr}\n"
98
+ )
99
+
100
+ project_path = os.path.join(temp_dir, app_name)
101
+ print(f"[DEBUG] Created base Jac app at {project_path}")
102
+ assert os.path.isdir(project_path)
103
+
104
+ # 2. Copy the contents from @all-in-one into the created app directory.
105
+ print("[DEBUG] Copying @all-in-one contents into created Jac app")
106
+ for entry in os.listdir(all_in_one_path):
107
+ src = os.path.join(all_in_one_path, entry)
108
+ dst = os.path.join(project_path, entry)
109
+ # Avoid copying node_modules / build artifacts from the example.
110
+ if entry in {"node_modules", "build", "dist", ".pytest_cache"}:
111
+ continue
112
+ if os.path.isdir(src):
113
+ shutil.copytree(src, dst, dirs_exist_ok=True)
114
+ else:
115
+ shutil.copy2(src, dst)
116
+
117
+ # 3. Run `npm install` inside the project directory so the frontend can build.
118
+ print("[DEBUG] Running 'npm install' in created Jac app")
119
+ npm_result = run(
120
+ ["npm", "install"],
121
+ cwd=project_path,
122
+ capture_output=True,
123
+ text=True,
124
+ )
125
+ print(
126
+ "[DEBUG] 'npm install' completed "
127
+ f"returncode={npm_result.returncode}\n"
128
+ f"STDOUT (truncated to 2000 chars):\n{npm_result.stdout[:2000]}\n"
129
+ f"STDERR (truncated to 2000 chars):\n{npm_result.stderr[:2000]}\n"
130
+ )
131
+
132
+ if npm_result.returncode != 0:
133
+ pytest.skip(
134
+ "Skipping: npm install failed or npm is not available in PATH."
135
+ )
136
+
137
+ 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"
139
+
140
+ # 4. Start the server: `jac serve app.jac`
141
+ # NOTE: We don't use text mode here, so `Popen` defaults to bytes.
142
+ # Use `Popen[bytes]` in the type annotation to keep mypy happy.
143
+ server: Popen[bytes] | None = None
144
+ try:
145
+ print("[DEBUG] Starting server with 'jac serve app.jac'")
146
+ server = Popen(
147
+ ["jac", "serve", "app.jac"],
148
+ cwd=project_path,
149
+ )
150
+
151
+ # Wait for localhost:8000 to become available
152
+ print("[DEBUG] Waiting for server to be available on 127.0.0.1:8000")
153
+ _wait_for_port("127.0.0.1", 8000, timeout=90.0)
154
+ print("[DEBUG] Server is now accepting connections on 127.0.0.1:8000")
155
+
156
+ # "/" – server up
157
+ try:
158
+ print("[DEBUG] Sending GET request to root endpoint /")
159
+ with urlopen(
160
+ "http://127.0.0.1:8000",
161
+ timeout=10,
162
+ ) as resp_root:
163
+ root_body = resp_root.read().decode("utf-8", errors="ignore")
164
+ print(
165
+ "[DEBUG] Received response from root endpoint /\n"
166
+ f"Status: {resp_root.status}\n"
167
+ f"Body (truncated to 500 chars):\n{root_body[:500]}"
168
+ )
169
+ assert resp_root.status == 200
170
+ assert '"Jac API Server"' in root_body
171
+ assert '"endpoints"' in root_body
172
+ except (URLError, HTTPError) as exc:
173
+ print(f"[DEBUG] Error while requesting root endpoint: {exc}")
174
+ pytest.fail(f"Failed to GET root endpoint: {exc}")
175
+
176
+ # "/page/app" – main page is loading
177
+ try:
178
+ print("[DEBUG] Sending GET request to /page/app endpoint")
179
+ with urlopen(
180
+ "http://127.0.0.1:8000/page/app",
181
+ timeout=200,
182
+ ) as resp_page:
183
+ page_body = resp_page.read().decode("utf-8", errors="ignore")
184
+ print(
185
+ "[DEBUG] Received response from /page/app endpoint\n"
186
+ f"Status: {resp_page.status}\n"
187
+ f"Body (truncated to 500 chars):\n{page_body[:500]}"
188
+ )
189
+ assert resp_page.status == 200
190
+ assert "<html" in page_body.lower()
191
+ except (URLError, HTTPError) as exc:
192
+ print(f"[DEBUG] Error while requesting /page/app endpoint: {exc}")
193
+ pytest.fail("Failed to GET /page/app endpoint")
194
+
195
+ # "/page/app#/nested" – relative paths / nested route
196
+ # (hash fragment is client-side only but server should still serve the app shell)
197
+ try:
198
+ print("[DEBUG] Sending GET request to /page/app#/nested endpoint")
199
+ with urlopen(
200
+ "http://127.0.0.1:8000/page/app#/nested",
201
+ timeout=200,
202
+ ) as resp_nested:
203
+ nested_body = resp_nested.read().decode(
204
+ "utf-8", errors="ignore"
205
+ )
206
+ print(
207
+ "[DEBUG] Received response from /page/app#/nested endpoint\n"
208
+ f"Status: {resp_nested.status}\n"
209
+ f"Body (truncated to 500 chars):\n{nested_body[:500]}"
210
+ )
211
+ assert resp_nested.status == 200
212
+ assert "<html" in nested_body.lower()
213
+ except (URLError, HTTPError) as exc:
214
+ print(
215
+ f"[DEBUG] Error while requesting /page/app#/nested endpoint: {exc}"
216
+ )
217
+ pytest.fail("Failed to GET /page/app#/nested endpoint")
218
+
219
+ # "/static/main.css" – CSS compiled and serving
220
+ try:
221
+ print("[DEBUG] Sending GET request to /static/main.css")
222
+ with urlopen(
223
+ "http://127.0.0.1:8000/static/main.css",
224
+ timeout=20,
225
+ ) as resp_css:
226
+ css_body = resp_css.read().decode("utf-8", errors="ignore")
227
+ print(
228
+ "[DEBUG] Received response from /static/main.css\n"
229
+ f"Status: {resp_css.status}\n"
230
+ f"Body (truncated to 500 chars):\n{css_body[:500]}"
231
+ )
232
+ assert resp_css.status == 200
233
+ assert len(css_body.strip()) > 0
234
+ except (URLError, HTTPError) as exc:
235
+ print(f"[DEBUG] Error while requesting /static/main.css: {exc}")
236
+ pytest.fail("Failed to GET /static/main.css")
237
+
238
+ # "/static/assets/burger.png" – static files are loading
239
+ try:
240
+ print("[DEBUG] Sending GET request to /static/assets/burger.png")
241
+ with urlopen(
242
+ "http://127.0.0.1:8000/static/assets/burger.png",
243
+ timeout=20,
244
+ ) as resp_png:
245
+ png_bytes = resp_png.read()
246
+ print(
247
+ "[DEBUG] Received response from /static/assets/burger.png\n"
248
+ f"Status: {resp_png.status}\n"
249
+ f"Content-Length: {len(png_bytes)} bytes"
250
+ )
251
+ assert resp_png.status == 200
252
+ assert len(png_bytes) > 0
253
+ assert png_bytes.startswith(b"\x89PNG"), (
254
+ "Expected PNG signature at start of burger.png"
255
+ )
256
+ except (URLError, HTTPError) as exc:
257
+ print(
258
+ f"[DEBUG] Error while requesting /static/assets/burger.png: {exc}"
259
+ )
260
+ pytest.fail("Failed to GET /static/assets/burger.png")
261
+
262
+ # "/walker/get_server_message" – walkers are integrated and up and running
263
+ try:
264
+ print("[DEBUG] Sending GET request to /walker/get_server_message")
265
+ with urlopen(
266
+ "http://127.0.0.1:8000/walker/get_server_message",
267
+ timeout=20,
268
+ ) as resp_walker:
269
+ walker_body = resp_walker.read().decode(
270
+ "utf-8", errors="ignore"
271
+ )
272
+ print(
273
+ "[DEBUG] Received response from /walker/get_server_message\n"
274
+ f"Status: {resp_walker.status}\n"
275
+ f"Body (truncated to 500 chars):\n{walker_body[:500]}"
276
+ )
277
+ assert resp_walker.status == 200
278
+ assert "get_server_message" in walker_body
279
+ except (URLError, HTTPError) as exc:
280
+ print(
281
+ f"[DEBUG] Error while requesting /walker/get_server_message: {exc}"
282
+ )
283
+ pytest.fail("Failed to GET /walker/get_server_message")
284
+
285
+ # POST /walker/create_todo – create a Todo via walker HTTP API
286
+ try:
287
+ print(
288
+ "[DEBUG] Sending POST request to /walker/create_todo endpoint"
289
+ )
290
+ payload = {
291
+ "text": "Sample todo from all-in-one app",
292
+ }
293
+ req = Request(
294
+ "http://127.0.0.1:8000/walker/create_todo",
295
+ data=json.dumps(payload).encode("utf-8"),
296
+ headers={"Content-Type": "application/json"},
297
+ method="POST",
298
+ )
299
+ with urlopen(req, timeout=20) as resp_create:
300
+ create_body = resp_create.read().decode(
301
+ "utf-8", errors="ignore"
302
+ )
303
+ print(
304
+ "[DEBUG] Received response from /walker/create_todo\n"
305
+ f"Status: {resp_create.status}\n"
306
+ f"Body (truncated to 500 chars):\n{create_body[:500]}"
307
+ )
308
+ assert resp_create.status == 200
309
+ # Basic sanity check: created Todo text should appear in the response payload.
310
+ assert "Sample todo from all-in-one app" in create_body
311
+ except (URLError, HTTPError) as exc:
312
+ print(f"[DEBUG] Error while requesting /walker/create_todo: {exc}")
313
+ pytest.fail("Failed to POST /walker/create_todo")
314
+
315
+ finally:
316
+ if server is not None:
317
+ print("[DEBUG] Terminating server process")
318
+ server.terminate()
319
+ try:
320
+ server.wait(timeout=15)
321
+ print("[DEBUG] Server process terminated cleanly")
322
+ except Exception:
323
+ print(
324
+ "[DEBUG] Server did not terminate cleanly, killing process"
325
+ )
326
+ server.kill()
327
+ finally:
328
+ print(f"[DEBUG] Restoring original working directory to {original_cwd}")
329
+ os.chdir(original_cwd)