Skip to content

Commit 7b64191

Browse files
apiadclaude
andcommitted
test(engine): add vertical slice 1 smoke tests + pin known gaps
Covers request → response → reactive-binding end to end via FastAPI's TestClient: basic view render, @app.local SSR data-bind emission for text and value, @app.server.rpc body validation, bundle endpoint syntax check. Two known gaps pinned as strict xfails: - examples/reactivity.py:73 uses class_name= because there is no first-class reactive class= binding (self._classes lives outside self._attrs, so the proxy-check at render time never fires for the class attribute). - _generate_bundle filters decorator lines with c.startswith('@') instead of c.strip().startswith('@'); nested @app.client.* / @app.local defs produce invalid bundles. Real usage defines them at module level, where this doesn't bite. 11 passed, 2 xfailed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b238859 commit 7b64191

1 file changed

Lines changed: 206 additions & 0 deletions

File tree

tests/test_engine.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""
2+
Engine smoke tests — vertical slice 1.
3+
4+
Covers the request → response → reactive-binding chain at the integration
5+
layer using FastAPI's TestClient. Intentionally light on internal unit tests
6+
of state/dom internals; those follow in slice 2.
7+
8+
Browser-only branches (the IS_BROWSER guards in state.py, dom.py, client.py)
9+
are NOT exercised here — they require a Pyodide simulator and are deferred.
10+
"""
11+
from dataclasses import dataclass
12+
13+
import pytest
14+
from fastapi.testclient import TestClient
15+
16+
from violetear import App, Document
17+
18+
19+
# Module-level definitions used by the bundle test.
20+
# The bundle generator (app.py:_generate_bundle) emits user state classes and
21+
# client functions at module scope, so it pulls their source via inspect.getsource.
22+
# If the originals are nested inside another function, the source comes back
23+
# indented and the bundle becomes invalid — see test_bundle_rejects_nested_defs
24+
# below for the pinned bug.
25+
@dataclass
26+
class _BundleState:
27+
n: int = 0
28+
29+
30+
async def _bundle_click(event):
31+
pass
32+
33+
34+
def test_view_renders_html_doc():
35+
"""A basic @app.view route returns a fully rendered HTML document."""
36+
app = App(title="Smoke Test", version="t1")
37+
38+
@app.view("/")
39+
def home():
40+
doc = Document(title="Hello")
41+
with doc.body as b:
42+
b.h1("It works")
43+
return doc
44+
45+
client = TestClient(app.api)
46+
r = client.get("/")
47+
48+
assert r.status_code == 200
49+
assert "text/html" in r.headers["content-type"]
50+
assert r.text.lstrip().startswith("<!DOCTYPE html>")
51+
assert "<title>Hello</title>" in r.text
52+
assert "<h1>" in r.text
53+
assert "It works" in r.text
54+
55+
56+
def test_reactive_ssr_emits_data_bind_for_text_and_value():
57+
"""@app.local proxies passed to .text() / .value() emit data-bind-* companions."""
58+
app = App(title="Reactive", version="t2")
59+
60+
@app.local
61+
@dataclass
62+
class UiState:
63+
theme: str = "light"
64+
username: str = "Guest"
65+
66+
@app.view("/")
67+
def home():
68+
doc = Document(title="Reactive")
69+
with doc.body as b:
70+
b.span().text(UiState.theme).id("theme-display")
71+
b.input(type="text").value(UiState.username).id("name-input")
72+
return doc
73+
74+
client = TestClient(app.api)
75+
html = client.get("/").text
76+
77+
# Static values rendered for SSR (so first paint is correct before hydration)
78+
assert "light" in html
79+
assert 'value="Guest"' in html
80+
81+
# data-bind-* companions emitted for the client runtime to discover
82+
assert 'data-bind-text="UiState.theme"' in html
83+
assert 'data-bind-value="UiState.username"' in html
84+
85+
86+
def test_rpc_endpoint_validates_body_and_returns_result():
87+
"""@app.server.rpc exposes the function via POST /_violetear/rpc/<name> with Pydantic body validation."""
88+
app = App(title="RPC", version="t3")
89+
90+
@app.server.rpc
91+
async def add(x: int, y: int) -> int:
92+
return x + y
93+
94+
@app.view("/")
95+
def home():
96+
return Document(title="RPC")
97+
98+
client = TestClient(app.api)
99+
100+
# Happy path
101+
r = client.post("/_violetear/rpc/add", json={"x": 2, "y": 3})
102+
assert r.status_code == 200, r.text
103+
assert r.json() == 5
104+
105+
# Pydantic validation: missing field rejected as 422
106+
r = client.post("/_violetear/rpc/add", json={"x": 2})
107+
assert r.status_code == 422
108+
109+
# Pydantic coercion: non-coercible value rejected as 422
110+
r = client.post("/_violetear/rpc/add", json={"x": "abc", "y": 1})
111+
assert r.status_code == 422
112+
113+
114+
def test_bundle_endpoint_returns_compilable_python():
115+
"""GET /_violetear/bundle.py returns Pyodide-bound source that is at minimum
116+
syntactically valid Python — catches regressions in the bundle generator.
117+
118+
Uses module-level state + callback because the bundle generator expects
119+
its inputs to be defined at module scope (see test_bundle_rejects_nested_defs)."""
120+
app = App(title="Bundle", version="t4")
121+
app.local(_BundleState)
122+
app.client.callback(_bundle_click)
123+
124+
@app.view("/")
125+
def home():
126+
return Document(title="Bundle")
127+
128+
client = TestClient(app.api)
129+
r = client.get("/_violetear/bundle.py")
130+
131+
assert r.status_code == 200
132+
assert "text/x-python" in r.headers["content-type"]
133+
134+
# The bundle must compile under the host Python — surfaces any
135+
# syntax-breaking regression in _generate_bundle / dedent layout.
136+
compile(r.text, "<violetear-bundle>", "exec")
137+
138+
139+
@pytest.mark.xfail(
140+
reason=(
141+
"Bundle generator (_generate_bundle in app.py:632-634) filters decorator lines "
142+
"with `c.startswith('@')` instead of `c.strip().startswith('@')`. When a client "
143+
"function is defined nested inside another function, its source comes back "
144+
"indented; the decorator line survives the filter and the bundle becomes invalid "
145+
"Python. Also affects state classes (similar pattern at line 617 uses .strip() "
146+
"for the decorator but still leaves class body indentation intact). Pinned as a "
147+
"known limitation: real-world usage defines @app.client.* and @app.local at "
148+
"module level, where this doesn't bite."
149+
),
150+
strict=True,
151+
)
152+
def test_bundle_rejects_nested_defs():
153+
"""Pin the known limitation: the bundle generator only supports module-level
154+
state classes and client functions; nested definitions produce invalid bundles."""
155+
app = App(title="Nested", version="t6")
156+
157+
@app.local
158+
@dataclass
159+
class NestedState:
160+
n: int = 0
161+
162+
@app.client.callback
163+
async def nested_click(event):
164+
NestedState.n += 1
165+
166+
@app.view("/")
167+
def home():
168+
return Document(title="x")
169+
170+
client = TestClient(app.api)
171+
r = client.get("/_violetear/bundle.py")
172+
compile(r.text, "<violetear-bundle>", "exec")
173+
174+
175+
@pytest.mark.xfail(
176+
reason=(
177+
"examples/reactivity.py:73 uses class_name= on b.div(...). Element treats "
178+
"that as a raw attr (rendered as class_name=\"...\" + data-bind-class_name=...) "
179+
"instead of class=/data-bind-class. Reactive class bindings aren't supported "
180+
"by markup.py: self._classes lives outside self._attrs, so the proxy-check at "
181+
"render time never fires for the class= attribute. Pinned as a known gap "
182+
"rather than silently fixed."
183+
),
184+
strict=True,
185+
)
186+
def test_reactive_class_binding_emits_data_bind_class():
187+
"""Pin the known gap: there is no first-class way to reactively bind the class= attribute."""
188+
app = App(title="Class Binding", version="t5")
189+
190+
@app.local
191+
@dataclass
192+
class UiState:
193+
theme: str = "light"
194+
195+
@app.view("/")
196+
def home():
197+
doc = Document(title="x")
198+
with doc.body as b:
199+
b.div(class_name=UiState.theme, id="app-container")
200+
return doc
201+
202+
client = TestClient(app.api)
203+
html = client.get("/").text
204+
205+
assert 'class="light"' in html
206+
assert 'data-bind-class="UiState.theme"' in html

0 commit comments

Comments
 (0)