Skip to content

Commit 739d061

Browse files
committed
[Spec 650][Phase: activity-feed] Add tests for relativeDate and buildActivityFeed
1 parent 664c22e commit 739d061

2 files changed

Lines changed: 113 additions & 3 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Unit tests for activity feed logic — relativeDate and buildActivityFeed.
3+
*/
4+
import { describe, it, expect, vi, afterEach } from 'vitest';
5+
import { relativeDate, buildActivityFeed } from '../src/components/TeamView.js';
6+
import type { TeamApiMember } from '../src/lib/api.js';
7+
8+
function makeMember(github: string, data: TeamApiMember['github_data'] = null): TeamApiMember {
9+
return { name: github, github, role: 'developer', filePath: '', github_data: data };
10+
}
11+
12+
describe('relativeDate', () => {
13+
afterEach(() => { vi.useRealTimers(); });
14+
15+
it('returns "just now" for timestamps less than 1 hour ago', () => {
16+
vi.useFakeTimers();
17+
vi.setSystemTime(new Date('2026-04-01T12:00:00Z'));
18+
expect(relativeDate('2026-04-01T11:30:00Z')).toBe('just now');
19+
expect(relativeDate('2026-04-01T11:59:59Z')).toBe('just now');
20+
});
21+
22+
it('returns "Xh ago" for timestamps 1-23 hours ago', () => {
23+
vi.useFakeTimers();
24+
vi.setSystemTime(new Date('2026-04-01T12:00:00Z'));
25+
expect(relativeDate('2026-04-01T11:00:00Z')).toBe('1h ago');
26+
expect(relativeDate('2026-04-01T06:00:00Z')).toBe('6h ago');
27+
expect(relativeDate('2026-03-31T13:00:00Z')).toBe('23h ago');
28+
});
29+
30+
it('returns "Xd ago" for timestamps 24+ hours ago', () => {
31+
vi.useFakeTimers();
32+
vi.setSystemTime(new Date('2026-04-01T12:00:00Z'));
33+
expect(relativeDate('2026-03-31T12:00:00Z')).toBe('1d ago');
34+
expect(relativeDate('2026-03-25T12:00:00Z')).toBe('7d ago');
35+
});
36+
});
37+
38+
describe('buildActivityFeed', () => {
39+
it('returns empty array when no members have activity', () => {
40+
const members = [makeMember('alice', {
41+
assignedIssues: [], openPRs: [],
42+
recentActivity: { mergedPRs: [], closedIssues: [] },
43+
})];
44+
expect(buildActivityFeed(members)).toEqual([]);
45+
});
46+
47+
it('returns empty array when github_data is null', () => {
48+
expect(buildActivityFeed([makeMember('alice')])).toEqual([]);
49+
});
50+
51+
it('aggregates merged PRs and closed issues from multiple members', () => {
52+
const members = [
53+
makeMember('alice', {
54+
assignedIssues: [], openPRs: [],
55+
recentActivity: {
56+
mergedPRs: [{ number: 10, title: 'PR A', url: 'https://github.com/org/repo/pull/10', mergedAt: '2026-04-01T10:00:00Z' }],
57+
closedIssues: [],
58+
},
59+
}),
60+
makeMember('bob', {
61+
assignedIssues: [], openPRs: [],
62+
recentActivity: {
63+
mergedPRs: [],
64+
closedIssues: [{ number: 5, title: 'Issue B', url: 'https://github.com/org/repo/issues/5', closedAt: '2026-04-01T08:00:00Z' }],
65+
},
66+
}),
67+
];
68+
const entries = buildActivityFeed(members);
69+
expect(entries).toHaveLength(2);
70+
expect(entries[0].author).toBe('alice');
71+
expect(entries[0].type).toBe('merged');
72+
expect(entries[1].author).toBe('bob');
73+
expect(entries[1].type).toBe('closed');
74+
});
75+
76+
it('sorts entries reverse chronologically', () => {
77+
const members = [
78+
makeMember('alice', {
79+
assignedIssues: [], openPRs: [],
80+
recentActivity: {
81+
mergedPRs: [
82+
{ number: 1, title: 'Old', url: 'u1', mergedAt: '2026-03-30T10:00:00Z' },
83+
{ number: 2, title: 'New', url: 'u2', mergedAt: '2026-04-01T10:00:00Z' },
84+
],
85+
closedIssues: [
86+
{ number: 3, title: 'Mid', url: 'u3', closedAt: '2026-03-31T10:00:00Z' },
87+
],
88+
},
89+
}),
90+
];
91+
const entries = buildActivityFeed(members);
92+
expect(entries.map(e => e.number)).toEqual([2, 3, 1]);
93+
});
94+
95+
it('correctly attributes entries to their member', () => {
96+
const members = [
97+
makeMember('alice', {
98+
assignedIssues: [], openPRs: [],
99+
recentActivity: {
100+
mergedPRs: [{ number: 1, title: 'X', url: 'u', mergedAt: '2026-04-01T10:00:00Z' }],
101+
closedIssues: [],
102+
},
103+
}),
104+
];
105+
const entries = buildActivityFeed(members);
106+
expect(entries[0]).toMatchObject({
107+
type: 'merged', number: 1, title: 'X', url: 'u', author: 'alice',
108+
});
109+
});
110+
});

packages/codev/dashboard/src/components/TeamView.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function MessageItem({ message }: { message: TeamApiMessage }) {
9191
);
9292
}
9393

94-
interface ActivityEntry {
94+
export interface ActivityEntry {
9595
type: 'merged' | 'closed';
9696
number: number;
9797
title: string;
@@ -100,7 +100,7 @@ interface ActivityEntry {
100100
author: string;
101101
}
102102

103-
function relativeDate(isoString: string): string {
103+
export function relativeDate(isoString: string): string {
104104
const diff = Date.now() - new Date(isoString).getTime();
105105
const hours = Math.floor(diff / (1000 * 60 * 60));
106106
if (hours < 1) return 'just now';
@@ -109,7 +109,7 @@ function relativeDate(isoString: string): string {
109109
return `${days}d ago`;
110110
}
111111

112-
function buildActivityFeed(members: TeamApiMember[]): ActivityEntry[] {
112+
export function buildActivityFeed(members: TeamApiMember[]): ActivityEntry[] {
113113
const entries: ActivityEntry[] = [];
114114
for (const member of members) {
115115
const gh = member.github_data;

0 commit comments

Comments
 (0)