import { describe, expect, it } from "vite-plus/test"; import { resolveContextStripLabelsCompact } from "./BranchToolbar.logic";import { COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX, COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX, COMPOSER_RESTING_EXPANSION_MIN_PX, getRestingComposerImagePreviewCounts, resolveComposerTimelineInset, resolveScrollToEndClearance, resolveRestingComposerControlsLayout, resolveRestingComposerControlsNaturalWidth, shouldAnimateComposerRestingTransition, shouldUseCompactComposerPrimaryActions, shouldUseCompactComposerFooter, shouldUseRestingComposerLayout,} from "./composerFooterLayout"; describe("getRestingComposerImagePreviewCounts", () => { it("shows at most three thumbnails and counts the remainder", () => { expect(getRestingComposerImagePreviewCounts(0)).toEqual({ visibleCount: 0, overflowCount: 0, }); expect(getRestingComposerImagePreviewCounts(3)).toEqual({ visibleCount: 3, overflowCount: 0, }); expect(getRestingComposerImagePreviewCounts(7)).toEqual({ visibleCount: 3, overflowCount: 4, }); });}); describe("shouldUseCompactComposerFooter", () => { it("stays expanded without a measured width", () => { expect(shouldUseCompactComposerFooter(null)).toBe(false); }); it("switches to compact mode below the breakpoint", () => { expect(shouldUseCompactComposerFooter(COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX - 1)).toBe(true); }); it("stays expanded at and above the breakpoint", () => { expect(shouldUseCompactComposerFooter(COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX)).toBe(false); expect(shouldUseCompactComposerFooter(COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX + 48)).toBe(false); }); it("uses a higher breakpoint for wide action states", () => { expect( shouldUseCompactComposerFooter(COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX - 1, { hasWideActions: true, }), ).toBe(true); expect( shouldUseCompactComposerFooter(COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX, { hasWideActions: true, }), ).toBe(false); });}); describe("shouldUseCompactComposerPrimaryActions", () => { it("matches the wide footer breakpoint", () => { expect( shouldUseCompactComposerPrimaryActions( COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX - 1, { hasWideActions: true }, ), ).toBe(true); expect( shouldUseCompactComposerPrimaryActions(COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX, { hasWideActions: true, }), ).toBe(false); });}); describe("resolveComposerTimelineInset", () => { it("follows the expanded overlay height", () => { expect( resolveComposerTimelineInset({ currentInset: 160, overlayHeight: 140, isResting: false }), ).toBe(140); }); it("keeps a larger expanded reservation while resting", () => { expect( resolveComposerTimelineInset({ currentInset: 200, overlayHeight: 60, isResting: true }), ).toBe(200); }); it("reserves the empty expansion when no larger height is known", () => { expect( resolveComposerTimelineInset({ currentInset: 0, overlayHeight: 60, isResting: true }), ).toBe(60 + COMPOSER_RESTING_EXPANSION_MIN_PX); });}); describe("shouldUseRestingComposerLayout", () => { const resting = { isExistingThread: true, isMobileViewport: false, isScrollCollapsed: true, hasExpandedChrome: false, hasMultilinePrompt: false, timelineOverflows: true, }; it("uses the resting layout after a timeline scroll", () => { expect(shouldUseRestingComposerLayout(resting)).toBe(true); }); it("keeps the composer expanded until the timeline is scrolled", () => { expect(shouldUseRestingComposerLayout({ ...resting, isScrollCollapsed: false })).toBe(false); }); it("keeps the composer expanded while the timeline fits above it", () => { expect(shouldUseRestingComposerLayout({ ...resting, timelineOverflows: false })).toBe(false); }); it("keeps new-thread composers expanded", () => { expect(shouldUseRestingComposerLayout({ ...resting, isExistingThread: false })).toBe(false); }); it("leaves responsive mobile on its existing collapse path", () => { expect(shouldUseRestingComposerLayout({ ...resting, isMobileViewport: true })).toBe(false); }); it("keeps drawers and composer-owned menus expanded", () => { expect(shouldUseRestingComposerLayout({ ...resting, hasExpandedChrome: true })).toBe(false); }); it.each([false, true])( "keeps multiline drafts expanded when scroll collapsed is %s", (isScrollCollapsed) => { expect( shouldUseRestingComposerLayout({ ...resting, hasMultilinePrompt: true, isScrollCollapsed, }), ).toBe(false); }, );}); describe("shouldAnimateComposerRestingTransition", () => { it("does not animate layout measurements that settle during initial mount", () => { expect( shouldAnimateComposerRestingTransition({ hasCompletedInitialLayout: false, stateChanged: true, hasInterruptedAnimation: false, }), ).toBe(false); }); it("animates later resting-state changes and interrupted transitions", () => { expect( shouldAnimateComposerRestingTransition({ hasCompletedInitialLayout: true, stateChanged: true, hasInterruptedAnimation: false, }), ).toBe(true); expect( shouldAnimateComposerRestingTransition({ hasCompletedInitialLayout: true, stateChanged: false, hasInterruptedAnimation: true, }), ).toBe(true); });}); describe("resolveRestingComposerControlsLayout", () => { // Picker 140 natural / 96 minimum, plus a 9px separator. Traits 60, // mode 140, overflow 24, gap 4. const base = { gap: 4, naturalFixedWidth: 149, minimumFixedWidth: 105, blockWidths: [60, 140], overflowWidth: 24, }; it("shows everything when the host has room", () => { // 149 + 60 + 140 + 4 * 2 = 357 expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 357 })).toEqual({ hiddenCount: 0, visible: true, }); }); it("moves trailing blocks into the overflow menu until the rest fits", () => { // 149 + 60 + 24 + 4 * 2 = 241 expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 356 })).toEqual({ hiddenCount: 1, visible: true, }); expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 241 })).toEqual({ hiddenCount: 1, visible: true, }); // 149 + 24 + 4 = 177 expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 240 })).toEqual({ hiddenCount: 2, visible: true, }); }); it("shrinks the picker after moving every trailing block into overflow", () => { expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 176 })).toEqual({ hiddenCount: 2, visible: true, }); // 105 + 24 + 4 = 133 expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 133 })).toEqual({ hiddenCount: 2, visible: true, }); }); it("hides the whole cluster below the picker's minimum readable width", () => { expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 132 })).toEqual({ hiddenCount: 2, visible: false, }); expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 0 })).toEqual({ hiddenCount: 2, visible: false, }); }); it("uses the same thresholds while shrinking and growing", () => { expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 240 })).toEqual({ hiddenCount: 2, visible: true, }); expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 241 })).toEqual({ hiddenCount: 1, visible: true, }); }); it("supports a single leading control without overflow blocks", () => { expect( resolveRestingComposerControlsLayout({ ...base, naturalFixedWidth: 140, minimumFixedWidth: 140, blockWidths: [], overflowWidth: 0, hostWidth: 139, }), ).toEqual({ hiddenCount: 0, visible: false }); });}); describe("context strip labels and resting composer controls", () => { // Widths captured from a desktop renderer that crashed with React error // 185. The strip is 724px wide. Its expanded labels need 327px, and the // rest of its chrome needs 125px. The composer controls sit in the host // that takes whatever is left. const stripWidth = 724; const labelWidth = 327; const chromeWidth = 125; const measurement = { gap: 4, naturalFixedWidth: 96.3828125 + 5 + 4, minimumFixedWidth: 52 + 5 + 4, blockWidths: [130.6953125, 119.671875], overflowWidth: 28, }; const naturalWidth = resolveRestingComposerControlsNaturalWidth(measurement); function hostWidth(compact: boolean): number { return stripWidth - chromeWidth - (compact ? 0 : labelWidth); } it("keeps the labels compact when the full controls only fit beside compact labels", () => { // Compact labels leave 599px, so the composer shows every block. const layout = resolveRestingComposerControlsLayout({ ...measurement, hostWidth: hostWidth(true), }); expect(layout).toEqual({ hiddenCount: 0, visible: true }); // The strip reserves the natural controls width, so expanding the // labels is off the table: 125 + 327 + 364 > 724. const compact = resolveContextStripLabelsCompact({ compact: true, neededWidth: chromeWidth + labelWidth + naturalWidth, availableWidth: stripWidth, }); expect(compact).toBe(true); // The next pass sees the same inputs and lands on the same answer. expect( resolveRestingComposerControlsLayout({ ...measurement, hostWidth: hostWidth(compact) }), ).toEqual(layout); }); it("does not settle when the strip only reserves the visible controls", () => { // Regression guard for the alternating layout. Reserving only the // controls left visible after two blocks moved into overflow makes the // strip expand its labels, which shrinks the host below what the full // controls need, which hides the blocks again. const hiddenControlsWidth = 137; const expands = !resolveContextStripLabelsCompact({ compact: true, neededWidth: chromeWidth + labelWidth + hiddenControlsWidth, availableWidth: stripWidth, }); expect(expands).toBe(true); expect( resolveRestingComposerControlsLayout({ ...measurement, hostWidth: hostWidth(false) }), ).toEqual({ hiddenCount: 2, visible: true }); });}); describe("resolveRestingComposerControlsLayout hysteresis", () => { // Same cluster as above: picker 140 natural / 96 minimum plus a 9px // separator, traits 60, mode 140, overflow 24, gap 4. const base = { gap: 4, naturalFixedWidth: 149, minimumFixedWidth: 105, blockWidths: [60, 140], overflowWidth: 24, }; it("keeps a block in overflow when re-showing it would leave no slack", () => { // 149 + 60 + 140 + 4 * 2 = 357 fills the host exactly. expect( resolveRestingComposerControlsLayout({ ...base, hostWidth: 357, previous: { hiddenCount: 1, visible: true }, }), ).toEqual({ hiddenCount: 1, visible: true }); }); it("re-shows a block once the host clears the slack margin", () => { expect( resolveRestingComposerControlsLayout({ ...base, hostWidth: 358, previous: { hiddenCount: 1, visible: true }, }), ).toEqual({ hiddenCount: 0, visible: true }); }); it("restores the blocks that fit when the full cluster has no slack", () => { const partlyRestored = resolveRestingComposerControlsLayout({ ...base, hostWidth: 357, previous: { hiddenCount: 2, visible: true }, }); expect(partlyRestored).toEqual({ hiddenCount: 1, visible: true }); expect( resolveRestingComposerControlsLayout({ ...base, hostWidth: 357, previous: partlyRestored }), ).toEqual(partlyRestored); expect( resolveRestingComposerControlsLayout({ ...base, hostWidth: 358, previous: partlyRestored }), ).toEqual({ hiddenCount: 0, visible: true }); }); it("requires slack before partially restoring a cluster", () => { // One inline block, the picker, and overflow need 149 + 60 + 24 + 8 = 241px. const previous = { hiddenCount: 2, visible: true }; expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 241, previous })).toEqual( previous, ); expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 242, previous })).toEqual({ hiddenCount: 1, visible: true, }); }); it("still resolves from scratch when there is no previous layout", () => { expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 357 })).toEqual({ hiddenCount: 0, visible: true, }); }); it("settles when the measured picker width jitters below a pixel", () => { // Regression guard for React error #185 ("Maximum update depth // exceeded"). The composer re-measures on every render, so the picker's // recovered natural width can land a fraction of a pixel apart between // renders. Without hysteresis that flips hiddenCount forever. let layout = { hiddenCount: 0, visible: true }; const settled: string[] = []; for (let index = 0; index < 10; index += 1) { layout = resolveRestingComposerControlsLayout({ ...base, // The picker's recovered natural width lands a half pixel apart // between renders. naturalFixedWidth: index % 2 === 0 ? 149 : 149.5, hostWidth: 357, previous: layout, }); if (index >= 2) settled.push(`${layout.hiddenCount}:${layout.visible}`); } expect(new Set(settled).size).toBe(1); }); it("keeps the cluster hidden until its minimum width clears the slack", () => { // 105 + 24 + 4 = 133 is the exact minimum for the hidden cluster. expect( resolveRestingComposerControlsLayout({ ...base, hostWidth: 133, previous: { hiddenCount: 2, visible: false }, }), ).toEqual({ hiddenCount: 2, visible: false }); expect( resolveRestingComposerControlsLayout({ ...base, hostWidth: 134, previous: { hiddenCount: 2, visible: false }, }), ).toEqual({ hiddenCount: 2, visible: true }); });}); describe("resolveScrollToEndClearance", () => { it("removes the side tab gap in both composer states while clearing overlapping attachments", () => { for (const overlayHeight of [120, 214]) { const layout = { overlayHeight, mainSurfaceTop: 534, button: { left: 340, right: 460 }, attachments: [{ top: 500, left: 600, right: 700 }], }; expect(resolveScrollToEndClearance(layout)).toBe(overlayHeight - 34); expect(resolveScrollToEndClearance({ ...layout, attachments: [] })).toBe(overlayHeight); expect( resolveScrollToEndClearance({ ...layout, attachments: [...layout.attachments, { top: 500, left: 100, right: 700 }], }), ).toBe(overlayHeight); expect(resolveScrollToEndClearance({ ...layout, button: { left: 590, right: 710 } })).toBe( overlayHeight, ); } });});
import { describe, expect, it } from "vite-plus/test";
import { resolveContextStripLabelsCompact } from "./BranchToolbar.logic";
import {
COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX,
COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX,
COMPOSER_RESTING_EXPANSION_MIN_PX,
getRestingComposerImagePreviewCounts,
resolveComposerTimelineInset,
resolveScrollToEndClearance,
resolveRestingComposerControlsLayout,
resolveRestingComposerControlsNaturalWidth,
shouldAnimateComposerRestingTransition,
shouldUseCompactComposerPrimaryActions,
shouldUseCompactComposerFooter,
shouldUseRestingComposerLayout,
} from "./composerFooterLayout";
describe("getRestingComposerImagePreviewCounts", () => {
it("shows at most three thumbnails and counts the remainder", () => {
expect(getRestingComposerImagePreviewCounts(0)).toEqual({
visibleCount: 0,
overflowCount: 0,
});
expect(getRestingComposerImagePreviewCounts(3)).toEqual({
visibleCount: 3,
overflowCount: 0,
});
expect(getRestingComposerImagePreviewCounts(7)).toEqual({
visibleCount: 3,
overflowCount: 4,
});
});
});
describe("shouldUseCompactComposerFooter", () => {
it("stays expanded without a measured width", () => {
expect(shouldUseCompactComposerFooter(null)).toBe(false);
});
it("switches to compact mode below the breakpoint", () => {
expect(shouldUseCompactComposerFooter(COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX - 1)).toBe(true);
});
it("stays expanded at and above the breakpoint", () => {
expect(shouldUseCompactComposerFooter(COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX)).toBe(false);
expect(shouldUseCompactComposerFooter(COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX + 48)).toBe(false);
});
it("uses a higher breakpoint for wide action states", () => {
expect(
shouldUseCompactComposerFooter(COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX - 1, {
hasWideActions: true,
}),
).toBe(true);
expect(
shouldUseCompactComposerFooter(COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX, {
hasWideActions: true,
}),
).toBe(false);
});
});
describe("shouldUseCompactComposerPrimaryActions", () => {
it("matches the wide footer breakpoint", () => {
expect(
shouldUseCompactComposerPrimaryActions(
COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX - 1,
{ hasWideActions: true },
),
).toBe(true);
expect(
shouldUseCompactComposerPrimaryActions(COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX, {
hasWideActions: true,
}),
).toBe(false);
});
});
describe("resolveComposerTimelineInset", () => {
it("follows the expanded overlay height", () => {
expect(
resolveComposerTimelineInset({ currentInset: 160, overlayHeight: 140, isResting: false }),
).toBe(140);
});
it("keeps a larger expanded reservation while resting", () => {
expect(
resolveComposerTimelineInset({ currentInset: 200, overlayHeight: 60, isResting: true }),
).toBe(200);
});
it("reserves the empty expansion when no larger height is known", () => {
expect(
resolveComposerTimelineInset({ currentInset: 0, overlayHeight: 60, isResting: true }),
).toBe(60 + COMPOSER_RESTING_EXPANSION_MIN_PX);
});
});
describe("shouldUseRestingComposerLayout", () => {
const resting = {
isExistingThread: true,
isMobileViewport: false,
isScrollCollapsed: true,
hasExpandedChrome: false,
hasMultilinePrompt: false,
timelineOverflows: true,
};
it("uses the resting layout after a timeline scroll", () => {
expect(shouldUseRestingComposerLayout(resting)).toBe(true);
});
it("keeps the composer expanded until the timeline is scrolled", () => {
expect(shouldUseRestingComposerLayout({ ...resting, isScrollCollapsed: false })).toBe(false);
});
it("keeps the composer expanded while the timeline fits above it", () => {
expect(shouldUseRestingComposerLayout({ ...resting, timelineOverflows: false })).toBe(false);
});
it("keeps new-thread composers expanded", () => {
expect(shouldUseRestingComposerLayout({ ...resting, isExistingThread: false })).toBe(false);
});
it("leaves responsive mobile on its existing collapse path", () => {
expect(shouldUseRestingComposerLayout({ ...resting, isMobileViewport: true })).toBe(false);
});
it("keeps drawers and composer-owned menus expanded", () => {
expect(shouldUseRestingComposerLayout({ ...resting, hasExpandedChrome: true })).toBe(false);
});
it.each([false, true])(
"keeps multiline drafts expanded when scroll collapsed is %s",
(isScrollCollapsed) => {
expect(
shouldUseRestingComposerLayout({
...resting,
hasMultilinePrompt: true,
isScrollCollapsed,
}),
).toBe(false);
},
);
});
describe("shouldAnimateComposerRestingTransition", () => {
it("does not animate layout measurements that settle during initial mount", () => {
expect(
shouldAnimateComposerRestingTransition({
hasCompletedInitialLayout: false,
stateChanged: true,
hasInterruptedAnimation: false,
}),
).toBe(false);
});
it("animates later resting-state changes and interrupted transitions", () => {
expect(
shouldAnimateComposerRestingTransition({
hasCompletedInitialLayout: true,
stateChanged: true,
hasInterruptedAnimation: false,
}),
).toBe(true);
expect(
shouldAnimateComposerRestingTransition({
hasCompletedInitialLayout: true,
stateChanged: false,
hasInterruptedAnimation: true,
}),
).toBe(true);
});
});
describe("resolveRestingComposerControlsLayout", () => {
// Picker 140 natural / 96 minimum, plus a 9px separator. Traits 60,
// mode 140, overflow 24, gap 4.
const base = {
gap: 4,
naturalFixedWidth: 149,
minimumFixedWidth: 105,
blockWidths: [60, 140],
overflowWidth: 24,
};
it("shows everything when the host has room", () => {
// 149 + 60 + 140 + 4 * 2 = 357
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 357 })).toEqual({
hiddenCount: 0,
visible: true,
});
});
it("moves trailing blocks into the overflow menu until the rest fits", () => {
// 149 + 60 + 24 + 4 * 2 = 241
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 356 })).toEqual({
hiddenCount: 1,
visible: true,
});
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 241 })).toEqual({
hiddenCount: 1,
visible: true,
});
// 149 + 24 + 4 = 177
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 240 })).toEqual({
hiddenCount: 2,
visible: true,
});
});
it("shrinks the picker after moving every trailing block into overflow", () => {
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 176 })).toEqual({
hiddenCount: 2,
visible: true,
});
// 105 + 24 + 4 = 133
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 133 })).toEqual({
hiddenCount: 2,
visible: true,
});
});
it("hides the whole cluster below the picker's minimum readable width", () => {
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 132 })).toEqual({
hiddenCount: 2,
visible: false,
});
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 0 })).toEqual({
hiddenCount: 2,
visible: false,
});
});
it("uses the same thresholds while shrinking and growing", () => {
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 240 })).toEqual({
hiddenCount: 2,
visible: true,
});
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 241 })).toEqual({
hiddenCount: 1,
visible: true,
});
});
it("supports a single leading control without overflow blocks", () => {
expect(
resolveRestingComposerControlsLayout({
...base,
naturalFixedWidth: 140,
minimumFixedWidth: 140,
blockWidths: [],
overflowWidth: 0,
hostWidth: 139,
}),
).toEqual({ hiddenCount: 0, visible: false });
});
});
describe("context strip labels and resting composer controls", () => {
// Widths captured from a desktop renderer that crashed with React error
// 185. The strip is 724px wide. Its expanded labels need 327px, and the
// rest of its chrome needs 125px. The composer controls sit in the host
// that takes whatever is left.
const stripWidth = 724;
const labelWidth = 327;
const chromeWidth = 125;
const measurement = {
gap: 4,
naturalFixedWidth: 96.3828125 + 5 + 4,
minimumFixedWidth: 52 + 5 + 4,
blockWidths: [130.6953125, 119.671875],
overflowWidth: 28,
};
const naturalWidth = resolveRestingComposerControlsNaturalWidth(measurement);
function hostWidth(compact: boolean): number {
return stripWidth - chromeWidth - (compact ? 0 : labelWidth);
}
it("keeps the labels compact when the full controls only fit beside compact labels", () => {
// Compact labels leave 599px, so the composer shows every block.
const layout = resolveRestingComposerControlsLayout({
...measurement,
hostWidth: hostWidth(true),
});
expect(layout).toEqual({ hiddenCount: 0, visible: true });
// The strip reserves the natural controls width, so expanding the
// labels is off the table: 125 + 327 + 364 > 724.
const compact = resolveContextStripLabelsCompact({
compact: true,
neededWidth: chromeWidth + labelWidth + naturalWidth,
availableWidth: stripWidth,
});
expect(compact).toBe(true);
// The next pass sees the same inputs and lands on the same answer.
expect(
resolveRestingComposerControlsLayout({ ...measurement, hostWidth: hostWidth(compact) }),
).toEqual(layout);
});
it("does not settle when the strip only reserves the visible controls", () => {
// Regression guard for the alternating layout. Reserving only the
// controls left visible after two blocks moved into overflow makes the
// strip expand its labels, which shrinks the host below what the full
// controls need, which hides the blocks again.
const hiddenControlsWidth = 137;
const expands = !resolveContextStripLabelsCompact({
compact: true,
neededWidth: chromeWidth + labelWidth + hiddenControlsWidth,
availableWidth: stripWidth,
});
expect(expands).toBe(true);
expect(
resolveRestingComposerControlsLayout({ ...measurement, hostWidth: hostWidth(false) }),
).toEqual({ hiddenCount: 2, visible: true });
});
});
describe("resolveRestingComposerControlsLayout hysteresis", () => {
// Same cluster as above: picker 140 natural / 96 minimum plus a 9px
// separator, traits 60, mode 140, overflow 24, gap 4.
const base = {
gap: 4,
naturalFixedWidth: 149,
minimumFixedWidth: 105,
blockWidths: [60, 140],
overflowWidth: 24,
};
it("keeps a block in overflow when re-showing it would leave no slack", () => {
// 149 + 60 + 140 + 4 * 2 = 357 fills the host exactly.
expect(
resolveRestingComposerControlsLayout({
...base,
hostWidth: 357,
previous: { hiddenCount: 1, visible: true },
}),
).toEqual({ hiddenCount: 1, visible: true });
});
it("re-shows a block once the host clears the slack margin", () => {
expect(
resolveRestingComposerControlsLayout({
...base,
hostWidth: 358,
previous: { hiddenCount: 1, visible: true },
}),
).toEqual({ hiddenCount: 0, visible: true });
});
it("restores the blocks that fit when the full cluster has no slack", () => {
const partlyRestored = resolveRestingComposerControlsLayout({
...base,
hostWidth: 357,
previous: { hiddenCount: 2, visible: true },
});
expect(partlyRestored).toEqual({ hiddenCount: 1, visible: true });
expect(
resolveRestingComposerControlsLayout({ ...base, hostWidth: 357, previous: partlyRestored }),
).toEqual(partlyRestored);
expect(
resolveRestingComposerControlsLayout({ ...base, hostWidth: 358, previous: partlyRestored }),
).toEqual({ hiddenCount: 0, visible: true });
});
it("requires slack before partially restoring a cluster", () => {
// One inline block, the picker, and overflow need 149 + 60 + 24 + 8 = 241px.
const previous = { hiddenCount: 2, visible: true };
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 241, previous })).toEqual(
previous,
);
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 242, previous })).toEqual({
hiddenCount: 1,
visible: true,
});
});
it("still resolves from scratch when there is no previous layout", () => {
expect(resolveRestingComposerControlsLayout({ ...base, hostWidth: 357 })).toEqual({
hiddenCount: 0,
visible: true,
});
});
it("settles when the measured picker width jitters below a pixel", () => {
// Regression guard for React error #185 ("Maximum update depth
// exceeded"). The composer re-measures on every render, so the picker's
// recovered natural width can land a fraction of a pixel apart between
// renders. Without hysteresis that flips hiddenCount forever.
let layout = { hiddenCount: 0, visible: true };
const settled: string[] = [];
for (let index = 0; index < 10; index += 1) {
layout = resolveRestingComposerControlsLayout({
...base,
// The picker's recovered natural width lands a half pixel apart
// between renders.
naturalFixedWidth: index % 2 === 0 ? 149 : 149.5,
hostWidth: 357,
previous: layout,
});
if (index >= 2) settled.push(`${layout.hiddenCount}:${layout.visible}`);
}
expect(new Set(settled).size).toBe(1);
});
it("keeps the cluster hidden until its minimum width clears the slack", () => {
// 105 + 24 + 4 = 133 is the exact minimum for the hidden cluster.
expect(
resolveRestingComposerControlsLayout({
...base,
hostWidth: 133,
previous: { hiddenCount: 2, visible: false },
}),
).toEqual({ hiddenCount: 2, visible: false });
expect(
resolveRestingComposerControlsLayout({
...base,
hostWidth: 134,
previous: { hiddenCount: 2, visible: false },
}),
).toEqual({ hiddenCount: 2, visible: true });
});
});
describe("resolveScrollToEndClearance", () => {
it("removes the side tab gap in both composer states while clearing overlapping attachments", () => {
for (const overlayHeight of [120, 214]) {
const layout = {
overlayHeight,
mainSurfaceTop: 534,
button: { left: 340, right: 460 },
attachments: [{ top: 500, left: 600, right: 700 }],
};
expect(resolveScrollToEndClearance(layout)).toBe(overlayHeight - 34);
expect(resolveScrollToEndClearance({ ...layout, attachments: [] })).toBe(overlayHeight);
expect(
resolveScrollToEndClearance({
...layout,
attachments: [...layout.attachments, { top: 500, left: 100, right: 700 }],
}),
).toBe(overlayHeight);
expect(resolveScrollToEndClearance({ ...layout, button: { left: 590, right: 710 } })).toBe(
overlayHeight,
);
}
});
});