Files
LangBot/web/tests/unit/assistant-dock-rail.test.mjs
T
TyperBody e6e1958fc5 fix(web): let the docked assistant button collapse again
`shouldCollapseRail` was keyed on `!railExpanded`, mirroring the expand
helper instead of negating it. Once a hover revealed the button it could
never collapse back into the rail, so the collapse looked broken after the
first restore.

Key the predicate on `railExpanded` being true, restore the mirror
relationship with `shouldExpandRail`, and add regression coverage for the
expand -> leave -> collapse round trip. Also lengthen the rail strip.
2026-09-25 13:22:18 +08:00

152 lines
5.0 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
import ts from 'typescript';
// The floating assistant button docks against a viewport edge, collapses into a
// short blue rail, and re-expands on hover. The transition rules are pure so
// they can be pinned here: the race this guards against is a button that drops
// under the cursor and never becomes visible again.
const sourcePath = new URL(
'../../src/app/home/components/assistant-dock.ts',
import.meta.url,
);
assert.ok(fs.existsSync(sourcePath), 'Missing assistant dock module');
const compiled = ts.transpileModule(fs.readFileSync(sourcePath, 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS },
}).outputText;
const loaded = { exports: {} };
new Function('require', 'module', 'exports', compiled)(
() => {
throw new Error('assistant-dock must stay dependency free');
},
loaded,
loaded.exports,
);
const {
ASSISTANT_BUTTON_SIZE,
ASSISTANT_SNAP_THRESHOLD,
ASSISTANT_RAIL_WIDTH,
clampAssistantPosition,
resolveAssistantEdge,
shouldCollapseRail,
shouldExpandRail,
restingRailExpanded,
} = loaded.exports;
const viewport = { width: 1280, height: 800 };
const maxX = viewport.width - ASSISTANT_BUTTON_SIZE;
test('clamping keeps the button fully inside the viewport', () => {
assert.deepEqual(clampAssistantPosition(-50, -50, viewport), { x: 0, y: 0 });
assert.deepEqual(clampAssistantPosition(9999, 9999, viewport), {
x: maxX,
y: viewport.height - ASSISTANT_BUTTON_SIZE,
});
assert.deepEqual(clampAssistantPosition(400, 300, viewport), {
x: 400,
y: 300,
});
});
test('a narrow viewport never produces a negative travel range', () => {
assert.deepEqual(
clampAssistantPosition(10, 10, { width: 20, height: 20 }),
{ x: 0, y: 0 },
);
});
test('positions within the snap threshold dock to the nearest edge', () => {
assert.equal(resolveAssistantEdge(0, viewport), 'left');
assert.equal(resolveAssistantEdge(ASSISTANT_SNAP_THRESHOLD, viewport), 'left');
assert.equal(resolveAssistantEdge(maxX, viewport), 'right');
assert.equal(
resolveAssistantEdge(maxX - ASSISTANT_SNAP_THRESHOLD, viewport),
'right',
);
});
test('a centred position stays free floating', () => {
assert.equal(resolveAssistantEdge(Math.round(maxX / 2), viewport), null);
assert.equal(
resolveAssistantEdge(ASSISTANT_SNAP_THRESHOLD + 1, viewport),
null,
);
});
test('the left edge wins ties so a centred drop is deterministic', () => {
// A viewport sized so both gaps are inside the snap threshold at x = 0.
const tight = { width: ASSISTANT_BUTTON_SIZE, height: 400 };
assert.equal(resolveAssistantEdge(0, tight), 'left');
});
test('hovering a docked rail expands it, hovering a free button does not', () => {
assert.equal(
shouldExpandRail({ dockedEdge: 'right', railExpanded: false, dragging: false }),
true,
);
assert.equal(
shouldExpandRail({ dockedEdge: null, railExpanded: false, dragging: false }),
false,
);
// Already expanded: nothing to do, so no redundant state churn on every move.
assert.equal(
shouldExpandRail({ dockedEdge: 'left', railExpanded: true, dragging: false }),
false,
);
});
test('a drag in progress never expands or collapses the rail', () => {
const dragging = { dockedEdge: 'left', railExpanded: false, dragging: true };
assert.equal(shouldExpandRail(dragging), false);
assert.equal(shouldCollapseRail(dragging), false);
});
test('only an *expanded* docked button collapses when the pointer leaves', () => {
// Regression: keying this on `!railExpanded` made a revealed button
// impossible to collapse again, so it stayed open forever after one hover.
assert.equal(
shouldCollapseRail({ dockedEdge: 'left', railExpanded: true, dragging: false }),
true,
);
assert.equal(
shouldCollapseRail({ dockedEdge: 'left', railExpanded: false, dragging: false }),
false,
);
assert.equal(
shouldCollapseRail({ dockedEdge: null, railExpanded: true, dragging: false }),
false,
);
});
test('expand then leave round-trips back to the collapsed rail', () => {
const docked = { dockedEdge: 'right', railExpanded: false, dragging: false };
// Hover: reveal the button.
assert.equal(shouldExpandRail(docked), true);
// Pointer leaves: the revealed button must collapse again.
assert.equal(
shouldCollapseRail({ ...docked, railExpanded: true }),
true,
);
// A second hover re-reveals it, so the cycle is repeatable.
assert.equal(shouldExpandRail(docked), true);
});
test('dropping always starts collapsed so the rail is never stuck open', () => {
// The pointer is still over the button on drop; the next pointermove expands.
assert.equal(restingRailExpanded(), false);
assert.equal(
shouldExpandRail({
dockedEdge: 'right',
railExpanded: restingRailExpanded(),
dragging: false,
}),
true,
);
});
test('the rail strip stays thinner than the button it replaces', () => {
assert.ok(ASSISTANT_RAIL_WIDTH > 0);
assert.ok(ASSISTANT_RAIL_WIDTH < ASSISTANT_BUTTON_SIZE);
});