summaryrefslogtreecommitdiff
path: root/tests/admin_inlines
diff options
context:
space:
mode:
authorMarijke Luttekes <mail@marijkeluttekes.dev>2024-05-21 22:09:26 -0300
committernessita <124304+nessita@users.noreply.github.com>2024-05-22 00:13:55 -0300
commit01ed59f753139afb514170ee7f7384c155ecbc2d (patch)
tree4b2a50dfa6abc08d1eb0f439f8078e7c1df1b8d0 /tests/admin_inlines
parent9c5fe93349bd4339c41d057b87046e5d28be6f77 (diff)
Refs #35189 -- Improved admin fieldset's accessibility by setting aria-labelledby.
Before this change, HTML <fieldset> elements in the admin site did not have an associated label to describe them. This commit defines a unique HTML id for the heading labeling a fieldset, and sets its aria-labelledby property to link the heading with the fieldset.
Diffstat (limited to 'tests/admin_inlines')
-rw-r--r--tests/admin_inlines/admin.py54
-rw-r--r--tests/admin_inlines/models.py21
-rw-r--r--tests/admin_inlines/tests.py420
3 files changed, 451 insertions, 44 deletions
diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py
index 0ec56d71b3..3cdaee22df 100644
--- a/tests/admin_inlines/admin.py
+++ b/tests/admin_inlines/admin.py
@@ -40,6 +40,8 @@ from .models import (
OutfitItem,
ParentModelWithCustomPk,
Person,
+ Photo,
+ Photographer,
Poll,
Profile,
ProfileCollection,
@@ -98,6 +100,57 @@ class AuthorAdmin(admin.ModelAdmin):
]
+class PhotoInlineMixin:
+ model = Photo
+ extra = 2
+ fieldsets = [
+ (None, {"fields": ["image", "title"]}),
+ (
+ "Details",
+ {"fields": ["description", "creation_date"], "classes": ["collapse"]},
+ ),
+ (
+ "Details", # Fieldset name intentionally duplicated
+ {"fields": ["update_date", "updated_by"]},
+ ),
+ ]
+
+
+class PhotoTabularInline(PhotoInlineMixin, admin.TabularInline):
+ pass
+
+
+class PhotoStackedExtra2Inline(PhotoInlineMixin, admin.StackedInline):
+ pass
+
+
+class PhotoStackedExtra3Inline(PhotoInlineMixin, admin.StackedInline):
+ extra = 3
+
+
+class PhotoStackedCollapsibleInline(PhotoInlineMixin, admin.StackedInline):
+ fieldsets = []
+ classes = ["collapse"]
+
+
+class PhotographerAdmin(admin.ModelAdmin):
+ fieldsets = [
+ (None, {"fields": ["firstname", "fullname"]}),
+ ("Advanced options", {"fields": ["nationality", "residency"]}),
+ (
+ "Advanced options", # Fieldset name intentionally duplicated
+ {"fields": ["siblings", "children"], "classes": ["collapse"]},
+ ),
+ ]
+ inlines = [
+ PhotoTabularInline,
+ PhotoTabularInline,
+ PhotoStackedExtra2Inline,
+ PhotoStackedExtra3Inline,
+ PhotoStackedCollapsibleInline,
+ ]
+
+
class InnerInline(admin.StackedInline):
model = Inner
can_delete = False
@@ -454,6 +507,7 @@ site.register(Teacher, TeacherAdmin)
site.register(Chapter, inlines=[FootNoteNonEditableInlineCustomForm])
site.register(OutfitItem, inlines=[WeaknessInlineCustomForm])
site.register(Person, inlines=[AuthorTabularInline, FashonistaStackedInline])
+site.register(Photographer, PhotographerAdmin)
site.register(Course, ClassAdminStackedHorizontal)
site.register(CourseProxy, ClassAdminStackedVertical)
site.register(CourseProxy1, ClassAdminTabularVertical)
diff --git a/tests/admin_inlines/models.py b/tests/admin_inlines/models.py
index eca5bae422..5a85556a55 100644
--- a/tests/admin_inlines/models.py
+++ b/tests/admin_inlines/models.py
@@ -180,6 +180,27 @@ class ShoppingWeakness(models.Model):
item = models.ForeignKey(OutfitItem, models.CASCADE)
+# Models for #35189
+
+
+class Photographer(Person):
+ fullname = models.CharField(max_length=100)
+ nationality = models.CharField(max_length=100)
+ residency = models.CharField(max_length=100)
+ siblings = models.IntegerField()
+ children = models.IntegerField()
+
+
+class Photo(models.Model):
+ photographer = models.ForeignKey(Photographer, on_delete=models.CASCADE)
+ image = models.CharField(max_length=100)
+ title = models.CharField(max_length=100)
+ description = models.TextField()
+ creation_date = models.DateField()
+ update_date = models.DateField()
+ updated_by = models.CharField(max_length=100)
+
+
# Models for #13510
diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py
index 9533cc9af3..7b38619cc0 100644
--- a/tests/admin_inlines/tests.py
+++ b/tests/admin_inlines/tests.py
@@ -117,7 +117,14 @@ class TestInline(TestDataMixin, TestCase):
"Autogenerated many-to-many inlines are displayed correctly (#13407)"
response = self.client.get(reverse("admin:admin_inlines_author_add"))
# The heading for the m2m inline block uses the right text
- self.assertContains(response, "<h2>Author-book relationships</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="Author_books-heading" class="inline-heading">'
+ "Author-book relationships</h2>"
+ ),
+ html=True,
+ )
# The "add another" label is correct
self.assertContains(response, "Add another Author-book relationship")
# The '+' is dropped from the autogenerated form prefix (Author_books+)
@@ -737,13 +744,35 @@ class TestInline(TestDataMixin, TestCase):
def test_inlines_plural_heading_foreign_key(self):
response = self.client.get(reverse("admin:admin_inlines_holder4_add"))
- self.assertContains(response, "<h2>Inner4 stackeds</h2>", html=True)
- self.assertContains(response, "<h2>Inner4 tabulars</h2>", html=True)
+ self.assertContains(
+ response,
+ (
+ '<h2 id="inner4stacked_set-heading" class="inline-heading">'
+ "Inner4 stackeds</h2>"
+ ),
+ html=True,
+ )
+ self.assertContains(
+ response,
+ (
+ '<h2 id="inner4tabular_set-heading" class="inline-heading">'
+ "Inner4 tabulars</h2>"
+ ),
+ html=True,
+ )
def test_inlines_singular_heading_one_to_one(self):
response = self.client.get(reverse("admin:admin_inlines_person_add"))
- self.assertContains(response, "<h2>Author</h2>", html=True) # Tabular.
- self.assertContains(response, "<h2>Fashionista</h2>", html=True) # Stacked.
+ self.assertContains(
+ response,
+ '<h2 id="author-heading" class="inline-heading">Author</h2>',
+ html=True,
+ ) # Tabular.
+ self.assertContains(
+ response,
+ '<h2 id="fashionista-heading" class="inline-heading">Fashionista</h2>',
+ html=True,
+ ) # Stacked.
def test_inlines_based_on_model_state(self):
parent = ShowInlineParent.objects.create(show_inlines=False)
@@ -914,28 +943,50 @@ class TestInlinePermissions(TestCase):
def test_inline_add_m2m_noperm(self):
response = self.client.get(reverse("admin:admin_inlines_author_add"))
# No change permission on books, so no inline
- self.assertNotContains(response, "<h2>Author-book relationships</h2>")
+ self.assertNotContains(
+ response,
+ (
+ '<h2 id="Author_books-heading" class="inline-heading">'
+ "Author-book relationships</h2>"
+ ),
+ html=True,
+ )
self.assertNotContains(response, "Add another Author-Book Relationship")
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_add_fk_noperm(self):
response = self.client.get(reverse("admin:admin_inlines_holder2_add"))
# No permissions on Inner2s, so no inline
- self.assertNotContains(response, "<h2>Inner2s</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+ html=True,
+ )
self.assertNotContains(response, "Add another Inner2")
self.assertNotContains(response, 'id="id_inner2_set-TOTAL_FORMS"')
def test_inline_change_m2m_noperm(self):
response = self.client.get(self.author_change_url)
# No change permission on books, so no inline
- self.assertNotContains(response, "<h2>Author-book relationships</h2>")
+ self.assertNotContains(
+ response,
+ (
+ '<h2 id="Author_books-heading" class="inline-heading">'
+ "Author-book relationships</h2>"
+ ),
+ html=True,
+ )
self.assertNotContains(response, "Add another Author-Book Relationship")
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_change_fk_noperm(self):
response = self.client.get(self.holder_change_url)
# No permissions on Inner2s, so no inline
- self.assertNotContains(response, "<h2>Inner2s</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+ html=True,
+ )
self.assertNotContains(response, "Add another Inner2")
self.assertNotContains(response, 'id="id_inner2_set-TOTAL_FORMS"')
@@ -959,7 +1010,14 @@ class TestInlinePermissions(TestCase):
self.assertIs(
response.context["inline_admin_formset"].has_delete_permission, False
)
- self.assertContains(response, "<h2>Author-book relationships</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="Author_books-heading" class="inline-heading">'
+ "Author-book relationships</h2>"
+ ),
+ html=True,
+ )
self.assertContains(
response,
'<input type="hidden" name="Author_books-TOTAL_FORMS" value="0" '
@@ -975,7 +1033,14 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(reverse("admin:admin_inlines_author_add"))
# No change permission on Books, so no inline
- self.assertNotContains(response, "<h2>Author-book relationships</h2>")
+ self.assertNotContains(
+ response,
+ (
+ '<h2 id="Author_books-heading" class="inline-heading">'
+ "Author-book relationships</h2>"
+ ),
+ html=True,
+ )
self.assertNotContains(response, "Add another Author-Book Relationship")
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
@@ -986,7 +1051,11 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(reverse("admin:admin_inlines_holder2_add"))
# Add permission on inner2s, so we get the inline
- self.assertContains(response, "<h2>Inner2s</h2>")
+ self.assertContains(
+ response,
+ '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+ html=True,
+ )
self.assertContains(response, "Add another Inner2")
self.assertContains(
response,
@@ -1002,7 +1071,14 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.author_change_url)
# No change permission on books, so no inline
- self.assertNotContains(response, "<h2>Author-book relationships</h2>")
+ self.assertNotContains(
+ response,
+ (
+ '<h2 id="Author_books-heading" class="inline-heading">'
+ "Author-book relationships</h2>"
+ ),
+ html=True,
+ )
self.assertNotContains(response, "Add another Author-Book Relationship")
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
@@ -1026,7 +1102,14 @@ class TestInlinePermissions(TestCase):
self.assertIs(
response.context["inline_admin_formset"].has_delete_permission, False
)
- self.assertContains(response, "<h2>Author-book relationships</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="Author_books-heading" class="inline-heading">'
+ "Author-book relationships</h2>"
+ ),
+ html=True,
+ )
self.assertContains(
response,
'<input type="hidden" name="Author_books-TOTAL_FORMS" value="1" '
@@ -1059,7 +1142,14 @@ class TestInlinePermissions(TestCase):
self.assertIs(
response.context["inline_admin_formset"].has_delete_permission, True
)
- self.assertContains(response, "<h2>Author-book relationships</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="Author_books-heading" class="inline-heading">'
+ "Author-book relationships</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Author-book relationship")
self.assertContains(
response,
@@ -1082,7 +1172,11 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# Add permission on inner2s, so we can add but not modify existing
- self.assertContains(response, "<h2>Inner2s</h2>")
+ self.assertContains(
+ response,
+ '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+ html=True,
+ )
self.assertContains(response, "Add another Inner2")
# 3 extra forms only, not the existing instance form
self.assertContains(
@@ -1105,7 +1199,16 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# Change permission on inner2s, so we can change existing but not add new
- self.assertContains(response, "<h2>Inner2s</h2>", count=2)
+ self.assertContains(
+ response,
+ '<h2 id="inner2_set-heading" class="inline-heading">Inner2s</h2>',
+ html=True,
+ )
+ self.assertContains(
+ response,
+ '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+ html=True,
+ )
# Just the one form for existing instances
self.assertContains(
response,
@@ -1148,7 +1251,11 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# Add/change perm, so we can add new and change existing
- self.assertContains(response, "<h2>Inner2s</h2>")
+ self.assertContains(
+ response,
+ '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+ html=True,
+ )
# One form for existing instance and three extra for new
self.assertContains(
response,
@@ -1174,7 +1281,11 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# Change/delete perm on inner2s, so we can change/delete existing
- self.assertContains(response, "<h2>Inner2s</h2>")
+ self.assertContains(
+ response,
+ '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+ html=True,
+ )
# One form for existing instance only, no new
self.assertContains(
response,
@@ -1205,7 +1316,16 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# All perms on inner2s, so we can add/change/delete
- self.assertContains(response, "<h2>Inner2s</h2>", count=2)
+ self.assertContains(
+ response,
+ '<h2 id="inner2_set-heading" class="inline-heading">Inner2s</h2>',
+ html=True,
+ )
+ self.assertContains(
+ response,
+ '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+ html=True,
+ )
# One form for existing instance only, three for new
self.assertContains(
response,
@@ -1367,22 +1487,69 @@ class TestVerboseNameInlineForms(TestDataMixin, TestCase):
response = modeladmin.changeform_view(request)
self.assertNotContains(response, "Add another Profile")
# Non-verbose model.
- self.assertContains(response, "<h2>Non-verbose childss</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="profile_set-heading" class="inline-heading">'
+ "Non-verbose childss</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Non-verbose child")
- self.assertNotContains(response, "<h2>Profiles</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="profile_set-heading" class="inline-heading">Profiles</h2>',
+ html=True,
+ )
# Model with verbose name.
- self.assertContains(response, "<h2>Childs with verbose names</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+ "Childs with verbose names</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Childs with verbose name")
- self.assertNotContains(response, "<h2>Model with verbose name onlys</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+ "Model with verbose name onlys</h2>",
+ html=True,
+ )
self.assertNotContains(response, "Add another Model with verbose name only")
# Model with verbose name plural.
- self.assertContains(response, "<h2>Childs with verbose name plurals</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+ "Childs with verbose name plurals</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Childs with verbose name plural")
- self.assertNotContains(response, "<h2>Model with verbose name plural only</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+ "Model with verbose name plural only</h2>",
+ html=True,
+ )
# Model with both verbose names.
- self.assertContains(response, "<h2>Childs with both verbose namess</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+ "Childs with both verbose namess</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Childs with both verbose names")
- self.assertNotContains(response, "<h2>Model with both - plural name</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+ "Model with both - plural name</h2>",
+ html=True,
+ )
self.assertNotContains(response, "Add another Model with both - name")
def test_verbose_name_plural_inline(self):
@@ -1415,21 +1582,68 @@ class TestVerboseNameInlineForms(TestDataMixin, TestCase):
request.user = self.superuser
response = modeladmin.changeform_view(request)
# Non-verbose model.
- self.assertContains(response, "<h2>Non-verbose childs</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="profile_set-heading" class="inline-heading">'
+ "Non-verbose childs</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Profile")
- self.assertNotContains(response, "<h2>Profiles</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="profile_set-heading" class="inline-heading">Profiles</h2>',
+ html=True,
+ )
# Model with verbose name.
- self.assertContains(response, "<h2>Childs with verbose name</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+ "Childs with verbose name</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Model with verbose name only")
- self.assertNotContains(response, "<h2>Model with verbose name onlys</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+ "Model with verbose name onlys</h2>",
+ html=True,
+ )
# Model with verbose name plural.
- self.assertContains(response, "<h2>Childs with verbose name plural</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+ "Childs with verbose name plural</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Profile")
- self.assertNotContains(response, "<h2>Model with verbose name plural only</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+ "Model with verbose name plural only</h2>",
+ html=True,
+ )
# Model with both verbose names.
- self.assertContains(response, "<h2>Childs with both verbose names</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+ "Childs with both verbose names</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Model with both - name")
- self.assertNotContains(response, "<h2>Model with both - plural name</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+ "Model with both - plural name</h2>",
+ html=True,
+ )
def test_both_verbose_names_inline(self):
class NonVerboseProfileInline(TabularInline):
@@ -1466,31 +1680,149 @@ class TestVerboseNameInlineForms(TestDataMixin, TestCase):
response = modeladmin.changeform_view(request)
self.assertNotContains(response, "Add another Profile")
# Non-verbose model.
- self.assertContains(response, "<h2>Non-verbose childs - plural name</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="profile_set-heading" class="inline-heading">'
+ "Non-verbose childs - plural name</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Non-verbose childs - name")
- self.assertNotContains(response, "<h2>Profiles</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="profile_set-heading" class="inline-heading">Profiles</h2>',
+ html=True,
+ )
# Model with verbose name.
- self.assertContains(response, "<h2>Childs with verbose name - plural name</h2>")
+ self.assertContains(
+ response,
+ (
+ '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+ "Childs with verbose name - plural name</h2>"
+ ),
+ html=True,
+ )
self.assertContains(response, "Add another Childs with verbose name - name")
- self.assertNotContains(response, "<h2>Model with verbose name onlys</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+ "Model with verbose name onlys</h2>",
+ html=True,
+ )
# Model with verbose name plural.
self.assertContains(
response,
- "<h2>Childs with verbose name plural - plural name</h2>",
+ (
+ '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+ "Childs with verbose name plural - plural name</h2>"
+ ),
+ html=True,
)
self.assertContains(
response,
"Add another Childs with verbose name plural - name",
)
- self.assertNotContains(response, "<h2>Model with verbose name plural only</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+ "Model with verbose name plural only</h2>",
+ html=True,
+ )
# Model with both verbose names.
- self.assertContains(response, "<h2>Childs with both - plural name</h2>")
+ self.assertContains(
+ response,
+ '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+ "Childs with both - plural name</h2>",
+ html=True,
+ )
self.assertContains(response, "Add another Childs with both - name")
- self.assertNotContains(response, "<h2>Model with both - plural name</h2>")
+ self.assertNotContains(
+ response,
+ '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+ "Model with both - plural name</h2>",
+ html=True,
+ )
self.assertNotContains(response, "Add another Model with both - name")
@override_settings(ROOT_URLCONF="admin_inlines.urls")
+class TestInlineWithFieldsets(TestDataMixin, TestCase):
+ def setUp(self):
+ self.client.force_login(self.superuser)
+
+ def test_inline_headings(self):
+ response = self.client.get(reverse("admin:admin_inlines_photographer_add"))
+ # Page main title.
+ self.assertContains(response, "<h1>Add photographer</h1>", html=True)
+
+ # Headings for the toplevel fieldsets. The first one has no name.
+ self.assertContains(response, '<fieldset class="module aligned ">')
+ # The second and third have the same "Advanced options" name, but the
+ # second one has the "collapse" class.
+ for x, classes in ((1, ""), (2, "collapse")):
+ heading_id = f"fieldset-0-advanced-options-{x}-heading"
+ with self.subTest(heading_id=heading_id):
+ self.assertContains(
+ response,
+ f'<fieldset class="module aligned {classes}" '
+ f'aria-labelledby="{heading_id}">',
+ )
+ self.assertContains(
+ response,
+ f'<h2 id="{heading_id}" class="fieldset-heading">'
+ "Advanced options</h2>",
+ )
+ self.assertContains(response, f'id="{heading_id}"', count=1)
+
+ # Headings and subheadings for all the inlines.
+ for inline_admin_formset in response.context["inline_admin_formsets"]:
+ prefix = inline_admin_formset.formset.prefix
+ heading_id = f"{prefix}-heading"
+ formset_heading = (
+ f'<h2 id="{heading_id}" class="inline-heading">Photos</h2>'
+ )
+ self.assertContains(response, formset_heading, html=True)
+ self.assertContains(response, f'id="{heading_id}"', count=1)
+
+ # If this is a TabularInline, do not make further asserts since
+ # fieldsets are not shown as such in this table layout.
+ if "tabular" in inline_admin_formset.opts.template:
+ continue
+
+ # Headings for every formset (the amount depends on `extra`).
+ for y, inline_admin_form in enumerate(inline_admin_formset):
+ y_plus_one = y + 1
+ form_heading = (
+ f'<h3><b>Photo:</b> <span class="inline_label">#{y_plus_one}</span>'
+ "</h3>"
+ )
+ self.assertContains(response, form_heading, html=True)
+
+ # Every fieldset defined for an inline's form.
+ for z, fieldset in enumerate(inline_admin_form):
+ if fieldset.name:
+ heading_id = f"{prefix}-{y}-details-{z}-heading"
+ self.assertContains(
+ response,
+ f'<fieldset class="module aligned {fieldset.classes}" '
+ f'aria-labelledby="{heading_id}">',
+ )
+ fieldset_heading = (
+ f'<h4 id="{heading_id}" class="fieldset-heading">'
+ f"Details</h4>"
+ )
+ self.assertContains(response, fieldset_heading)
+ self.assertContains(response, f'id="{heading_id}"', count=1)
+
+ else:
+ fieldset_html = (
+ f'<fieldset class="module aligned {fieldset.classes}">'
+ )
+ self.assertContains(response, fieldset_html)
+
+
+@override_settings(ROOT_URLCONF="admin_inlines.urls")
class SeleniumTests(AdminSeleniumTestCase):
available_apps = ["admin_inlines"] + AdminSeleniumTestCase.available_apps