New markup over an old stylesheet
Reported from a desktop browser: a stray close button beside the logo, badly drawn, and a page that would not scroll. None of it was in the code that was running -- it was the code the browser had not fetched. The worker caches /static/ under a cache named for the release while the files in it carried no version, and a page is fetched network-first. That only ever worked because the worker used to seize every open tab the instant it installed and wipe the old cache. 1.1.0 stopped it doing that, rightly -- it was swapping stylesheets out from under a streaming reply -- and a momentary mismatch became a permanent one: new markup over the previous release's CSS for as long as the old worker lived. `.sidebar__close` had no rule there, so `.btn--icon` made it inline-flex: visible everywhere, placed by nothing. Every /static/ URL carries the release now, written by `templating.asset` and precached by `sw.js:versioned` -- both halves, because caches.match compares the query too and precaching the bare path would cache entries nothing requests. Self-correcting: updating is enough. The header was also a brand with a button appended and margin-left:auto doing the placing, which holds exactly while that button is last. Two slots now: a brand that shrinks and truncates, and a rail on the trailing edge. Verified before changing anything: with the current stylesheet the button is display:none at 1280 and, with thirty chats and forty messages, both scrollers scroll. The first measurement said the thread did not -- that was scroll-behavior: smooth reporting where it started. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -264,3 +264,50 @@ def test_the_page_asks_for_the_whole_screen_and_then_pays_for_it(client, registe
|
||||
app = (STATIC_DIR / "css" / "app.css").read_text()
|
||||
assert "var(--safe-top)" in app
|
||||
assert "var(--safe-bottom)" in app
|
||||
|
||||
|
||||
# --- A release cannot be drawn with the previous release's stylesheet --------
|
||||
def test_every_static_asset_carries_the_release(client: TestClient, registered):
|
||||
"""The bug this is here to stop shipped in 1.1.0.
|
||||
|
||||
The worker caches `/static/...` under a cache named for the release, and a
|
||||
page is fetched network-first while its assets come from that cache -- so
|
||||
once the worker stopped claiming open tabs the instant it installed (which
|
||||
it had to, or it swaps stylesheets under somebody mid-reply), new HTML and
|
||||
old CSS were served together. What that looked like was a close button
|
||||
meant for a phone drawer appearing, unstyled, on every desktop.
|
||||
|
||||
A version in the URL settles it: the new HTML asks for something the old
|
||||
cache has never heard of.
|
||||
"""
|
||||
import re
|
||||
|
||||
for path in ("/chat", "/settings"):
|
||||
page = client.get(path).text
|
||||
bare = re.findall(r'(?:href|src)="(/static/[^"?]+)"', page)
|
||||
assert not bare, f"{path} loads unversioned assets: {bare[:5]}"
|
||||
|
||||
|
||||
def test_no_template_reaches_past_the_helper(client: TestClient):
|
||||
"""`url_for('static', ...)` produces a URL with no version in it, so one
|
||||
left behind is one asset that can still come from the wrong release."""
|
||||
from pathlib import Path
|
||||
|
||||
import lembas
|
||||
|
||||
root = Path(lembas.__file__).parent / "web/templates"
|
||||
offenders = [
|
||||
str(p.relative_to(root))
|
||||
for p in root.rglob("*.html")
|
||||
if "url_for('static'" in p.read_text(encoding="utf-8")
|
||||
]
|
||||
assert not offenders, f"still using url_for for static assets: {offenders}"
|
||||
|
||||
|
||||
def test_the_worker_precaches_what_a_page_will_ask_for():
|
||||
"""`caches.match` compares the whole URL. Precaching the bare path fills the
|
||||
cache with entries nothing requests, and every asset then goes to the
|
||||
network on every load while looking perfectly cached."""
|
||||
source = (STATIC_DIR / "js" / "sw.js").read_text()
|
||||
assert 'path + "?v=" + VERSION' in source
|
||||
assert "versioned(path)" in source
|
||||
|
||||
+6
-1
@@ -324,7 +324,12 @@ def test_no_page_loads_a_script_that_the_base_template_already_loads():
|
||||
from pathlib import Path
|
||||
|
||||
templates = Path(__file__).resolve().parents[1] / "src/lembas/web/templates"
|
||||
pattern = re.compile(r"path='js/([a-z_]+\.js)'")
|
||||
# Both spellings, because the way a static URL is written has changed once
|
||||
# already: `url_for('static', path='js/x.js')` became `asset('js/x.js')`
|
||||
# when assets started carrying the release. The assertion below that the set
|
||||
# is non-empty is what turned that rename into a loud failure rather than a
|
||||
# sweep that silently stopped sweeping -- keep it.
|
||||
pattern = re.compile(r"(?:path=|asset\()'js/([a-z_]+\.js)'")
|
||||
always = set(pattern.findall((templates / "base.html").read_text()))
|
||||
assert always, "base.html stopped loading any script; this test is now blind"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user