Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
18 changes: 10 additions & 8 deletions debug_toolbar/static/debug_toolbar/js/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,16 @@ const djdt = {
}
},
ensureHandleVisibility() {
const djDebug = getDebugElement();
const handle = djDebug.querySelector("#djDebugToolbarHandle");
// set handle position
const handleTop = Math.min(
localStorage.getItem("djdt.top") || 265,
globalThis.innerHeight - handle.offsetWidth
);
handle.style.top = `${handleTop}px`;
requestAnimationFrame(() => {
const djDebug = getDebugElement();
const handle = djDebug.querySelector("#djDebugToolbarHandle");
// set handle position
const handleTop = Math.min(
localStorage.getItem("djdt.top") || 265,
globalThis.innerHeight - handle.offsetWidth
);
handle.style.top = `${handleTop}px`;
});
},
hideToolbar() {
const djDebug = getDebugElement();
Expand Down
5 changes: 5 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ Pending
older versions of Django, the panel explains that upgrading is required.
* Fixed the Django version check in the SQL panel test suite for Django's
boolean parameter handling.
* Deferred the toolbar handle's position calculation to an animation frame,
so it no longer forces a synchronous layout during initialization. On
pages with a large amount of CSS this could briefly show the page before
its styles were applied.
* Restored the select and explain buttons for queries that run without
parameters.
* Fixed the error shown when panel content fails to load, which could not
find the toolbar window inside the shadow root.
* Stopped the history panel buttons from submitting their form when clicked
before the panel script has loaded, which navigated away from the page.


7.0.0 (2026-06-17)
------------------

Expand Down
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 @@ -1026,6 +1026,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")
Loading