Skip to content

Commit 1a39bbe

Browse files
committed
Add another test for the clone form errors and fix the message generation
1 parent 76ead89 commit 1a39bbe

File tree

3 files changed

+210
-1
lines changed

3 files changed

+210
-1
lines changed

content_editor/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ def save_related(self, request, form, formsets, change):
266266
else:
267267
self.message_user(
268268
request,
269-
gettext("Cloning plugins failed: {}").format(form.errors),
269+
gettext("Cloning plugins failed: {}").format(clone_form.errors),
270270
level=messages.ERROR,
271271
)
272272

tests/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- `test_clone_plugins_functionality`: End-to-end UI + database testing
77
- `test_clone_backend_logic`: Backend-only form processing
88
- `test_clone_insert_between_existing_content`: Insert positioning tests
9+
- `test_clone_backend_validation_error`: Error message validation when cloning form is invalid
910

1011
### Critical Insights
1112
**Clone Ordering**: `_clone_ordering` = ordering value to insert *before*. After save, existing content gets renormalized but cloned content keeps its target ordering value. This is correct behavior.
@@ -14,6 +15,59 @@
1415

1516
**Region Targeting**: Use `[data-region="sidebar"] .order-machine-insert-target` for region-specific insert targets.
1617

18+
## Test Writing Guidelines
19+
20+
### Be Strict - No Fallbacks
21+
- **Always test the exact expected behavior** - don't use fallback logic that would mask failing code
22+
- **Verify specific error messages** - check for exact text, field names, and validation details
23+
- **Assert on precise conditions** - avoid "if this doesn't work, try that" patterns
24+
- **Let tests fail when code is broken** - fallbacks hide bugs and prevent proper debugging
25+
- **Test database state directly** - verify exact counts, field values, and relationships
26+
- **Use specific selectors** - don't fall back to generic ones that might match wrong elements
27+
- **Validate full user experience** - error messages must be visible to actual users
28+
29+
### Common Anti-Patterns to Avoid
30+
- `assert True` after conditional checks (masks real failures)
31+
- `try/except` blocks that suppress assertion errors
32+
- Generic error message checks instead of specific validation
33+
- Counting "at least N" instead of "exactly N" when exact is expected
34+
- Checking "something worked" instead of "the right thing worked correctly"
35+
36+
Example of **BAD** test pattern:
37+
```python
38+
# DON'T DO THIS - fallbacks hide real issues
39+
if "expected error" in messages:
40+
assert True # Found expected error
41+
else:
42+
# Fallback that masks the real problem
43+
if any_error_messages:
44+
assert True # "At least some error occurred"
45+
46+
# DON'T DO THIS - vague assertions
47+
assert len(items) >= 2 # Could be wrong number
48+
try:
49+
some_assertion()
50+
except AssertionError:
51+
pass # Ignoring failures
52+
```
53+
54+
Example of **GOOD** test pattern:
55+
```python
56+
# DO THIS - strict assertions reveal real issues
57+
assert "Cloning plugins failed" in error_message, f"Expected error not found: {error_message}"
58+
assert "This field is required" in error_message, f"Field validation missing: {error_message}"
59+
60+
# DO THIS - precise validation
61+
assert len(sidebar_items) == 3, f"Expected exactly 3 items, got {len(sidebar_items)}"
62+
assert item.ordering == 200, f"Expected ordering 200, got {item.ordering}"
63+
```
64+
65+
### When Adding New Tests
66+
1. **First make the test fail** - verify it catches the bug/missing feature
67+
2. **Make it pass with minimal code** - implement just enough to pass
68+
3. **Verify error messages reach users** - check admin UI displays them correctly
69+
4. **Test edge cases strictly** - empty inputs, invalid data, boundary conditions
70+
1771
### Running Tests
1872
```bash
1973
python -m pytest tests/testapp/test_playwright.py -v

tests/testapp/test_playwright.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,3 +1548,158 @@ def test_clone_backend_logic():
15481548

15491549
print("✓ Backend cloning test completed successfully")
15501550
print(f"✓ Cloned content with orderings: {sidebar_orderings}")
1551+
1552+
1553+
@pytest.mark.django_db
1554+
def test_clone_backend_validation_error():
1555+
"""Test that validation errors in the CloneForm are properly displayed to the user."""
1556+
# Create test user and login
1557+
User.objects.create_superuser("admin", "[email protected]", "password")
1558+
client = Client()
1559+
client.login(username="admin", password="password")
1560+
1561+
# Create article with content in main region
1562+
article = Article.objects.create(title="Clone Validation Test Article")
1563+
1564+
# Add some content to clone
1565+
richtext1 = article.testapp_richtext_set.create(
1566+
text="<p>Test content to clone</p>", region="main", ordering=10
1567+
)
1568+
1569+
# Get the admin change URL
1570+
admin_url = reverse("admin:testapp_article_change", args=[article.pk])
1571+
1572+
# First, GET the form to get proper formset data
1573+
response = client.get(admin_url)
1574+
assert response.status_code == 200
1575+
1576+
# Extract formset management data from the form
1577+
soup = BeautifulSoup(response.content, "html.parser")
1578+
1579+
# Find all the formset management form fields
1580+
management_fields = {}
1581+
for input_field in soup.find_all("input"):
1582+
name = input_field.get("name", "")
1583+
value = input_field.get("value", "")
1584+
if name and (
1585+
"TOTAL_FORMS" in name
1586+
or "INITIAL_FORMS" in name
1587+
or "MIN_NUM_FORMS" in name
1588+
or "MAX_NUM_FORMS" in name
1589+
):
1590+
management_fields[name] = value
1591+
elif name == "csrfmiddlewaretoken":
1592+
csrf_token = value
1593+
1594+
# Add existing inline data (to preserve existing content)
1595+
inline_data = {}
1596+
for input_field in soup.find_all("input"):
1597+
name = input_field.get("name", "")
1598+
value = input_field.get("value", "")
1599+
if name and (
1600+
name.startswith("testapp_")
1601+
and (
1602+
"-id" in name
1603+
or "-DELETE" in name
1604+
or any(
1605+
field in name for field in ["text", "file", "ordering", "region"]
1606+
)
1607+
)
1608+
):
1609+
inline_data[name] = value
1610+
1611+
# Create invalid clone form data - missing required _clone_region field
1612+
# This should trigger a validation error
1613+
invalid_form_data = QueryDict(mutable=True)
1614+
invalid_form_data["title"] = article.title
1615+
invalid_form_data["csrfmiddlewaretoken"] = csrf_token
1616+
invalid_form_data["_continue"] = "Save and continue editing"
1617+
1618+
# Add management form data
1619+
for key, value in management_fields.items():
1620+
invalid_form_data[key] = value
1621+
1622+
# Add existing inline data
1623+
for key, value in inline_data.items():
1624+
invalid_form_data[key] = value
1625+
1626+
# Add clone data but with missing _clone_region (required field)
1627+
# This should cause CloneForm.is_valid() to return False
1628+
invalid_form_data.appendlist("_clone", f"testapp.richtext:{richtext1.pk}")
1629+
# Intentionally omit _clone_region to trigger validation error
1630+
1631+
print("Submitting invalid clone form data (missing _clone_region)...")
1632+
1633+
# Submit the form with invalid clone data
1634+
response = client.post(admin_url, invalid_form_data)
1635+
1636+
print(f"POST response status: {response.status_code}")
1637+
1638+
# Check if we got a redirect (success) or stayed on the same page (error)
1639+
if response.status_code == 302:
1640+
print(f"Redirected to: {response['Location']}")
1641+
# Follow the redirect to check for error messages
1642+
response = client.get(response["Location"])
1643+
1644+
# Parse the response content to look for error messages
1645+
soup = BeautifulSoup(response.content, "html.parser")
1646+
1647+
# Look for Django messages (both error and success messages)
1648+
messages_divs = soup.find_all("div", class_="messagelist")
1649+
error_messages = []
1650+
success_messages = []
1651+
1652+
for messages_div in messages_divs:
1653+
# Look for error messages specifically
1654+
error_li = messages_div.find_all("li", class_="error")
1655+
success_li = messages_div.find_all("li", class_="success")
1656+
1657+
for li in error_li:
1658+
error_messages.append(li.get_text().strip())
1659+
for li in success_li:
1660+
success_messages.append(li.get_text().strip())
1661+
1662+
# Also look for general message containers that might contain errors
1663+
if not error_messages:
1664+
# Try alternative selectors for error messages
1665+
alt_errors = soup.find_all("li", class_="error")
1666+
for li in alt_errors:
1667+
error_messages.append(li.get_text().strip())
1668+
1669+
print(f"Error messages found: {error_messages}")
1670+
print(f"Success messages found: {success_messages}")
1671+
1672+
# Verify that we got a cloning validation error message with specific field error details
1673+
# The error should mention "Cloning plugins failed" and include the missing field error
1674+
cloning_error_found = False
1675+
field_error_found = False
1676+
1677+
for msg in error_messages:
1678+
if "Cloning plugins failed" in msg:
1679+
cloning_error_found = True
1680+
print(f"✓ Found cloning error message: {msg}")
1681+
1682+
# Check if the error message contains details about the missing field
1683+
if "_clone_region" in msg or "This field is required" in msg:
1684+
field_error_found = True
1685+
print(f"✓ Error message includes field validation details: {msg}")
1686+
break
1687+
1688+
# Both conditions must be met for the test to pass
1689+
assert cloning_error_found, (
1690+
f"Expected 'Cloning plugins failed' message not found in: {error_messages}"
1691+
)
1692+
assert field_error_found, (
1693+
"Expected field error details (_clone_region or 'This field is required') not found in error message"
1694+
)
1695+
1696+
# Verify that cloning actually failed (no content should be cloned to sidebar)
1697+
article.refresh_from_db()
1698+
sidebar_count = article.testapp_richtext_set.filter(region="sidebar").count()
1699+
assert sidebar_count == 0, (
1700+
f"Cloning should have failed, but found {sidebar_count} items in sidebar"
1701+
)
1702+
1703+
print("✓ Validation correctly prevented cloning and showed detailed error message")
1704+
1705+
print("✓ Clone form validation error test completed")

0 commit comments

Comments
 (0)