Skip to content

Commit dae5d5e

Browse files
committed
Add integration tests with Playwright.
1 parent 2113f61 commit dae5d5e

File tree

3 files changed

+510
-3
lines changed

3 files changed

+510
-3
lines changed

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dev = [
3939
"ruff",
4040
"genbadge[all]>=1.1.1",
4141
"ty>=0.0.10",
42+
"pytest-playwright>=0.7.2",
4243
]
4344

4445
[project.optional-dependencies]
@@ -130,12 +131,13 @@ ban-relative-imports = "all"
130131
"tests/**/*" = ["PLR2004", "S101", "TID252"]
131132

132133
[tool.pytest.ini_options]
133-
addopts = "--quiet --failed-first --reuse-db --nomigrations -p no:warnings -m 'not slow' --benchmark-skip"
134+
addopts = "--quiet --failed-first --reuse-db --nomigrations -p no:warnings -m 'not slow and not integration' --benchmark-skip"
134135
testpaths = [
135136
"tests"
136137
]
137138
markers = [
138139
"slow: marks tests as slow",
140+
"integration: marks tests as integration",
139141
]
140142

141143
[tool.coverage.run]

tests/integration/test_basic.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""
2+
Playwright integration tests for django-unicorn example app.
3+
4+
These tests require the example app to be running on localhost:8080.
5+
Start it with: just runserver
6+
7+
These tests verify actual reactivity - that when inputs change, the DOM updates accordingly.
8+
"""
9+
10+
import pytest
11+
from playwright.sync_api import Page, expect
12+
13+
pytestmark = pytest.mark.integration
14+
15+
BASE_URL = "http://localhost:8080"
16+
17+
18+
class TestHomepage:
19+
"""Tests for the homepage."""
20+
21+
def test_homepage_loads(self, page: Page):
22+
"""Test that the homepage loads and contains expected content."""
23+
page.goto(BASE_URL, timeout=60000)
24+
expect(page).to_have_title("django-unicorn examples")
25+
expect(page.locator("body")).to_contain_text("unicorn")
26+
27+
28+
class TestTextInputsComponent:
29+
"""Tests for the text-inputs component with unicorn:model binding."""
30+
31+
def test_input_updates_display(self, page: Page):
32+
"""Test that typing in an input updates the displayed text."""
33+
page.goto(f"{BASE_URL}/text-inputs", timeout=60000)
34+
35+
# Wait for component to initialize
36+
page.wait_for_selector("[unicorn\\:id]", timeout=10000)
37+
38+
# Get the first component on the page
39+
component = page.locator("[unicorn\\:id]").first
40+
41+
# The component displays "Hello {{ name|title }}" - initially "Hello World"
42+
expect(component.locator("text=Hello World")).to_be_visible()
43+
44+
# Find a text input with unicorn:model="name" that doesn't have partial updates
45+
# The first one (nth 0) has u:partial.id="model-id" which would prevent the greeting from updating
46+
text_input = component.locator("input[unicorn\\:model='name']").nth(1)
47+
expect(text_input).to_be_visible()
48+
49+
# Clear and type a new name
50+
text_input.fill("")
51+
text_input.fill("Playwright")
52+
53+
# Blur to trigger the model update
54+
text_input.blur()
55+
56+
# Wait for the server response and DOM update
57+
page.wait_for_timeout(2000)
58+
59+
# The greeting should now show "Hello Playwright"
60+
expect(component.locator("text=Hello Playwright")).to_be_visible()
61+
62+
def test_button_click_updates_name(self, page: Page):
63+
"""Test that clicking a button updates the name via setter."""
64+
page.goto(f"{BASE_URL}/text-inputs", timeout=60000)
65+
66+
# Wait for component to initialize
67+
page.wait_for_selector("[unicorn\\:id]", timeout=10000)
68+
69+
# Get the first component
70+
component = page.locator("[unicorn\\:id]").first
71+
72+
# Find the button that sets name to 'human' (use first to handle multiple)
73+
button = component.locator("button:has-text(\"name='human'\")").first
74+
expect(button).to_be_visible()
75+
76+
# Click the button
77+
button.click()
78+
79+
# Wait for update
80+
page.wait_for_timeout(1500)
81+
82+
# Verify the greeting updated within this component
83+
expect(component.locator("text=Hello Human")).to_be_visible()
84+
85+
def test_reset_button_restores_initial_state(self, page: Page):
86+
"""Test that the $reset button restores the component to initial state."""
87+
page.goto(f"{BASE_URL}/text-inputs", timeout=60000)
88+
89+
# Wait for component to initialize
90+
page.wait_for_selector("[unicorn\\:id]", timeout=10000)
91+
92+
# Get the first component
93+
component = page.locator("[unicorn\\:id]").first
94+
95+
# Change the name first
96+
button = component.locator("button:has-text(\"name='human'\")").first
97+
button.click()
98+
page.wait_for_timeout(1500)
99+
100+
# Verify name changed
101+
expect(component.locator("text=Hello Human")).to_be_visible()
102+
103+
# Click reset (the reset button text is exactly "Reset the component")
104+
reset_button = component.locator("button:has-text('Reset the component')")
105+
reset_button.click()
106+
page.wait_for_timeout(1500)
107+
108+
# Verify name is back to "World"
109+
expect(component.locator("text=Hello World")).to_be_visible()
110+
111+
112+
class TestValidationComponent:
113+
"""Tests for the validation component."""
114+
115+
def test_text_input_updates_display(self, page: Page):
116+
"""Test that changing text input updates the displayed value."""
117+
page.goto(f"{BASE_URL}/validation", timeout=60000)
118+
119+
# Wait for component to initialize
120+
page.wait_for_selector("[unicorn\\:id]", timeout=10000)
121+
122+
# Initial state - component shows {{ text }}: hello
123+
component = page.locator("[unicorn\\:id]").first
124+
expect(component.locator("text=: hello")).to_be_visible()
125+
126+
# Find the text input by ID
127+
text_input = page.locator("#textId")
128+
expect(text_input).to_be_visible()
129+
130+
# Clear and type new value
131+
text_input.fill("")
132+
text_input.fill("testing")
133+
text_input.blur()
134+
135+
# Wait for update
136+
page.wait_for_timeout(1500)
137+
138+
# The displayed value should update
139+
expect(component.locator("text=: testing")).to_be_visible()
140+
141+
def test_button_click_sets_text(self, page: Page):
142+
"""Test that clicking set_text_no_validation updates text."""
143+
page.goto(f"{BASE_URL}/validation", timeout=60000)
144+
145+
# Wait for component to initialize
146+
page.wait_for_selector("[unicorn\\:id]", timeout=10000)
147+
148+
component = page.locator("[unicorn\\:id]").first
149+
150+
# Click the button
151+
button = page.locator("button:has-text('set_text_no_validation()')")
152+
button.click()
153+
154+
# Wait for update
155+
page.wait_for_timeout(1500)
156+
157+
# Text should be "no validation"
158+
expect(component.locator("text=: no validation")).to_be_visible()
159+
160+
def test_reset_restores_initial_state(self, page: Page):
161+
"""Test that $reset restores component to initial state."""
162+
page.goto(f"{BASE_URL}/validation", timeout=60000)
163+
164+
# Wait for component to initialize
165+
page.wait_for_selector("[unicorn\\:id]", timeout=10000)
166+
167+
component = page.locator("[unicorn\\:id]").first
168+
169+
# Change the text
170+
text_input = page.locator("#textId")
171+
text_input.fill("changed")
172+
text_input.blur()
173+
page.wait_for_timeout(1500)
174+
175+
# Verify it changed - look for the changed text
176+
expect(component.locator("text=: changed")).to_be_visible()
177+
178+
# Click reset - there are two reset buttons, pick the first one
179+
reset_button = page.locator("button:has-text('$reset')").first
180+
reset_button.click()
181+
page.wait_for_timeout(1500)
182+
183+
# Verify it's back to "hello"
184+
expect(component.locator("text=: hello")).to_be_visible()
185+
186+
187+
class TestPollingComponent:
188+
"""Tests for the polling component."""
189+
190+
def test_polling_page_loads(self, page: Page):
191+
"""Test that the polling page loads with a component."""
192+
page.goto(f"{BASE_URL}/polling", timeout=60000)
193+
194+
# Wait for component to initialize
195+
page.wait_for_selector("[unicorn\\:id]", timeout=10000)
196+
197+
# Component should be visible
198+
component = page.locator("[unicorn\\:id]").first
199+
expect(component).to_be_visible()
200+
201+
202+
class TestModelsComponent:
203+
"""Tests for the models component."""
204+
205+
def test_models_page_loads(self, page: Page):
206+
"""Test that the models page loads with a component."""
207+
page.goto(f"{BASE_URL}/models", timeout=60000)
208+
209+
# Wait for component to initialize
210+
page.wait_for_selector("[unicorn\\:id]", timeout=10000)
211+
212+
# Component should be visible
213+
component = page.locator("[unicorn\\:id]").first
214+
expect(component).to_be_visible()
215+
216+
217+
class TestHtmlInputsComponent:
218+
"""Tests for the html-inputs component."""
219+
220+
def test_html_inputs_page_loads(self, page: Page):
221+
"""Test that the html-inputs page loads with a component."""
222+
page.goto(f"{BASE_URL}/html-inputs", timeout=60000)
223+
224+
# Wait for component to initialize
225+
page.wait_for_selector("[unicorn\\:id]", timeout=10000)
226+
227+
# Component should be visible
228+
component = page.locator("[unicorn\\:id]").first
229+
expect(component).to_be_visible()
230+
231+
232+
class TestDirectView:
233+
"""Tests for direct view components."""
234+
235+
def test_direct_view_loads(self, page: Page):
236+
"""Test that direct view page loads correctly."""
237+
page.goto(f"{BASE_URL}/direct-view", timeout=60000)
238+
239+
# Wait for component to initialize
240+
page.wait_for_selector("[unicorn\\:id]", timeout=10000)
241+
242+
# Component should be visible
243+
component = page.locator("[unicorn\\:id]").first
244+
expect(component).to_be_visible()

0 commit comments

Comments
 (0)