diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 97bb0b8f2..c4b1ce72d 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -808,5 +808,6 @@ "src/ui/imageryBoxTool.test.mjs", "src/ui/recentImagery.test.mjs", "src/layers/recentImagery/testDoubles.mjs", - "scripts/qa-recent-imagery.mjs" + "scripts/qa-recent-imagery.mjs", + "src/ui/railScrollers.test.mjs" ] diff --git a/src/ui/railScrollers.test.mjs b/src/ui/railScrollers.test.mjs new file mode 100644 index 000000000..cf549be35 --- /dev/null +++ b/src/ui/railScrollers.test.mjs @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8'); + +// The rail's measuring pass lifts these panel scrollers to their natural +// height, which clamps their scroll offset to zero. Only scrollers tagged +// `data-rail-scroller` get their offset restored afterwards, so every lifted +// scroller must carry the tag or a layout pass snaps it back to the top. +test('every scroller the rail measuring pass lifts restores its scroll offset', () => { + const css = read('./styles/layers.css'); + const templates = [ + read('./templates/context.html'), + read('./templates/layer-panels.html'), + ].join('\n'); + const lifted = [ + ...css.matchAll( + /#right-context-rail\[data-rail-measuring\]\s*>\s*\[data-panel-id\]:not\(\.collapsed\)\s*>\s*\.([a-z-]+)/g, + ), + ].map((match) => match[1]); + assert.ok(lifted.length > 0, 'measuring CSS lists lifted scrollers'); + for (const className of lifted) { + const tag = templates.match( + new RegExp(`
]*>`), + )?.[0]; + if (!tag) continue; + assert.match(tag, /data-rail-scroller/, `${className} restores its scroll`); + } +}); diff --git a/src/ui/templates/context.html b/src/ui/templates/context.html index 6ce5562a6..fbed0cf7c 100644 --- a/src/ui/templates/context.html +++ b/src/ui/templates/context.html @@ -2,7 +2,7 @@