152 lines
4.3 KiB
TypeScript
152 lines
4.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { TASK_PILL } from '../../../../packages/agent-graph/src/constants/canvas-constants';
|
|
import {
|
|
KanbanLayoutEngine,
|
|
getOwnerKanbanBaseX,
|
|
} from '../../../../packages/agent-graph/src/layout/kanbanLayout';
|
|
import {
|
|
ACTIVITY_LANE,
|
|
getActivityAnchorTarget,
|
|
getActivityLaneBounds,
|
|
} from '../../../../packages/agent-graph/src/layout/activityLane';
|
|
|
|
import type { GraphNode } from '@claude-teams/agent-graph';
|
|
|
|
function createMemberNode(id: string, x: number, y: number, memberName: string): GraphNode {
|
|
return {
|
|
id,
|
|
kind: 'member',
|
|
label: memberName,
|
|
state: 'active',
|
|
x,
|
|
y,
|
|
domainRef: { kind: 'member', teamName: 'team', memberName },
|
|
};
|
|
}
|
|
|
|
function createLeadNode(x: number, y: number): GraphNode {
|
|
return {
|
|
id: 'lead:team',
|
|
kind: 'lead',
|
|
label: 'team lead',
|
|
state: 'active',
|
|
x,
|
|
y,
|
|
domainRef: { kind: 'lead', teamName: 'team', memberName: 'lead' },
|
|
};
|
|
}
|
|
|
|
function createTaskNode(
|
|
id: string,
|
|
ownerId: string,
|
|
status: NonNullable<GraphNode['taskStatus']>
|
|
): GraphNode {
|
|
return {
|
|
id,
|
|
kind: 'task',
|
|
label: id,
|
|
state: 'active',
|
|
ownerId,
|
|
taskStatus: status,
|
|
reviewState: 'none',
|
|
domainRef: { kind: 'task', teamName: 'team', taskId: id },
|
|
};
|
|
}
|
|
|
|
describe('kanban layout activity-lane avoidance', () => {
|
|
it('anchors right-side member kanban columns to the left of the owner', () => {
|
|
const baseX = getOwnerKanbanBaseX({
|
|
ownerX: 220,
|
|
ownerKind: 'member',
|
|
activeColumnCount: 3,
|
|
columnWidth: 180,
|
|
leadX: 0,
|
|
});
|
|
|
|
expect(baseX).toBe(220 - 2 * 180);
|
|
});
|
|
|
|
it('anchors left-side member kanban columns to the right of the owner', () => {
|
|
const baseX = getOwnerKanbanBaseX({
|
|
ownerX: -220,
|
|
ownerKind: 'member',
|
|
activeColumnCount: 3,
|
|
columnWidth: 180,
|
|
leadX: 0,
|
|
});
|
|
|
|
expect(baseX).toBe(-220);
|
|
});
|
|
|
|
it('keeps member task pills below the reserved activity lane', () => {
|
|
const lead = createLeadNode(0, 0);
|
|
const member = createMemberNode('member:jack', 220, 40, 'jack');
|
|
const tasks = [
|
|
createTaskNode('task:todo', member.id, 'pending'),
|
|
createTaskNode('task:wip', member.id, 'in_progress'),
|
|
createTaskNode('task:done', member.id, 'completed'),
|
|
];
|
|
|
|
KanbanLayoutEngine.layout([lead, member, ...tasks]);
|
|
|
|
const anchor = getActivityAnchorTarget({
|
|
nodeX: member.x ?? 0,
|
|
nodeY: member.y ?? 0,
|
|
nodeKind: 'member',
|
|
leadX: lead.x ?? null,
|
|
});
|
|
const laneBounds = getActivityLaneBounds(anchor.x, anchor.y);
|
|
const topmostTaskEdge = Math.min(...tasks.map((task) => (task.y ?? 0) - TASK_PILL.height / 2));
|
|
|
|
expect(topmostTaskEdge).toBeGreaterThan(laneBounds.bottom);
|
|
});
|
|
|
|
it('keeps left-side member task pills below the reserved activity lane', () => {
|
|
const lead = createLeadNode(0, 0);
|
|
const member = createMemberNode('member:alice', -220, 40, 'alice');
|
|
const tasks = [
|
|
createTaskNode('task:todo', member.id, 'pending'),
|
|
createTaskNode('task:wip', member.id, 'in_progress'),
|
|
createTaskNode('task:done', member.id, 'completed'),
|
|
];
|
|
|
|
KanbanLayoutEngine.layout([lead, member, ...tasks]);
|
|
|
|
const anchor = getActivityAnchorTarget({
|
|
nodeX: member.x ?? 0,
|
|
nodeY: member.y ?? 0,
|
|
nodeKind: 'member',
|
|
leadX: lead.x ?? null,
|
|
});
|
|
const laneBounds = getActivityLaneBounds(anchor.x, anchor.y);
|
|
const topmostTaskEdge = Math.min(...tasks.map((task) => (task.y ?? 0) - TASK_PILL.height / 2));
|
|
|
|
expect(topmostTaskEdge).toBeGreaterThan(laneBounds.bottom);
|
|
});
|
|
|
|
it('pushes task zones below overlapping activity lanes from nearby owners', () => {
|
|
const lead = createLeadNode(0, 0);
|
|
const member = createMemberNode('member:alice', 120, 120, 'alice');
|
|
const tasks = [
|
|
createTaskNode('task:todo', member.id, 'pending'),
|
|
createTaskNode('task:wip', member.id, 'in_progress'),
|
|
];
|
|
|
|
const nearbyLane = {
|
|
ownerId: 'member:tom',
|
|
left: 20,
|
|
top: -120,
|
|
right: 20 + ACTIVITY_LANE.width,
|
|
bottom: 180,
|
|
};
|
|
|
|
KanbanLayoutEngine.layout([lead, member, ...tasks], {
|
|
activityLaneBounds: [nearbyLane],
|
|
});
|
|
|
|
const topmostTaskEdge = Math.min(...tasks.map((task) => (task.y ?? 0) - TASK_PILL.height / 2));
|
|
|
|
expect(topmostTaskEdge).toBeGreaterThan(nearbyLane.bottom);
|
|
});
|
|
});
|