+ {railCollapsed && (
+
+ )}
) : (
{
+ 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('a docked, idle button is the only state that collapses', () => {
+ assert.equal(
+ shouldCollapseRail({ dockedEdge: 'left', railExpanded: false, dragging: false }),
+ true,
+ );
+ assert.equal(
+ shouldCollapseRail({ dockedEdge: null, railExpanded: false, dragging: false }),
+ false,
+ );
+});
+
+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);
+});