agent-ecosystem/test/renderer/features/agent-graph/activityLane.test.ts

148 lines
4.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
ACTIVITY_LANE,
findActivityItemAt,
getActivityAnchorScreenPlacement,
getActivityAnchorTarget,
getActivityLaneBounds,
packActivityLaneScreenRects,
packActivityLaneWorldRects,
getVisibleActivityWindow,
} from '../../../../packages/agent-graph/src/layout/activityLane';
import type { GraphActivityItem, GraphNode } from '@claude-teams/agent-graph';
function createItems(count: number): GraphActivityItem[] {
return Array.from({ length: count }, (_, index) => ({
id: `item-${index + 1}`,
kind: 'inbox_message',
timestamp: `2026-04-13T12:00:0${index}Z`,
title: `Item ${index + 1}`,
}));
}
describe('activity lane helpers', () => {
it('keeps the newest visible window in newest-first order', () => {
const window = getVisibleActivityWindow(createItems(6));
expect(window.items.map((item) => item.id)).toEqual(['item-1', 'item-2', 'item-3']);
expect(window.overflowCount).toBe(3);
});
it('places activity lanes above their owners', () => {
const leadTarget = getActivityAnchorTarget({ nodeX: 100, nodeY: 80, nodeKind: 'lead' });
const memberTarget = getActivityAnchorTarget({ nodeX: 100, nodeY: 80, nodeKind: 'member' });
const memberLeftOfLeadTarget = getActivityAnchorTarget({
nodeX: 80,
nodeY: 80,
nodeKind: 'member',
leadX: 100,
});
expect(leadTarget.x).toBe(100 - ACTIVITY_LANE.width / 2);
expect(memberTarget.x).toBe(100 - ACTIVITY_LANE.width / 2);
expect(memberLeftOfLeadTarget.x).toBe(80 - ACTIVITY_LANE.width / 2);
expect(leadTarget.y).toBeLessThan(80);
expect(memberTarget.y).toBeLessThan(80);
});
it('keeps the activity lane fully above the owner node', () => {
const ownerY = 120;
const memberTarget = getActivityAnchorTarget({ nodeX: 100, nodeY: ownerY, nodeKind: 'member' });
const bounds = getActivityLaneBounds(memberTarget.x, memberTarget.y);
expect(bounds.bottom).toBeLessThan(ownerY);
});
it('hits visible activity pills in the owner lane', () => {
const node: GraphNode = {
id: 'member:team:alice',
kind: 'member',
label: 'alice',
state: 'active',
x: 100,
y: 80,
activityItems: createItems(3),
domainRef: { kind: 'member', teamName: 'team', memberName: 'alice' },
};
const anchor = getActivityAnchorTarget({ nodeX: 100, nodeY: 80, nodeKind: 'member' });
const bounds = getActivityLaneBounds(anchor.x, anchor.y);
const hit = findActivityItemAt(
bounds.left + ACTIVITY_LANE.width / 2,
bounds.top + ACTIVITY_LANE.headerHeight + ACTIVITY_LANE.itemHeight / 2,
[node]
);
expect(hit?.ownerNodeId).toBe(node.id);
expect(hit?.item.id).toBe('item-1');
});
it('keeps activity lane at its world-space position instead of clamping to the viewport', () => {
const placement = getActivityAnchorScreenPlacement({
anchorX: 40,
anchorY: 60,
cameraX: 0,
cameraY: 0,
zoom: 1,
viewportWidth: 800,
viewportHeight: 600,
});
expect(placement.x).toBe(40);
expect(placement.y).toBe(60);
expect(placement.visible).toBe(true);
});
it('stays visible when only part of the lane is inside the viewport', () => {
const placement = getActivityAnchorScreenPlacement({
anchorX: -40,
anchorY: 40,
cameraX: 0,
cameraY: 0,
zoom: 1,
viewportWidth: 800,
viewportHeight: 600,
});
expect(placement.x).toBeLessThan(0);
expect(placement.visible).toBe(true);
});
it('packs overlapping lanes on the same side without moving independent lanes', () => {
const placements = packActivityLaneScreenRects([
{ id: 'lane-a', side: 'right', x: 400, y: 100, width: 296, height: 220 },
{ id: 'lane-b', side: 'right', x: 420, y: 150, width: 296, height: 220 },
{ id: 'lane-c', side: 'left', x: 120, y: 150, width: 296, height: 220 },
]);
expect(placements.get('lane-a')).toEqual({ x: 400, y: 100 });
expect(placements.get('lane-b')).toEqual({ x: 420, y: 328 });
expect(placements.get('lane-c')).toEqual({ x: 120, y: 150 });
});
it('packs world lanes globally even when they came from different legacy sides', () => {
const placements = packActivityLaneWorldRects([
{ id: 'lane-a', side: 'left', x: 100, y: 100, width: 296, height: 220 },
{ id: 'lane-b', side: 'right', x: 120, y: 140, width: 296, height: 220 },
]);
expect(placements.get('lane-a')).toEqual({ x: 100, y: 100 });
expect(placements.get('lane-b')).toEqual({ x: 120, y: 328 });
});
it('tracks graph zoom so activity lanes behave like world elements', () => {
const placement = getActivityAnchorScreenPlacement({
anchorX: 40,
anchorY: 60,
cameraX: 0,
cameraY: 0,
zoom: 4,
viewportWidth: 800,
viewportHeight: 600,
});
expect(placement.scale).toBe(4);
});
});