mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge upstream/main into pr/panel-resize
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
commit
11603729dc
234 files changed
+45360
-944
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { readStylesheet } from '../testSupport/readStylesheet.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
@@ -214,6 +215,34 @@ test('right layout uses keyboard focus when there is no preferred panel', () =>
|
||||
assert.equal(f.second.classList.contains('collapsed'), false);
|
||||
});
|
||||
|
||||
for (const collapsed of [true, false]) {
|
||||
test(`right layout ignores a hidden ${collapsed ? 'collapsed' : 'expanded'} panel`, () => {
|
||||
const baseline = fixture('right');
|
||||
baseline.stack.children = [baseline.first];
|
||||
baseline.expand(baseline.first, 600);
|
||||
baseline.run();
|
||||
|
||||
const f = fixture('right');
|
||||
f.expand(f.first, 600);
|
||||
f.second.id = 'weather-panel';
|
||||
f.second.hidden = true;
|
||||
if (!collapsed) f.expand(f.second, 900);
|
||||
f.options.preferredPanelId = 'weather-panel';
|
||||
f.run();
|
||||
|
||||
assert.deepEqual(f.stack.dataset, baseline.stack.dataset);
|
||||
assert.equal(
|
||||
f.first.style.getPropertyValue('--right-panel-allocated-height'),
|
||||
baseline.first.style.getPropertyValue('--right-panel-allocated-height'),
|
||||
);
|
||||
assert.equal(f.second.classList.contains('collapsed'), collapsed);
|
||||
assert.deepEqual(f.second.writes, []);
|
||||
assert.deepEqual(f.collapsed, []);
|
||||
assert.equal(f.retries(), 0);
|
||||
assert.equal(f.stack.classList.contains('layout-focus'), false);
|
||||
});
|
||||
}
|
||||
|
||||
test('right layout retains Display allocation during measurement and caps restored scroll', () => {
|
||||
const f = fixture('right');
|
||||
f.first.id = 'pp-toggles';
|
||||
@@ -294,3 +323,326 @@ test('natural height includes visible content, margins and wrapper chrome, exclu
|
||||
100,
|
||||
);
|
||||
});
|
||||
|
||||
// Reproduce the allocation-dependent readings seen beside the weather card.
|
||||
// Outside measurement, CCTV is 391 px in focus and 629 px in normal mode;
|
||||
// Removing only the allocation leaves focus CSS active: its next reading is
|
||||
// 422 px, so the old decision exits focus and then re-enters on the 629 px read.
|
||||
function thrashingRightRail({ focused = false, weatherOpen = false } = {}) {
|
||||
const f = fixture('right');
|
||||
f.options.windowRef.innerHeight = 920;
|
||||
f.options.leftStack.rect.top = 439.6;
|
||||
f.stack.computed.rowGap = '8px';
|
||||
const display = element('pp-toggles', { collapsed: true });
|
||||
const cctv = element('cctv-panel', { height: 629 });
|
||||
const weather = element('weather-panel', {
|
||||
height: 444,
|
||||
collapsed: !weatherOpen,
|
||||
});
|
||||
const context = element('global-context-panel', { collapsed: true });
|
||||
if (!weatherOpen) weather.classList.add('layout-auto-collapsed');
|
||||
f.stack.children = [display, cctv, weather, context];
|
||||
f.stack.classList.toggle('layout-focus', focused);
|
||||
f.options.preferredPanelId = cctv.id;
|
||||
f.options.displayPanel = display;
|
||||
const allocation = '--right-panel-allocated-height';
|
||||
const measuring = () =>
|
||||
f.stack.getAttribute('data-rail-measuring') !== undefined;
|
||||
for (const panel of f.stack.children) {
|
||||
panel.parentElement = f.stack;
|
||||
panel.intrinsicHeight = panel.rect.height;
|
||||
panel.style.setProperty(allocation, '391px');
|
||||
panel.getBoundingClientRect = () => {
|
||||
let height;
|
||||
if (panel.classList.contains('collapsed')) {
|
||||
height =
|
||||
measuring() || f.stack.classList.contains('layout-focus') ? 44 : 0;
|
||||
} else if (measuring()) {
|
||||
height = panel.intrinsicHeight;
|
||||
} else if (f.stack.classList.contains('layout-focus')) {
|
||||
height = 391;
|
||||
} else {
|
||||
height = panel.intrinsicHeight;
|
||||
}
|
||||
return { ...panel.rect, height, bottom: panel.rect.top + height };
|
||||
};
|
||||
Object.defineProperty(panel, 'scrollHeight', {
|
||||
get: () => {
|
||||
if (measuring())
|
||||
return panel.classList.contains('collapsed')
|
||||
? 44
|
||||
: panel.intrinsicHeight;
|
||||
if (panel.classList.contains('collapsed'))
|
||||
return panel.getBoundingClientRect().height;
|
||||
return f.stack.classList.contains('layout-focus') &&
|
||||
panel.style.getPropertyValue(allocation)
|
||||
? 603
|
||||
: 422;
|
||||
},
|
||||
});
|
||||
}
|
||||
return { ...f, cctv, weather };
|
||||
}
|
||||
|
||||
for (const focused of [false, true]) {
|
||||
test(`right rail settles mode-dependent CCTV and weather heights within two passes from ${focused ? 'focus' : 'normal'}`, () => {
|
||||
const f = thrashingRightRail({ focused });
|
||||
assert.equal(f.cctv.getBoundingClientRect().height, focused ? 391 : 629);
|
||||
assert.equal(f.weather.getBoundingClientRect().height, focused ? 44 : 0);
|
||||
const modes = [];
|
||||
const needs = [];
|
||||
for (let pass = 0; pass < 10; pass++) {
|
||||
f.run();
|
||||
modes.push(f.stack.classList.contains('layout-focus'));
|
||||
needs.push(f.stack.dataset.requiredHeight);
|
||||
assert.equal(f.stack.dataset.availableHeight, '443.6');
|
||||
assert.equal(f.stack.getAttribute('data-rail-measuring'), undefined);
|
||||
}
|
||||
assert.deepEqual(modes.slice(1), Array(9).fill(true));
|
||||
assert.deepEqual(needs, Array(10).fill('681.0'));
|
||||
assert.equal(f.retries(), 0);
|
||||
assert.equal(f.cctv.writes.filter(([op]) => op === 'remove').length, 0);
|
||||
});
|
||||
}
|
||||
|
||||
test('right rail keeps focus through 30 px content growth inside the hysteresis band', () => {
|
||||
const f = thrashingRightRail();
|
||||
f.run();
|
||||
// Header + gap contribute 52 px: need moves 420 -> 450 -> 420.
|
||||
for (const height of [368, 398, 368, 398, 368]) {
|
||||
f.cctv.intrinsicHeight = height;
|
||||
f.run();
|
||||
assert.equal(f.stack.classList.contains('layout-focus'), true);
|
||||
}
|
||||
f.cctv.intrinsicHeight = 335; // Need 387 < 443.6 - 55.2.
|
||||
f.run();
|
||||
assert.equal(f.stack.classList.contains('layout-focus'), false);
|
||||
f.cctv.intrinsicHeight = 365;
|
||||
f.run();
|
||||
assert.equal(f.stack.classList.contains('layout-focus'), false);
|
||||
});
|
||||
|
||||
test('right rail enters focus only above available height and leaves below the full deadband', () => {
|
||||
const f = thrashingRightRail();
|
||||
f.options.windowRef.innerHeight = 1000;
|
||||
f.options.leftStack.rect.top = 500;
|
||||
f.cctv.intrinsicHeight = 408; // Need equals available (460 px).
|
||||
f.run();
|
||||
assert.equal(f.stack.classList.contains('layout-focus'), false);
|
||||
f.cctv.intrinsicHeight = 409;
|
||||
f.run();
|
||||
assert.equal(f.stack.classList.contains('layout-focus'), true);
|
||||
f.cctv.intrinsicHeight = 348; // Need equals the 400 px exit boundary.
|
||||
f.run();
|
||||
assert.equal(f.stack.classList.contains('layout-focus'), true);
|
||||
f.cctv.intrinsicHeight = 347;
|
||||
f.run();
|
||||
assert.equal(f.stack.classList.contains('layout-focus'), false);
|
||||
});
|
||||
|
||||
test('right rail auto-collapse requests only one retry and settles within two passes', () => {
|
||||
const f = thrashingRightRail({ weatherOpen: true });
|
||||
const modes = [];
|
||||
for (let pass = 0; pass < 10; pass++) {
|
||||
f.run();
|
||||
modes.push(f.stack.classList.contains('layout-focus'));
|
||||
}
|
||||
assert.deepEqual(f.collapsed, ['weather-panel']);
|
||||
assert.equal(f.retries(), 1);
|
||||
assert.deepEqual(modes.slice(1), Array(9).fill(true));
|
||||
});
|
||||
|
||||
test('right rail retry cannot enqueue another retry even if disclosure changes before it runs', () => {
|
||||
const f = thrashingRightRail({ weatherOpen: true });
|
||||
f.run();
|
||||
assert.equal(f.retries(), 1);
|
||||
f.weather.classList.remove('collapsed', 'layout-auto-collapsed');
|
||||
f.run();
|
||||
assert.equal(f.retries(), 1);
|
||||
assert.equal(f.stack.dataset.layoutMode, 'focus');
|
||||
});
|
||||
|
||||
test('right rail restores presentation even when intrinsic measurement throws', () => {
|
||||
const f = thrashingRightRail();
|
||||
f.cctv.getBoundingClientRect = () => {
|
||||
throw new Error('measurement failed');
|
||||
};
|
||||
assert.throws(() => f.run(), /measurement failed/);
|
||||
assert.equal(f.stack.getAttribute('data-rail-measuring'), undefined);
|
||||
});
|
||||
|
||||
test('right rail measuring pass keeps an opted-in scroller position', () => {
|
||||
const f = fixture('right');
|
||||
f.expand(f.first, 300);
|
||||
let scrollTop = 240;
|
||||
let writes = 0;
|
||||
let clamp = true;
|
||||
const body = {
|
||||
get scrollTop() {
|
||||
return scrollTop;
|
||||
},
|
||||
set scrollTop(value) {
|
||||
writes++;
|
||||
scrollTop = value;
|
||||
},
|
||||
};
|
||||
f.stack.querySelectorAll = (selector) =>
|
||||
selector === '[data-rail-scroller]' ? [body] : [];
|
||||
const setAttribute = f.stack.setAttribute;
|
||||
f.stack.setAttribute = (name, value) => {
|
||||
setAttribute(name, value);
|
||||
// The lifted max-height removes the overflow and clamps the offset.
|
||||
if (clamp && name === 'data-rail-measuring') scrollTop = 0;
|
||||
};
|
||||
f.run();
|
||||
assert.equal(scrollTop, 240);
|
||||
assert.equal(writes, 1);
|
||||
clamp = false;
|
||||
f.run();
|
||||
assert.equal(writes, 1, 'an unclamped offset is not rewritten');
|
||||
});
|
||||
|
||||
for (const variant of ['minimal', 'full']) {
|
||||
test(`right rail does not retry collapse that the ${variant} HUD immediately restores`, () => {
|
||||
const f = fixture('right', { hud: { visible: true, variant } });
|
||||
f.expand(f.first, 900);
|
||||
f.expand(f.second, 900);
|
||||
for (let pass = 0; pass < 10; pass++) f.run();
|
||||
assert.equal(f.retries(), 0);
|
||||
assert.equal(f.first.classList.contains('collapsed'), false);
|
||||
assert.equal(f.second.classList.contains('collapsed'), false);
|
||||
});
|
||||
}
|
||||
// ── Narrow-screen rail overflow pin ──────────────────────────────────────────
|
||||
// At ≤720px both panel stacks become scroll containers (overflow-y: auto),
|
||||
// which also makes their overflow-x compute to auto. Each panel's decorative
|
||||
// .panel-glow is absolutely positioned with a negative inset, so inside a
|
||||
// scroll container that overhang is no longer harmless paint: it becomes
|
||||
// 18–20px of scrollable overflow on both axes, drawing a horizontal scrollbar
|
||||
// band under the expanded CCTV/Context/Data panel plus a vertical scrollbar
|
||||
// that scrolls nothing but glow. The narrow block therefore pins every hosted
|
||||
// glow to its panel box.
|
||||
|
||||
/** Strip comments and return the bodies of every ≤720px media block. */
|
||||
function narrowScreenBlocks(css) {
|
||||
const source = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
const blocks = [];
|
||||
let from = 0;
|
||||
for (;;) {
|
||||
const start = source.indexOf('@media (max-width: 720px) {', from);
|
||||
if (start === -1) break;
|
||||
const open = source.indexOf('{', start);
|
||||
let depth = 0;
|
||||
let close = -1;
|
||||
for (let index = open; index < source.length; index += 1) {
|
||||
if (source[index] === '{') depth += 1;
|
||||
if (source[index] === '}' && (depth -= 1) === 0) {
|
||||
close = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert.notEqual(close, -1, 'unterminated narrow-screen media block');
|
||||
blocks.push(source.slice(open + 1, close));
|
||||
from = close + 1;
|
||||
}
|
||||
assert.ok(blocks.length, 'narrow-screen media block is missing');
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/** Split a flat (non-nested) rule list into [selectors, declarations] pairs. */
|
||||
function flatRules(block) {
|
||||
assert.doesNotMatch(block, /@media/, 'nested media queries are not modelled');
|
||||
return [...block.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map(
|
||||
([, selectors, declarations]) => [
|
||||
selectors.split(',').map((part) => part.trim().replace(/\s+/g, ' ')),
|
||||
declarations,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test('narrow-screen rails pin every hosted panel glow inside its panel box', () => {
|
||||
const css = readStylesheet(new URL('../../style.css', import.meta.url));
|
||||
for (const panel of [
|
||||
'data-panel',
|
||||
'scene-panel',
|
||||
'cctv-panel',
|
||||
'global-context-panel',
|
||||
]) {
|
||||
assert.match(
|
||||
css,
|
||||
new RegExp(`#${panel} \\.panel-glow \\{[^}]*\\binset: -\\d+px;`),
|
||||
`${panel} glow no longer overhangs its panel; revisit this pin`,
|
||||
);
|
||||
}
|
||||
const narrow = narrowScreenBlocks(css).flatMap(flatRules);
|
||||
const scrollingRails = [
|
||||
...new Set(
|
||||
narrow
|
||||
.filter(([, declarations]) =>
|
||||
/\boverflow(?:-y)?:\s*(?:auto|scroll)\b/.test(declarations),
|
||||
)
|
||||
.flatMap(([selectors]) => selectors)
|
||||
.filter((selector) =>
|
||||
/^#(?:left-panel-stack|right-context-rail)$/.test(selector),
|
||||
),
|
||||
),
|
||||
].sort();
|
||||
assert.deepEqual(scrollingRails, [
|
||||
'#left-panel-stack',
|
||||
'#right-context-rail',
|
||||
]);
|
||||
for (const rail of scrollingRails) {
|
||||
assert.ok(
|
||||
narrow.some(
|
||||
([selectors, declarations]) =>
|
||||
selectors.includes(`${rail} .panel-glow`) &&
|
||||
/(?:^|;)\s*inset:\s*0\s*;/.test(declarations),
|
||||
),
|
||||
`${rail} scrolls at ≤720px but does not pin its panel glows (inset: 0)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('right layout ignores a hidden panel: no lane, no gap, no auto-collapse', () => {
|
||||
const f = fixture('right');
|
||||
const imagery = element('recent-imagery-panel', { height: 0 });
|
||||
imagery.hidden = true;
|
||||
imagery.scrollHeight = 0;
|
||||
imagery.rect.height = 0;
|
||||
f.stack.children.push(imagery);
|
||||
imagery.parentElement = f.stack;
|
||||
// A tall panel expands into focus mode: the hidden sibling is not among the
|
||||
// later panels that focus mode collapses, and it counts for nothing.
|
||||
f.expand(f.first, 900);
|
||||
f.run();
|
||||
assert.equal(f.stack.dataset.layoutMode, 'focus');
|
||||
assert.equal(imagery.classList.contains('collapsed'), false);
|
||||
assert.equal(imagery.classList.contains('layout-auto-collapsed'), false);
|
||||
assert.equal(f.stack.dataset.expandedCount, '1');
|
||||
assert.equal(
|
||||
imagery.style.getPropertyValue('--right-panel-allocated-height'),
|
||||
'',
|
||||
);
|
||||
assert.equal(imagery.getAttribute('aria-hidden'), undefined);
|
||||
const alone = parseFloat(
|
||||
f.first.style.getPropertyValue('--right-panel-allocated-height'),
|
||||
);
|
||||
// Shown (and expanded) it joins the allocation like any other panel.
|
||||
imagery.hidden = false;
|
||||
imagery.scrollHeight = 300;
|
||||
imagery.rect.height = 300;
|
||||
const retriesBefore = f.retries();
|
||||
f.run();
|
||||
assert.ok(
|
||||
f.collapsed.includes('recent-imagery-panel'),
|
||||
'focus mode collapses the later panel and asks for another pass',
|
||||
);
|
||||
assert.equal(f.retries(), retriesBefore + 1);
|
||||
f.run();
|
||||
assert.equal(f.stack.dataset.expandedCount, '1');
|
||||
assert.ok(
|
||||
parseFloat(
|
||||
f.first.style.getPropertyValue('--right-panel-allocated-height'),
|
||||
) <= alone,
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user