[fix](trx-frontend-http): measure whether the tab bar fits, and watch for it
CI / lint (push) Successful in 2m20s
CI / test (push) Successful in 8m25s
CI / frontend (push) Successful in 3m36s
CI / reuse (push) Successful in 3s

CI put the tab strip 9px into the controls at 1440px with no scaling at
all, on a bar that had every degradation step available to it and used
none of them.  It used none because nothing thought anything was wrong:
the fit test was an arithmetic estimate — identity + nav.scrollWidth +
actions.scrollWidth + a 48px allowance for the gaps — and on a platform
whose fonts run wider than the one it was written on, that allowance no
longer covered what it stands for.  An estimate that says "fits" stops
the ladder before its first rung.

It reads the geometry now: the controls have to stay inside the bar, and
no tab may reach them.  That is the same measurement the test makes, so
the two cannot disagree about any platform's metrics.  The tabs are the
subject rather than the nav's box because the nav shrinks below its
content — the box gets smaller while the tabs keep their width and slide
underneath the controls.

A second fault turned up while probing this: the strip only reflowed on
window resize.  The rig name arriving from the server, the style picker
filling in, a font swapping in wider metrics — each changes what fits
without touching the window, and the bar sat there as it was through all
of them.  A ResizeObserver on the bar and the controls covers those, and
document.fonts.ready covers the swap.

The guard sweeps text scales and adds a station name too long for the
bar, but it should be said plainly: it passes against the old code too.
Nothing here reproduces on this machine — a 4px viewport sweep from 1080
to 1500, three wide font stacks and scales from 1.0 to 3.0 all failed to
make the old estimate lie.  What is fixed is the mechanism that could.

Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
sjg
2026-08-04 20:32:38 +02:00
parent c527f20a88
commit 84bdf2593c
3 changed files with 113 additions and 51 deletions
@@ -275,37 +275,70 @@ try {
assert.ok(menu.height > 40 && menu.width > 80, `menu rendered ${menu.width}x${menu.height}`);
assert.ok(menu.onTop, "menu is painted underneath the page");
// Wider text than this machine renders. The checks above passed on macOS
// while CI, whose system font is wider, put the tab strip into the controls:
// the bar had run out of moves and the tabs kept their full width anyway.
// Every piece of text in the bar is scaled, not just the tabs — the identity
// block is what runs out of room first at 1100px, where the bookmark gutters
// make the card narrower than it is at 900px.
const scale = 1.9;
await page.addStyleTag({ content: `
// Wider text than this machine renders. This suite passed on macOS twice
// while CI, whose system font is wider, put the tab strip into the controls
// the second time by 9px at 1440px with no scaling at all, because the fit
// test was an arithmetic estimate whose fixed allowance for the gaps did not
// cover them at those metrics. A single scale factor cannot stand in for
// another platform's fonts, so sweep: somewhere in this range is whatever CI
// renders, and the strip has to hold at every step of it.
const applyTextScale = (scale) => page.addStyleTag({ content: `
.tab-bar .title { font-size: ${1.05 * scale}rem !important; }
.tab-bar .subtitle { font-size: ${0.78 * scale}rem !important; }
.tab-bar .tab { font-size: ${0.95 * scale}rem !important; }
.tab-bar select, .tab-bar button { font-size: ${0.95 * scale}rem !important; }
` });
for (const width of [1440, 1280, 1100, 900]) {
await page.setViewportSize({ width, height: 900 });
await page.waitForTimeout(250);
const crowded = await page.evaluate(() => {
const nav = document.querySelector(".tab-bar-nav");
const actions = document.querySelector(".top-bar-actions");
const tabs = [...nav.querySelectorAll(".tab")].filter((tab) => tab.offsetParent !== null);
return {
overlap: Math.round(Math.max(...tabs.map((tab) => tab.getBoundingClientRect().right))
- actions.getBoundingClientRect().left),
iconsOnly: nav.classList.contains("nav-icons-only"),
};
});
assert.ok(crowded.overlap <= 0,
`with wider text the tab strip overlaps the controls by ${crowded.overlap}px at ${width}px`);
assert.equal(crowded.iconsOnly, true, `the strip kept its labels at ${width}px with no room for them`);
const measureBar = () => page.evaluate(() => {
const nav = document.querySelector(".tab-bar-nav");
const actions = document.querySelector(".top-bar-actions");
const tabs = [...nav.querySelectorAll(".tab")].filter((tab) => tab.offsetParent !== null);
return {
overlap: Math.round(Math.max(...tabs.map((tab) => tab.getBoundingClientRect().right))
- actions.getBoundingClientRect().left),
iconsOnly: nav.classList.contains("nav-icons-only"),
};
});
for (const scale of [1.0, 1.15, 1.3, 1.5, 1.75, 2.0]) {
await applyTextScale(scale);
// 1100px is the narrowest bar in the app: the bookmark gutters take 9.5rem
// a side above that width, leaving less room than 900px has.
for (const width of [1440, 1280, 1100, 900]) {
await page.setViewportSize({ width, height: 900 });
await page.waitForTimeout(120);
const crowded = await measureBar();
assert.ok(crowded.overlap <= 0,
`at ${scale}x text the tab strip overlaps the controls by ${crowded.overlap}px at ${width}px`);
}
}
// A station name long enough that the bar cannot hold it, which is the rung
// below icons: the identity has to give, not the strip.
await page.evaluate(() => {
document.getElementById("rig-subtitle").textContent =
"Rig: Shack SDR — RTL-SDR v4 on the attic dipole, north-west";
});
await applyTextScale(1.6);
for (const width of [1440, 1100]) {
await page.setViewportSize({ width, height: 900 });
await page.waitForTimeout(200);
const crowded = await measureBar();
assert.ok(crowded.overlap <= 0,
`with a long station name the tab strip overlaps the controls by ${crowded.overlap}px at ${width}px`);
}
// ...and when the text changes under a bar that is not resized. The rig name
// arrives from the server, a web font swaps in: neither is a window resize,
// and the strip used to sit there as it was.
await page.setViewportSize({ width: 1440, height: 900 });
await page.waitForTimeout(200);
await applyTextScale(2.4);
await page.waitForTimeout(400);
const unresized = await measureBar();
assert.ok(unresized.overlap <= 0,
`text grew without a resize and the strip overlaps by ${unresized.overlap}px`);
assert.equal(unresized.iconsOnly, true, "the strip kept its labels with no room for them");
} finally {
await browser.close();
await fixture.close();