Files
Codeman/test/tab-overflow.test.ts
T

142 lines
5.5 KiB
TypeScript
Raw Normal View History

import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import vm from 'node:vm';
import { describe, expect, it } from 'vitest';
type ScrollInput = {
scrollLeft?: number;
clientWidth?: number;
scrollWidth?: number;
tabLeft?: number;
tabWidth?: number;
padding?: number;
};
function loadTabOverflowHelper() {
const context = vm.createContext({ window: {}, globalThis: {} });
const source = readFileSync(resolve(import.meta.dirname, '../src/web/public/constants.js'), 'utf8');
vm.runInContext(source, context, { filename: 'constants.js' });
return (
context.window as {
CodemanTabOverflow: {
shouldAutoWrapTabs: (input: unknown) => boolean;
computeTabScrollLeft: (input: ScrollInput) => number;
TAB_SCROLL_REVEAL_PX: number;
};
}
).CodemanTabOverflow;
}
describe('tab overflow layout policy', () => {
it('auto-wraps desktop tabs when their rendered width exceeds available tab space', () => {
const helper = loadTabOverflowHelper();
expect(
helper.shouldAutoWrapTabs({
deviceType: 'desktop',
manualTwoRows: false,
tabCount: 18,
scrollWidth: 1400,
clientWidth: 760,
})
).toBe(true);
});
it('does not auto-wrap when manual tall tabs are enabled or on mobile/tablet', () => {
const helper = loadTabOverflowHelper();
expect(
helper.shouldAutoWrapTabs({
deviceType: 'desktop',
manualTwoRows: true,
tabCount: 18,
scrollWidth: 1400,
clientWidth: 760,
})
).toBe(false);
expect(
helper.shouldAutoWrapTabs({
deviceType: 'mobile',
manualTwoRows: false,
tabCount: 18,
scrollWidth: 1400,
clientWidth: 320,
})
).toBe(false);
});
it('respects the boundary conditions (exact fit, +1 tolerance, and tabCount < 2)', () => {
const helper = loadTabOverflowHelper();
const base = { deviceType: 'desktop' as const, manualTwoRows: false, tabCount: 6 };
// Exact fit: no overflow, no wrap.
expect(helper.shouldAutoWrapTabs({ ...base, scrollWidth: 800, clientWidth: 800 })).toBe(false);
// Within the +1 sub-pixel tolerance: still no wrap.
expect(helper.shouldAutoWrapTabs({ ...base, scrollWidth: 801, clientWidth: 800 })).toBe(false);
// 2px over: wrap.
expect(helper.shouldAutoWrapTabs({ ...base, scrollWidth: 802, clientWidth: 800 })).toBe(true);
// A single overflowing tab must not wrap (need at least 2 to form a second row).
expect(helper.shouldAutoWrapTabs({ ...base, tabCount: 1, scrollWidth: 1400, clientWidth: 760 })).toBe(false);
});
});
// Issue #257: the phone tab strip scrolls horizontally, so the active tab can
// sit entirely outside the visible slice. These pin the scroll target math that
// _scrollActiveTabIntoView() feeds with measured rects.
describe('mobile tab strip scroll-into-view policy', () => {
// A 5-tab phone strip: 335px visible of 558px of tabs.
const strip = { clientWidth: 335, scrollWidth: 558 };
const pad = 16;
it('scrolls right to reveal a tab past the right edge, leaving the reveal sliver', () => {
const helper = loadTabOverflowHelper();
// Last tab: 458..558, strip parked at 0.
const target = helper.computeTabScrollLeft({ ...strip, scrollLeft: 0, tabLeft: 458, tabWidth: 100 });
// 558 + 16 - 335 = 239, clamped to the 223px maximum.
expect(target).toBe(223);
// The revealed tab is now inside the window.
expect(458).toBeGreaterThanOrEqual(target);
expect(558).toBeLessThanOrEqual(target + strip.clientWidth);
});
it('scrolls left to reveal a tab before the left edge', () => {
const helper = loadTabOverflowHelper();
// First tab: 0..150, strip scrolled to the end.
expect(helper.computeTabScrollLeft({ ...strip, scrollLeft: 223, tabLeft: 0, tabWidth: 150 })).toBe(0);
// A middle tab partially cut off on the left: reveal it with the sliver.
expect(helper.computeTabScrollLeft({ ...strip, scrollLeft: 223, tabLeft: 200, tabWidth: 100 })).toBe(200 - pad);
});
it('leaves an already-visible tab alone (callers skip the write)', () => {
const helper = loadTabOverflowHelper();
expect(helper.computeTabScrollLeft({ ...strip, scrollLeft: 100, tabLeft: 152, tabWidth: 100 })).toBe(100);
});
it('never scrolls a strip that fits, and never leaves the scrollable range', () => {
const helper = loadTabOverflowHelper();
// Everything fits: nothing to scroll, whatever the tab geometry says.
expect(
helper.computeTabScrollLeft({ clientWidth: 900, scrollWidth: 400, scrollLeft: 0, tabLeft: 300, tabWidth: 100 })
).toBe(0);
// Clamped at both ends.
const low = helper.computeTabScrollLeft({ ...strip, scrollLeft: 40, tabLeft: 4, tabWidth: 100 });
expect(low).toBe(0);
const high = helper.computeTabScrollLeft({ ...strip, scrollLeft: 0, tabLeft: 500, tabWidth: 58 });
expect(high).toBeLessThanOrEqual(strip.scrollWidth - strip.clientWidth);
});
it('aligns the start of a tab too wide to fit the window', () => {
const helper = loadTabOverflowHelper();
// 330px tab in a 335px window: no position shows it plus padding.
expect(
helper.computeTabScrollLeft({ clientWidth: 335, scrollWidth: 900, scrollLeft: 0, tabLeft: 400, tabWidth: 330 })
).toBe(400);
});
it('tolerates missing measurements instead of producing NaN', () => {
const helper = loadTabOverflowHelper();
expect(helper.computeTabScrollLeft({})).toBe(0);
expect(helper.computeTabScrollLeft(undefined as unknown as ScrollInput)).toBe(0);
expect(helper.TAB_SCROLL_REVEAL_PX).toBe(pad);
});
});