Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add a regression test for the handle position reflow
The fixture serves its stylesheet from a deliberately slow view and
records, from an animation frame registered ahead of the link, whether
that sheet had applied. Firefox withholds animation frames while a
stylesheet is pending, so a first frame that sees unstyled content means
something forced layout and lifted rendering suppression early.

The toolbar has to be collapsed for hideToolbar() to reach
ensureHandleVisibility(), and localStorage is cleared around the test
because djdt.show takes precedence over SHOW_COLLAPSED and persists
across tests in the shared driver.

Reverting the requestAnimationFrame wrapper fails the test 10 times out
of 10, and restoring it passes 10 out of 10. The check is specific to
Firefox: Chrome does not lift render blocking on a forced layout, so the
unpatched code does not flash there.
  • Loading branch information
novucs committed Aug 7, 2026
commit 6378f613bc02f1bd2835a4e1542b9997cfd4c52e
14 changes: 14 additions & 0 deletions tests/templates/flash_of_unstyled_content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% extends "base.html" %}

{% block head %}
<script>
window.djdtStyledOnFirstFrame = null;
requestAnimationFrame(() => {
const styles = getComputedStyle(document.documentElement);
window.djdtStyledOnFirstFrame = Boolean(
styles.getPropertyValue("--delayed-stylesheet")
);
});
</script>
<link rel="stylesheet" href="{% url 'delayed-stylesheet' %}">
{% endblock head %}
23 changes: 23 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,29 @@ def test_theme_toggle(self):
self.assertEqual(toolbar.get_attribute("data-user-theme"), "auto")
self.assertEqual(toolbar.get_attribute("data-theme"), "light")

def reset_stored_toolbar_state(self):
self.get("/regular/basic/")
self.selenium.execute_script("localStorage.clear()")

@override_settings(DEBUG_TOOLBAR_CONFIG={"SHOW_COLLAPSED": True})
def test_collapsed_toolbar_does_not_flash_unstyled_content(self):
"""Firefox withholds animation frames until pending stylesheets load, so
a first frame that sees unstyled content means the toolbar forced layout
early and let an unstyled paint through."""
self.reset_stored_toolbar_state()
self.addCleanup(self.reset_stored_toolbar_state)

self.get("/flash_of_unstyled_content/")
self.wait.until(
lambda selenium: selenium.execute_script(
"return window.djdtStyledOnFirstFrame !== null"
)
)

self.assertTrue(
self.selenium.execute_script("return window.djdtStyledOnFirstFrame")
)

def test_async_sql_action(self):
self.get("/async_execute_sql/")
self.selenium.find_element(By.ID, "SQLPanel")
Expand Down
9 changes: 9 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@
path("json_view/", views.json_view),
path("redirect/", views.redirect_view),
path("ajax/", views.ajax_view),
path(
"flash_of_unstyled_content/",
views.flash_of_unstyled_content_view,
),
path(
"delayed_stylesheet.css",
views.delayed_stylesheet_view,
name="delayed-stylesheet",
),
path("login_without_redirect/", LoginView.as_view(redirect_field_name=None)),
path("csp_view/", views.csp_view),
path("admin/", admin.site.urls),
Expand Down
14 changes: 13 additions & 1 deletion tests/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import asyncio
import time

from asgiref.sync import sync_to_async
from django.contrib.auth.models import User
from django.core.cache import cache
from django.http import HttpResponseRedirect, JsonResponse
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render
from django.template.response import TemplateResponse
from django.views.decorators.cache import cache_page
Expand Down Expand Up @@ -135,3 +136,14 @@ def redirect_view(request):

def ajax_view(request):
return render(request, "ajax/ajax.html")


def flash_of_unstyled_content_view(request):
return render(request, "flash_of_unstyled_content.html")


def delayed_stylesheet_view(request):
# Holds the sheet pending long enough for the toolbar's async script to run first.
time.sleep(0.1)
css = "html { --delayed-stylesheet: applied; }"
return HttpResponse(css, content_type="text/css")