python2mobile 1.0.1__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.
- examples/example_ecommerce_app.py +189 -0
- examples/example_todo_app.py +159 -0
- p2m/__init__.py +31 -0
- p2m/cli.py +470 -0
- p2m/config.py +205 -0
- p2m/core/__init__.py +18 -0
- p2m/core/api.py +191 -0
- p2m/core/ast_walker.py +171 -0
- p2m/core/database.py +192 -0
- p2m/core/events.py +56 -0
- p2m/core/render_engine.py +597 -0
- p2m/core/runtime.py +128 -0
- p2m/core/state.py +51 -0
- p2m/core/validator.py +284 -0
- p2m/devserver/__init__.py +9 -0
- p2m/devserver/server.py +84 -0
- p2m/i18n/__init__.py +7 -0
- p2m/i18n/translator.py +74 -0
- p2m/imagine/__init__.py +35 -0
- p2m/imagine/agent.py +463 -0
- p2m/imagine/legacy.py +217 -0
- p2m/llm/__init__.py +20 -0
- p2m/llm/anthropic_provider.py +78 -0
- p2m/llm/base.py +42 -0
- p2m/llm/compatible_provider.py +120 -0
- p2m/llm/factory.py +72 -0
- p2m/llm/ollama_provider.py +89 -0
- p2m/llm/openai_provider.py +79 -0
- p2m/testing/__init__.py +41 -0
- p2m/ui/__init__.py +43 -0
- p2m/ui/components.py +301 -0
- python2mobile-1.0.1.dist-info/METADATA +238 -0
- python2mobile-1.0.1.dist-info/RECORD +50 -0
- python2mobile-1.0.1.dist-info/WHEEL +5 -0
- python2mobile-1.0.1.dist-info/entry_points.txt +2 -0
- python2mobile-1.0.1.dist-info/top_level.txt +3 -0
- tests/test_basic_engine.py +281 -0
- tests/test_build_generation.py +603 -0
- tests/test_build_test_gate.py +150 -0
- tests/test_carousel_modal.py +84 -0
- tests/test_config_system.py +272 -0
- tests/test_i18n.py +101 -0
- tests/test_ifood_app_integration.py +172 -0
- tests/test_imagine_cli.py +133 -0
- tests/test_imagine_command.py +341 -0
- tests/test_llm_providers.py +321 -0
- tests/test_new_apps_integration.py +588 -0
- tests/test_ollama_functional.py +329 -0
- tests/test_real_world_apps.py +228 -0
- tests/test_run_integration.py +776 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for p2m build code generation.
|
|
3
|
+
|
|
4
|
+
Validates that CodeGenerator produces correct output files for all supported
|
|
5
|
+
platforms (Flutter, React Native, Web, Android, iOS) using the real test apps
|
|
6
|
+
from tests-p2m/.
|
|
7
|
+
|
|
8
|
+
Coverage:
|
|
9
|
+
- Project file loading from multi-directory app structures
|
|
10
|
+
- Output file existence and structure for each target
|
|
11
|
+
- Key content assertions per platform
|
|
12
|
+
- generate() dispatcher with valid/invalid targets
|
|
13
|
+
- All 5 targets against both todo_app and ecommerce_app
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
import json
|
|
18
|
+
import tempfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from unittest.mock import MagicMock, patch
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
# Framework path
|
|
24
|
+
FRAMEWORK_PATH = Path(__file__).parent.parent
|
|
25
|
+
TESTS_P2M_PATH = FRAMEWORK_PATH.parent / "tests-p2m"
|
|
26
|
+
|
|
27
|
+
if str(FRAMEWORK_PATH) not in sys.path:
|
|
28
|
+
sys.path.insert(0, str(FRAMEWORK_PATH))
|
|
29
|
+
|
|
30
|
+
from p2m.build.generator import CodeGenerator
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
# Fixtures
|
|
35
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def generator():
|
|
39
|
+
"""CodeGenerator instance with a mocked LLM provider (not used for generation)."""
|
|
40
|
+
with patch("p2m.build.generator.LLMFactory") as mock_factory:
|
|
41
|
+
mock_factory.create.return_value = MagicMock()
|
|
42
|
+
config = MagicMock()
|
|
43
|
+
config.llm.provider = "openai"
|
|
44
|
+
config.llm.api_key = "test-key"
|
|
45
|
+
config.llm.model = "gpt-4o"
|
|
46
|
+
config.llm.base_url = ""
|
|
47
|
+
config.llm.x_api_key = ""
|
|
48
|
+
gen = CodeGenerator(config)
|
|
49
|
+
return gen
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def todo_files(generator):
|
|
54
|
+
"""Python files loaded from tests-p2m/todo_app."""
|
|
55
|
+
return generator.load_project_files(str(TESTS_P2M_PATH / "todo_app"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.fixture
|
|
59
|
+
def ecommerce_files(generator):
|
|
60
|
+
"""Python files loaded from tests-p2m/ecommerce_app."""
|
|
61
|
+
return generator.load_project_files(str(TESTS_P2M_PATH / "ecommerce_app"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.fixture
|
|
65
|
+
def outdir():
|
|
66
|
+
"""Fresh temporary directory for each test's build output."""
|
|
67
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
68
|
+
yield Path(tmpdir)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
# Project file loading
|
|
73
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
class TestProjectFileLoading:
|
|
76
|
+
"""load_project_files() behavior."""
|
|
77
|
+
|
|
78
|
+
def test_loads_main_py_todo(self, generator):
|
|
79
|
+
"""todo_app: main.py at the project root is loaded."""
|
|
80
|
+
files = generator.load_project_files(str(TESTS_P2M_PATH / "todo_app"))
|
|
81
|
+
assert "main.py" in files
|
|
82
|
+
|
|
83
|
+
def test_loads_main_py_ecommerce(self, generator):
|
|
84
|
+
"""ecommerce_app: main.py at the project root is loaded."""
|
|
85
|
+
files = generator.load_project_files(str(TESTS_P2M_PATH / "ecommerce_app"))
|
|
86
|
+
assert "main.py" in files
|
|
87
|
+
|
|
88
|
+
def test_todo_main_has_create_view(self, generator):
|
|
89
|
+
"""todo_app/main.py contains the create_view function."""
|
|
90
|
+
files = generator.load_project_files(str(TESTS_P2M_PATH / "todo_app"))
|
|
91
|
+
assert "create_view" in files["main.py"]
|
|
92
|
+
|
|
93
|
+
def test_todo_main_has_event_handlers(self, generator):
|
|
94
|
+
"""todo_app/main.py registers handlers: add_todo, clear_done, nav_go."""
|
|
95
|
+
files = generator.load_project_files(str(TESTS_P2M_PATH / "todo_app"))
|
|
96
|
+
assert "add_todo" in files["main.py"]
|
|
97
|
+
assert "clear_done" in files["main.py"]
|
|
98
|
+
assert "nav_go" in files["main.py"]
|
|
99
|
+
|
|
100
|
+
def test_ecommerce_main_has_handlers(self, generator):
|
|
101
|
+
"""ecommerce_app/main.py registers search, cart, checkout handlers."""
|
|
102
|
+
files = generator.load_project_files(str(TESTS_P2M_PATH / "ecommerce_app"))
|
|
103
|
+
assert "search_products" in files["main.py"]
|
|
104
|
+
assert "confirm_order" in files["main.py"]
|
|
105
|
+
assert "nav_checkout" in files["main.py"]
|
|
106
|
+
|
|
107
|
+
def test_returns_dict_of_strings(self, generator):
|
|
108
|
+
"""load_project_files returns Dict[str, str]."""
|
|
109
|
+
files = generator.load_project_files(str(TESTS_P2M_PATH / "todo_app"))
|
|
110
|
+
assert isinstance(files, dict)
|
|
111
|
+
for key, value in files.items():
|
|
112
|
+
assert isinstance(key, str)
|
|
113
|
+
assert isinstance(value, str)
|
|
114
|
+
|
|
115
|
+
def test_ignores_test_prefix_files(self, generator, tmp_path):
|
|
116
|
+
"""Files starting with 'test_' are excluded."""
|
|
117
|
+
(tmp_path / "main.py").write_text("# app")
|
|
118
|
+
(tmp_path / "test_unit.py").write_text("# test")
|
|
119
|
+
files = generator.load_project_files(str(tmp_path))
|
|
120
|
+
assert "main.py" in files
|
|
121
|
+
assert "test_unit.py" not in files
|
|
122
|
+
|
|
123
|
+
def test_loads_subdirectory_py_files(self, generator):
|
|
124
|
+
"""
|
|
125
|
+
load_project_files recursively loads all .py files from subdirectories
|
|
126
|
+
including state/, views/, and components/.
|
|
127
|
+
"""
|
|
128
|
+
files = generator.load_project_files(str(TESTS_P2M_PATH / "todo_app"))
|
|
129
|
+
# main.py at root plus subdirectory module files
|
|
130
|
+
assert "main.py" in files
|
|
131
|
+
# At least one subdirectory file should be loaded
|
|
132
|
+
subdirectory_files = [k for k in files.keys() if "/" in k or "\\" in k]
|
|
133
|
+
assert len(subdirectory_files) > 0, "Expected subdirectory .py files to be loaded"
|
|
134
|
+
|
|
135
|
+
def test_ecommerce_loads_subdirectory_files(self, generator):
|
|
136
|
+
"""ecommerce_app subdirectory files (state/, views/, components/) are loaded."""
|
|
137
|
+
files = generator.load_project_files(str(TESTS_P2M_PATH / "ecommerce_app"))
|
|
138
|
+
assert "main.py" in files
|
|
139
|
+
subdirectory_files = [k for k in files.keys() if "/" in k or "\\" in k]
|
|
140
|
+
assert len(subdirectory_files) > 0, "Expected subdirectory .py files to be loaded"
|
|
141
|
+
|
|
142
|
+
def test_multiple_root_py_files(self, generator, tmp_path):
|
|
143
|
+
"""Multiple root-level .py files are all loaded."""
|
|
144
|
+
(tmp_path / "main.py").write_text("# main")
|
|
145
|
+
(tmp_path / "utils.py").write_text("# utils")
|
|
146
|
+
(tmp_path / "config.py").write_text("# config")
|
|
147
|
+
files = generator.load_project_files(str(tmp_path))
|
|
148
|
+
assert "main.py" in files
|
|
149
|
+
assert "utils.py" in files
|
|
150
|
+
assert "config.py" in files
|
|
151
|
+
|
|
152
|
+
def test_empty_project_dir(self, generator, tmp_path):
|
|
153
|
+
"""Empty directory returns an empty dict."""
|
|
154
|
+
files = generator.load_project_files(str(tmp_path))
|
|
155
|
+
assert files == {}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
# Flutter generation
|
|
160
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
class TestFlutterGeneration:
|
|
163
|
+
"""generate_flutter() / generate('flutter', ...) output validation."""
|
|
164
|
+
|
|
165
|
+
def test_creates_pubspec_yaml(self, generator, todo_files, outdir):
|
|
166
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
167
|
+
assert (outdir / "pubspec.yaml").exists()
|
|
168
|
+
|
|
169
|
+
def test_creates_lib_main_dart(self, generator, todo_files, outdir):
|
|
170
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
171
|
+
assert (outdir / "lib" / "main.dart").exists()
|
|
172
|
+
|
|
173
|
+
def test_pubspec_has_flutter_sdk(self, generator, todo_files, outdir):
|
|
174
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
175
|
+
content = (outdir / "pubspec.yaml").read_text()
|
|
176
|
+
assert "flutter:" in content
|
|
177
|
+
assert "sdk: flutter" in content
|
|
178
|
+
|
|
179
|
+
def test_pubspec_has_http(self, generator, todo_files, outdir):
|
|
180
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
181
|
+
content = (outdir / "pubspec.yaml").read_text()
|
|
182
|
+
assert "http:" in content
|
|
183
|
+
|
|
184
|
+
def test_pubspec_has_shared_preferences(self, generator, todo_files, outdir):
|
|
185
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
186
|
+
content = (outdir / "pubspec.yaml").read_text()
|
|
187
|
+
assert "shared_preferences:" in content
|
|
188
|
+
|
|
189
|
+
def test_pubspec_has_valid_app_name(self, generator, todo_files, outdir):
|
|
190
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
191
|
+
content = (outdir / "pubspec.yaml").read_text()
|
|
192
|
+
assert "name: p2m_app" in content
|
|
193
|
+
|
|
194
|
+
def test_main_dart_imports_flutter(self, generator, todo_files, outdir):
|
|
195
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
196
|
+
content = (outdir / "lib" / "main.dart").read_text()
|
|
197
|
+
assert "import 'package:flutter/material.dart'" in content
|
|
198
|
+
|
|
199
|
+
def test_main_dart_has_main_entry(self, generator, todo_files, outdir):
|
|
200
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
201
|
+
content = (outdir / "lib" / "main.dart").read_text()
|
|
202
|
+
assert "void main()" in content
|
|
203
|
+
assert "runApp" in content
|
|
204
|
+
|
|
205
|
+
def test_main_dart_has_material_app(self, generator, todo_files, outdir):
|
|
206
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
207
|
+
content = (outdir / "lib" / "main.dart").read_text()
|
|
208
|
+
assert "MaterialApp" in content
|
|
209
|
+
|
|
210
|
+
def test_main_dart_has_stateful_widget(self, generator, todo_files, outdir):
|
|
211
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
212
|
+
content = (outdir / "lib" / "main.dart").read_text()
|
|
213
|
+
assert "StatefulWidget" in content
|
|
214
|
+
|
|
215
|
+
def test_main_dart_has_build_method(self, generator, todo_files, outdir):
|
|
216
|
+
generator.generate_flutter(todo_files, str(outdir))
|
|
217
|
+
content = (outdir / "lib" / "main.dart").read_text()
|
|
218
|
+
assert "Widget build(BuildContext context)" in content
|
|
219
|
+
|
|
220
|
+
def test_flutter_ecommerce(self, generator, ecommerce_files, outdir):
|
|
221
|
+
"""Flutter generation works for ecommerce_app too."""
|
|
222
|
+
generator.generate_flutter(ecommerce_files, str(outdir))
|
|
223
|
+
assert (outdir / "pubspec.yaml").exists()
|
|
224
|
+
assert (outdir / "lib" / "main.dart").exists()
|
|
225
|
+
|
|
226
|
+
def test_output_dir_created_if_missing(self, generator, todo_files, tmp_path):
|
|
227
|
+
"""generate_flutter creates the output directory if it does not exist."""
|
|
228
|
+
nested = tmp_path / "deep" / "nested" / "build"
|
|
229
|
+
generator.generate_flutter(todo_files, str(nested))
|
|
230
|
+
assert nested.exists()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
234
|
+
# React Native generation
|
|
235
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
class TestReactNativeGeneration:
|
|
238
|
+
"""generate_react_native() / generate('react-native', ...) output validation."""
|
|
239
|
+
|
|
240
|
+
def test_creates_package_json(self, generator, todo_files, outdir):
|
|
241
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
242
|
+
assert (outdir / "package.json").exists()
|
|
243
|
+
|
|
244
|
+
def test_creates_app_tsx(self, generator, todo_files, outdir):
|
|
245
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
246
|
+
assert (outdir / "App.tsx").exists()
|
|
247
|
+
|
|
248
|
+
def test_package_json_valid_json(self, generator, todo_files, outdir):
|
|
249
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
250
|
+
content = json.loads((outdir / "package.json").read_text())
|
|
251
|
+
assert isinstance(content, dict)
|
|
252
|
+
|
|
253
|
+
def test_package_json_has_react_native(self, generator, todo_files, outdir):
|
|
254
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
255
|
+
content = json.loads((outdir / "package.json").read_text())
|
|
256
|
+
assert "react-native" in content["dependencies"]
|
|
257
|
+
|
|
258
|
+
def test_package_json_has_react(self, generator, todo_files, outdir):
|
|
259
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
260
|
+
content = json.loads((outdir / "package.json").read_text())
|
|
261
|
+
assert "react" in content["dependencies"]
|
|
262
|
+
|
|
263
|
+
def test_package_json_has_typescript_devdep(self, generator, todo_files, outdir):
|
|
264
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
265
|
+
content = json.loads((outdir / "package.json").read_text())
|
|
266
|
+
assert "typescript" in content["devDependencies"]
|
|
267
|
+
|
|
268
|
+
def test_package_json_scripts(self, generator, todo_files, outdir):
|
|
269
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
270
|
+
content = json.loads((outdir / "package.json").read_text())
|
|
271
|
+
assert "start" in content["scripts"]
|
|
272
|
+
assert "android" in content["scripts"]
|
|
273
|
+
assert "ios" in content["scripts"]
|
|
274
|
+
|
|
275
|
+
def test_app_tsx_imports_react(self, generator, todo_files, outdir):
|
|
276
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
277
|
+
content = (outdir / "App.tsx").read_text()
|
|
278
|
+
assert "import React" in content
|
|
279
|
+
|
|
280
|
+
def test_app_tsx_imports_react_native(self, generator, todo_files, outdir):
|
|
281
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
282
|
+
content = (outdir / "App.tsx").read_text()
|
|
283
|
+
assert "react-native" in content
|
|
284
|
+
|
|
285
|
+
def test_app_tsx_default_export(self, generator, todo_files, outdir):
|
|
286
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
287
|
+
content = (outdir / "App.tsx").read_text()
|
|
288
|
+
assert "export default" in content
|
|
289
|
+
|
|
290
|
+
def test_app_tsx_safe_area_view(self, generator, todo_files, outdir):
|
|
291
|
+
generator.generate_react_native(todo_files, str(outdir))
|
|
292
|
+
content = (outdir / "App.tsx").read_text()
|
|
293
|
+
assert "SafeAreaView" in content
|
|
294
|
+
|
|
295
|
+
def test_rn_ecommerce(self, generator, ecommerce_files, outdir):
|
|
296
|
+
"""React Native generation works for ecommerce_app."""
|
|
297
|
+
generator.generate_react_native(ecommerce_files, str(outdir))
|
|
298
|
+
assert (outdir / "App.tsx").exists()
|
|
299
|
+
content = json.loads((outdir / "package.json").read_text())
|
|
300
|
+
assert "react-native" in content["dependencies"]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
304
|
+
# Web generation
|
|
305
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
class TestWebGeneration:
|
|
308
|
+
"""generate_web() / generate('web', ...) output validation."""
|
|
309
|
+
|
|
310
|
+
def test_creates_index_html(self, generator, todo_files, outdir):
|
|
311
|
+
generator.generate_web(todo_files, str(outdir))
|
|
312
|
+
assert (outdir / "index.html").exists()
|
|
313
|
+
|
|
314
|
+
def test_index_html_doctype(self, generator, todo_files, outdir):
|
|
315
|
+
generator.generate_web(todo_files, str(outdir))
|
|
316
|
+
content = (outdir / "index.html").read_text()
|
|
317
|
+
assert "<!DOCTYPE html>" in content
|
|
318
|
+
|
|
319
|
+
def test_index_html_structure(self, generator, todo_files, outdir):
|
|
320
|
+
generator.generate_web(todo_files, str(outdir))
|
|
321
|
+
content = (outdir / "index.html").read_text()
|
|
322
|
+
assert "<html" in content
|
|
323
|
+
assert "</html>" in content
|
|
324
|
+
assert "<head" in content
|
|
325
|
+
assert "<body" in content
|
|
326
|
+
|
|
327
|
+
def test_index_html_charset(self, generator, todo_files, outdir):
|
|
328
|
+
generator.generate_web(todo_files, str(outdir))
|
|
329
|
+
content = (outdir / "index.html").read_text()
|
|
330
|
+
assert "charset" in content.lower()
|
|
331
|
+
|
|
332
|
+
def test_index_html_viewport_meta(self, generator, todo_files, outdir):
|
|
333
|
+
generator.generate_web(todo_files, str(outdir))
|
|
334
|
+
content = (outdir / "index.html").read_text()
|
|
335
|
+
assert "viewport" in content
|
|
336
|
+
|
|
337
|
+
def test_index_html_python2mobile_brand(self, generator, todo_files, outdir):
|
|
338
|
+
generator.generate_web(todo_files, str(outdir))
|
|
339
|
+
content = (outdir / "index.html").read_text()
|
|
340
|
+
assert "Python2Mobile" in content
|
|
341
|
+
|
|
342
|
+
def test_index_html_has_script(self, generator, todo_files, outdir):
|
|
343
|
+
generator.generate_web(todo_files, str(outdir))
|
|
344
|
+
content = (outdir / "index.html").read_text()
|
|
345
|
+
assert "<script" in content
|
|
346
|
+
|
|
347
|
+
def test_index_html_has_style(self, generator, todo_files, outdir):
|
|
348
|
+
generator.generate_web(todo_files, str(outdir))
|
|
349
|
+
content = (outdir / "index.html").read_text()
|
|
350
|
+
assert "<style" in content
|
|
351
|
+
|
|
352
|
+
def test_web_ecommerce(self, generator, ecommerce_files, outdir):
|
|
353
|
+
"""Web generation works for ecommerce_app."""
|
|
354
|
+
generator.generate_web(ecommerce_files, str(outdir))
|
|
355
|
+
assert (outdir / "index.html").exists()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
359
|
+
# Android generation
|
|
360
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
class TestAndroidGeneration:
|
|
363
|
+
"""generate_android() / generate('android', ...) output validation."""
|
|
364
|
+
|
|
365
|
+
def test_creates_build_gradle(self, generator, todo_files, outdir):
|
|
366
|
+
generator.generate_android(todo_files, str(outdir))
|
|
367
|
+
assert (outdir / "build.gradle").exists()
|
|
368
|
+
|
|
369
|
+
def test_creates_android_manifest(self, generator, todo_files, outdir):
|
|
370
|
+
generator.generate_android(todo_files, str(outdir))
|
|
371
|
+
assert (outdir / "AndroidManifest.xml").exists()
|
|
372
|
+
|
|
373
|
+
def test_creates_main_activity_kt(self, generator, todo_files, outdir):
|
|
374
|
+
generator.generate_android(todo_files, str(outdir))
|
|
375
|
+
assert (outdir / "MainActivity.kt").exists()
|
|
376
|
+
|
|
377
|
+
def test_creates_activity_main_xml(self, generator, todo_files, outdir):
|
|
378
|
+
generator.generate_android(todo_files, str(outdir))
|
|
379
|
+
assert (outdir / "activity_main.xml").exists()
|
|
380
|
+
|
|
381
|
+
def test_manifest_has_internet_permission(self, generator, todo_files, outdir):
|
|
382
|
+
generator.generate_android(todo_files, str(outdir))
|
|
383
|
+
content = (outdir / "AndroidManifest.xml").read_text()
|
|
384
|
+
assert "android.permission.INTERNET" in content
|
|
385
|
+
|
|
386
|
+
def test_manifest_has_main_activity(self, generator, todo_files, outdir):
|
|
387
|
+
generator.generate_android(todo_files, str(outdir))
|
|
388
|
+
content = (outdir / "AndroidManifest.xml").read_text()
|
|
389
|
+
assert "MainActivity" in content
|
|
390
|
+
assert "android.intent.action.MAIN" in content
|
|
391
|
+
|
|
392
|
+
def test_manifest_has_package(self, generator, todo_files, outdir):
|
|
393
|
+
generator.generate_android(todo_files, str(outdir))
|
|
394
|
+
content = (outdir / "AndroidManifest.xml").read_text()
|
|
395
|
+
assert 'package="com.p2m.app"' in content
|
|
396
|
+
|
|
397
|
+
def test_main_activity_package(self, generator, todo_files, outdir):
|
|
398
|
+
generator.generate_android(todo_files, str(outdir))
|
|
399
|
+
content = (outdir / "MainActivity.kt").read_text()
|
|
400
|
+
assert "package com.p2m.app" in content
|
|
401
|
+
|
|
402
|
+
def test_main_activity_class(self, generator, todo_files, outdir):
|
|
403
|
+
generator.generate_android(todo_files, str(outdir))
|
|
404
|
+
content = (outdir / "MainActivity.kt").read_text()
|
|
405
|
+
assert "class MainActivity" in content
|
|
406
|
+
assert "AppCompatActivity" in content
|
|
407
|
+
|
|
408
|
+
def test_main_activity_on_create(self, generator, todo_files, outdir):
|
|
409
|
+
generator.generate_android(todo_files, str(outdir))
|
|
410
|
+
content = (outdir / "MainActivity.kt").read_text()
|
|
411
|
+
assert "onCreate" in content
|
|
412
|
+
|
|
413
|
+
def test_build_gradle_kotlin_android(self, generator, todo_files, outdir):
|
|
414
|
+
generator.generate_android(todo_files, str(outdir))
|
|
415
|
+
content = (outdir / "build.gradle").read_text()
|
|
416
|
+
assert "kotlin-android" in content
|
|
417
|
+
|
|
418
|
+
def test_build_gradle_compile_sdk(self, generator, todo_files, outdir):
|
|
419
|
+
generator.generate_android(todo_files, str(outdir))
|
|
420
|
+
content = (outdir / "build.gradle").read_text()
|
|
421
|
+
assert "compileSdk" in content
|
|
422
|
+
|
|
423
|
+
def test_build_gradle_appcompat(self, generator, todo_files, outdir):
|
|
424
|
+
generator.generate_android(todo_files, str(outdir))
|
|
425
|
+
content = (outdir / "build.gradle").read_text()
|
|
426
|
+
assert "appcompat" in content
|
|
427
|
+
|
|
428
|
+
def test_layout_xml_is_valid(self, generator, todo_files, outdir):
|
|
429
|
+
generator.generate_android(todo_files, str(outdir))
|
|
430
|
+
content = (outdir / "activity_main.xml").read_text()
|
|
431
|
+
assert '<?xml version="1.0"' in content
|
|
432
|
+
assert "layout_width" in content
|
|
433
|
+
|
|
434
|
+
def test_android_ecommerce(self, generator, ecommerce_files, outdir):
|
|
435
|
+
"""Android generation works for ecommerce_app."""
|
|
436
|
+
generator.generate_android(ecommerce_files, str(outdir))
|
|
437
|
+
assert (outdir / "MainActivity.kt").exists()
|
|
438
|
+
assert (outdir / "AndroidManifest.xml").exists()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
442
|
+
# iOS generation
|
|
443
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
class TestIOSGeneration:
|
|
446
|
+
"""generate_ios() / generate('ios', ...) output validation."""
|
|
447
|
+
|
|
448
|
+
def test_creates_package_swift(self, generator, todo_files, outdir):
|
|
449
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
450
|
+
assert (outdir / "Package.swift").exists()
|
|
451
|
+
|
|
452
|
+
def test_creates_content_view_swift(self, generator, todo_files, outdir):
|
|
453
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
454
|
+
assert (outdir / "ContentView.swift").exists()
|
|
455
|
+
|
|
456
|
+
def test_creates_app_swift(self, generator, todo_files, outdir):
|
|
457
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
458
|
+
assert (outdir / "App.swift").exists()
|
|
459
|
+
|
|
460
|
+
def test_package_swift_targets_ios14(self, generator, todo_files, outdir):
|
|
461
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
462
|
+
content = (outdir / "Package.swift").read_text()
|
|
463
|
+
assert ".iOS(.v14)" in content
|
|
464
|
+
|
|
465
|
+
def test_package_swift_has_swift_tools_version(self, generator, todo_files, outdir):
|
|
466
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
467
|
+
content = (outdir / "Package.swift").read_text()
|
|
468
|
+
assert "swift-tools-version" in content
|
|
469
|
+
|
|
470
|
+
def test_content_view_imports_swiftui(self, generator, todo_files, outdir):
|
|
471
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
472
|
+
content = (outdir / "ContentView.swift").read_text()
|
|
473
|
+
assert "import SwiftUI" in content
|
|
474
|
+
|
|
475
|
+
def test_content_view_struct(self, generator, todo_files, outdir):
|
|
476
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
477
|
+
content = (outdir / "ContentView.swift").read_text()
|
|
478
|
+
assert "struct ContentView" in content
|
|
479
|
+
|
|
480
|
+
def test_content_view_body_property(self, generator, todo_files, outdir):
|
|
481
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
482
|
+
content = (outdir / "ContentView.swift").read_text()
|
|
483
|
+
assert "var body: some View" in content
|
|
484
|
+
|
|
485
|
+
def test_content_view_has_state(self, generator, todo_files, outdir):
|
|
486
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
487
|
+
content = (outdir / "ContentView.swift").read_text()
|
|
488
|
+
assert "@State" in content
|
|
489
|
+
|
|
490
|
+
def test_app_swift_has_main_attribute(self, generator, todo_files, outdir):
|
|
491
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
492
|
+
content = (outdir / "App.swift").read_text()
|
|
493
|
+
assert "@main" in content
|
|
494
|
+
|
|
495
|
+
def test_app_swift_imports_swiftui(self, generator, todo_files, outdir):
|
|
496
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
497
|
+
content = (outdir / "App.swift").read_text()
|
|
498
|
+
assert "import SwiftUI" in content
|
|
499
|
+
|
|
500
|
+
def test_app_swift_window_group(self, generator, todo_files, outdir):
|
|
501
|
+
generator.generate_ios(todo_files, str(outdir))
|
|
502
|
+
content = (outdir / "App.swift").read_text()
|
|
503
|
+
assert "WindowGroup" in content
|
|
504
|
+
|
|
505
|
+
def test_ios_ecommerce(self, generator, ecommerce_files, outdir):
|
|
506
|
+
"""iOS generation works for ecommerce_app."""
|
|
507
|
+
generator.generate_ios(ecommerce_files, str(outdir))
|
|
508
|
+
assert (outdir / "ContentView.swift").exists()
|
|
509
|
+
assert (outdir / "App.swift").exists()
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
513
|
+
# generate() dispatcher
|
|
514
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
class TestGenerateDispatcher:
|
|
517
|
+
"""The generate() method routes to the correct target generator."""
|
|
518
|
+
|
|
519
|
+
def test_flutter(self, generator, todo_files, outdir):
|
|
520
|
+
generator.generate("flutter", todo_files, str(outdir))
|
|
521
|
+
assert (outdir / "pubspec.yaml").exists()
|
|
522
|
+
|
|
523
|
+
def test_react_native(self, generator, todo_files, outdir):
|
|
524
|
+
generator.generate("react-native", todo_files, str(outdir))
|
|
525
|
+
assert (outdir / "App.tsx").exists()
|
|
526
|
+
|
|
527
|
+
def test_web(self, generator, todo_files, outdir):
|
|
528
|
+
generator.generate("web", todo_files, str(outdir))
|
|
529
|
+
assert (outdir / "index.html").exists()
|
|
530
|
+
|
|
531
|
+
def test_android(self, generator, todo_files, outdir):
|
|
532
|
+
generator.generate("android", todo_files, str(outdir))
|
|
533
|
+
assert (outdir / "MainActivity.kt").exists()
|
|
534
|
+
|
|
535
|
+
def test_ios(self, generator, todo_files, outdir):
|
|
536
|
+
generator.generate("ios", todo_files, str(outdir))
|
|
537
|
+
assert (outdir / "ContentView.swift").exists()
|
|
538
|
+
|
|
539
|
+
def test_invalid_target_raises_value_error(self, generator, todo_files, outdir):
|
|
540
|
+
with pytest.raises(ValueError, match="Unsupported target"):
|
|
541
|
+
generator.generate("xamarin", todo_files, str(outdir))
|
|
542
|
+
|
|
543
|
+
def test_unsupported_target_message_includes_target(self, generator, todo_files, outdir):
|
|
544
|
+
with pytest.raises(ValueError, match="xamarin"):
|
|
545
|
+
generator.generate("xamarin", todo_files, str(outdir))
|
|
546
|
+
|
|
547
|
+
def test_case_insensitive_flutter(self, generator, todo_files, outdir):
|
|
548
|
+
"""Target strings are lowercased before dispatch."""
|
|
549
|
+
generator.generate("Flutter", todo_files, str(outdir))
|
|
550
|
+
assert (outdir / "pubspec.yaml").exists()
|
|
551
|
+
|
|
552
|
+
def test_case_insensitive_android(self, generator, todo_files, outdir):
|
|
553
|
+
generator.generate("ANDROID", todo_files, str(outdir))
|
|
554
|
+
assert (outdir / "MainActivity.kt").exists()
|
|
555
|
+
|
|
556
|
+
def test_supported_targets_constant(self):
|
|
557
|
+
"""SUPPORTED_TARGETS lists all 5 expected platforms."""
|
|
558
|
+
assert set(CodeGenerator.SUPPORTED_TARGETS) == {
|
|
559
|
+
"flutter", "react-native", "web", "android", "ios"
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
564
|
+
# All targets × both apps
|
|
565
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
class TestAllTargetsBothApps:
|
|
568
|
+
"""Generate all 5 targets for both todo_app and ecommerce_app."""
|
|
569
|
+
|
|
570
|
+
@pytest.mark.parametrize("target", CodeGenerator.SUPPORTED_TARGETS)
|
|
571
|
+
def test_todo_app_all_targets(self, generator, todo_files, target):
|
|
572
|
+
"""todo_app generates without error for every supported target."""
|
|
573
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
574
|
+
generator.generate(target, todo_files, tmpdir)
|
|
575
|
+
assert len(list(Path(tmpdir).iterdir())) > 0, (
|
|
576
|
+
f"No output files created for target '{target}'"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
@pytest.mark.parametrize("target", CodeGenerator.SUPPORTED_TARGETS)
|
|
580
|
+
def test_ecommerce_app_all_targets(self, generator, ecommerce_files, target):
|
|
581
|
+
"""ecommerce_app generates without error for every supported target."""
|
|
582
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
583
|
+
generator.generate(target, ecommerce_files, tmpdir)
|
|
584
|
+
assert len(list(Path(tmpdir).iterdir())) > 0, (
|
|
585
|
+
f"No output files created for target '{target}'"
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
@pytest.mark.parametrize("target,expected_file", [
|
|
589
|
+
("flutter", "pubspec.yaml"),
|
|
590
|
+
("react-native", "App.tsx"),
|
|
591
|
+
("web", "index.html"),
|
|
592
|
+
("android", "MainActivity.kt"),
|
|
593
|
+
("ios", "ContentView.swift"),
|
|
594
|
+
])
|
|
595
|
+
def test_key_output_file_per_target(self, generator, todo_files, target, expected_file):
|
|
596
|
+
"""Each target produces its primary output file for todo_app."""
|
|
597
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
598
|
+
generator.generate(target, todo_files, tmpdir)
|
|
599
|
+
# Flutter nests main.dart under lib/
|
|
600
|
+
if target == "flutter":
|
|
601
|
+
assert (Path(tmpdir) / "lib" / "main.dart").exists()
|
|
602
|
+
else:
|
|
603
|
+
assert (Path(tmpdir) / expected_file).exists()
|