fix(ui): align the right rail to where the left rail settles

The right rail took its top from the left stack's bounding rect while the
left stack's `top` was still transitioning (150 ms): the left layout sets
its target and calls onAligned() synchronously, the right pass runs a
frame later mid-transition, and nothing re-ran it when the transition
ended. The rail then sat ~32 px off and jumped on the next unrelated
relayout - under the pointer in CI's panel gates, which is why a header
press missed on every run.

settledLeftRailTop() reads the end value of a running `top` transition
(falling back to dataset.safeTopPct, then the rect). A settled stack is
read from its rect as before, which keeps Cyber's CSS top override (and
cockpit's transition: none) exactly as they were. The Cyber test's
pinned pattern follows the new helper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-06 16:54:06 +01:00
1 parent f17bf7116d
commit 9dde7b4a62
3 files changed
+85 -2

No files matched your search

+50
View File
@@ -357,6 +357,56 @@ test('right rail aligns to the current left rail and excludes hidden obstacles',
assert.equal(f.stack.dataset.safeTop, '200.0');
});
// The left pass commits its top and requests this pass in the same frame, but
// `top` then animates for 150 ms and nothing re-runs the right pass when it
// lands. CI docked the right rail at 234 px (26vh) while the left settled at
// 266.4 px (29.6vh). Mid-flight the right rail must read the destination.
function animatingLeftRail({ keyframes = true } = {}) {
const f = fixture('right');
const left = f.options.leftStack;
left.rect.top = 250;
left.computed.top = '250px';
left.dataset.safeTopPct = '29.60';
left.getAnimations = () => [
{ transitionProperty: 'bottom', effect: null },
{
transitionProperty: 'top',
effect: {
getKeyframes: () =>
keyframes ? [{ top: '234px' }, { top: '266.4px' }] : [],
},
},
];
return f;
}
test('right rail aligns to where a mid-transition left rail settles', () => {
const f = animatingLeftRail();
f.run();
assert.equal(f.stack.dataset.safeTop, '266.4');
assert.equal(
f.stack.style.getPropertyValue('--right-stack-safe-top'),
'266.4px',
);
});
test('right rail falls back to the committed left target when the transition end is unreadable', () => {
const f = animatingLeftRail({ keyframes: false });
f.run();
assert.equal(f.stack.dataset.safeTop, '266.4');
});
test('right rail follows a settled left rail whose CSS overrides the committed target', () => {
// Cyber pins the left rail with `top: ... !important`, so the committed
// percentage is not where it renders and no `top` transition runs.
const f = fixture('right', { hud: { visible: true, variant: 'cyber' } });
f.options.leftStack.rect.top = 220;
f.options.leftStack.dataset.safeTopPct = '26.00';
f.options.leftStack.getAnimations = () => [];
f.run();
assert.equal(f.stack.dataset.safeTop, '220.0');
});
test('natural height includes visible content, margins and wrapper chrome, excluding hidden rows', () => {
const panel = element('panel');
const inner = element('inner', { top: 100 });