mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-10 06:20:13 +00:00
fix: resolve test import paths and vi.mock hoisting issues
- Fix import paths (5 '../' → 4 '../' for src imports) - Fix test-utils import path (2 '../' → 3 '../') - Fix vi.mock hoisting by inlining mock function - Fix mockData scope in Privacy Mode test - Fix test assertions (getByRole → querySelector, getByText → getAllByText) - Add barrel export for getProjectDisplayName utility
This commit is contained in:
@@ -12,3 +12,4 @@ export { TokenBreakdownChart } from './token-breakdown-chart';
|
|||||||
export { UsageInsightsCard } from './usage-insights-card';
|
export { UsageInsightsCard } from './usage-insights-card';
|
||||||
export { UsageSummaryCards } from './usage-summary-cards';
|
export { UsageSummaryCards } from './usage-summary-cards';
|
||||||
export { UsageTrendChart } from './usage-trend-chart';
|
export { UsageTrendChart } from './usage-trend-chart';
|
||||||
|
export { getProjectDisplayName } from './project-name-utils';
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export function getProjectDisplayName(path: string): string {
|
|||||||
|
|
||||||
// Remove leading/trailing slashes and split into segments
|
// Remove leading/trailing slashes and split into segments
|
||||||
const cleanPath = path.replace(/^\/|\/$/g, '');
|
const cleanPath = path.replace(/^\/|\/$/g, '');
|
||||||
const segments = cleanPath.split('/').filter(segment => segment.length > 0);
|
const segments = cleanPath.split('/').filter((segment) => segment.length > 0);
|
||||||
|
|
||||||
// Return the last segment (leaf folder name)
|
// Return the last segment (leaf folder name)
|
||||||
return segments[segments.length - 1] || '';
|
return segments[segments.length - 1] || '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,6 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function formatCompact(num: number): string {
|
function formatCompact(num: number): string {
|
||||||
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)}B`;
|
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)}B`;
|
||||||
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
|
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
// Import the function from the utility
|
// Import the function from the utility
|
||||||
import { getProjectDisplayName } from '../../../../../src/components/analytics/project-name-utils';
|
import { getProjectDisplayName } from '../../../../src/components/analytics/project-name-utils';
|
||||||
|
|
||||||
describe('getProjectDisplayName', () => {
|
describe('getProjectDisplayName', () => {
|
||||||
describe('Simple project paths', () => {
|
describe('Simple project paths', () => {
|
||||||
@@ -24,13 +24,21 @@ describe('getProjectDisplayName', () => {
|
|||||||
|
|
||||||
describe('Complex project paths', () => {
|
describe('Complex project paths', () => {
|
||||||
it('returns leaf folder for worktree paths', () => {
|
it('returns leaf folder for worktree paths', () => {
|
||||||
expect(getProjectDisplayName('/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08')).toBe('2026-01-08');
|
expect(
|
||||||
expect(getProjectDisplayName('/home/user/workspaces/repo-name/worktrees/feature-branch')).toBe('feature-branch');
|
getProjectDisplayName(
|
||||||
|
'/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08'
|
||||||
|
)
|
||||||
|
).toBe('2026-01-08');
|
||||||
|
expect(
|
||||||
|
getProjectDisplayName('/home/user/workspaces/repo-name/worktrees/feature-branch')
|
||||||
|
).toBe('feature-branch');
|
||||||
expect(getProjectDisplayName('/project/repo/worktrees/v2.0')).toBe('v2.0');
|
expect(getProjectDisplayName('/project/repo/worktrees/v2.0')).toBe('v2.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles nested paths', () => {
|
it('handles nested paths', () => {
|
||||||
expect(getProjectDisplayName('/home/user/projects/web-dashboard/src/components')).toBe('components');
|
expect(getProjectDisplayName('/home/user/projects/web-dashboard/src/components')).toBe(
|
||||||
|
'components'
|
||||||
|
);
|
||||||
expect(getProjectDisplayName('/opt/apps/my-app/lib/utils')).toBe('utils');
|
expect(getProjectDisplayName('/opt/apps/my-app/lib/utils')).toBe('utils');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -72,7 +80,8 @@ describe('getProjectDisplayName', () => {
|
|||||||
it('displays correct project name for worktree project', () => {
|
it('displays correct project name for worktree project', () => {
|
||||||
// Before fix: would show "08"
|
// Before fix: would show "08"
|
||||||
// After fix: should show "2026-01-08"
|
// After fix: should show "2026-01-08"
|
||||||
const path = '/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect.worktrees/2026-01-08';
|
const path =
|
||||||
|
'/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect.worktrees/2026-01-08';
|
||||||
expect(getProjectDisplayName(path)).toBe('2026-01-08');
|
expect(getProjectDisplayName(path)).toBe('2026-01-08');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,14 +106,9 @@ describe('getProjectDisplayName', () => {
|
|||||||
|
|
||||||
describe('Regression tests', () => {
|
describe('Regression tests', () => {
|
||||||
it('does not return empty string for valid paths', () => {
|
it('does not return empty string for valid paths', () => {
|
||||||
const testCases = [
|
const testCases = ['/project', '/home/user/app', '/var/log/nginx', '/tmp/test-file'];
|
||||||
'/project',
|
|
||||||
'/home/user/app',
|
|
||||||
'/var/log/nginx',
|
|
||||||
'/tmp/test-file',
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(path => {
|
testCases.forEach((path) => {
|
||||||
const result = getProjectDisplayName(path);
|
const result = getProjectDisplayName(path);
|
||||||
expect(result).not.toBe('');
|
expect(result).not.toBe('');
|
||||||
expect(result).not.toBeUndefined();
|
expect(result).not.toBeUndefined();
|
||||||
@@ -124,4 +128,4 @@ describe('getProjectDisplayName', () => {
|
|||||||
expect(getProjectDisplayName('/home/user/v1.2.3')).toBe('v1.2.3');
|
expect(getProjectDisplayName('/home/user/v1.2.3')).toBe('v1.2.3');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,17 +5,16 @@
|
|||||||
|
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { SessionStatsCard } from '../../../../../src/components/analytics/session-stats-card';
|
import { SessionStatsCard } from '../../../../src/components/analytics/session-stats-card';
|
||||||
import { AllProviders } from '../../../setup/test-utils';
|
import { AllProviders } from '../../../setup/test-utils';
|
||||||
import type { PaginatedSessions } from '../../../../../src/hooks/use-usage';
|
import type { PaginatedSessions } from '../../../../src/hooks/use-usage';
|
||||||
|
|
||||||
// Mock date-fns to return consistent dates
|
// Mock date-fns to return consistent dates
|
||||||
const mockFormatDistanceToNow = vi.fn();
|
|
||||||
vi.mock('date-fns', async () => {
|
vi.mock('date-fns', async () => {
|
||||||
const actual = await vi.importActual('date-fns');
|
const actual = await vi.importActual('date-fns');
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
formatDistanceToNow: mockFormatDistanceToNow,
|
formatDistanceToNow: vi.fn(() => '27 minutes ago'),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,17 +22,16 @@ describe('SessionStatsCard', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset all mocks
|
// Reset all mocks
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
// Mock formatDistanceToNow to return consistent values
|
|
||||||
mockFormatDistanceToNow.mockReturnValue('27 minutes ago');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Loading and Empty States', () => {
|
describe('Loading and Empty States', () => {
|
||||||
it('renders loading skeleton when isLoading is true', () => {
|
it('renders loading skeleton when isLoading is true', () => {
|
||||||
render(<SessionStatsCard data={undefined} isLoading={true} />, { wrapper: AllProviders });
|
const { container } = render(<SessionStatsCard data={undefined} isLoading={true} />, {
|
||||||
|
wrapper: AllProviders,
|
||||||
|
});
|
||||||
|
|
||||||
// Should have card structure with skeleton loading state
|
// Should have skeleton loading elements
|
||||||
expect(screen.getByRole('generic')).toBeInTheDocument();
|
expect(container.querySelector('[data-slot="skeleton"]')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows empty state when no data available', () => {
|
it('shows empty state when no data available', () => {
|
||||||
@@ -59,7 +57,12 @@ describe('SessionStatsCard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Session Stats Display', () => {
|
describe('Session Stats Display', () => {
|
||||||
const createMockSession = (projectPath: string, inputTokens: number, outputTokens: number, cost: number) => ({
|
const createMockSession = (
|
||||||
|
projectPath: string,
|
||||||
|
inputTokens: number,
|
||||||
|
outputTokens: number,
|
||||||
|
cost: number
|
||||||
|
) => ({
|
||||||
sessionId: `session-${Math.random()}`,
|
sessionId: `session-${Math.random()}`,
|
||||||
projectPath,
|
projectPath,
|
||||||
inputTokens,
|
inputTokens,
|
||||||
@@ -71,7 +74,12 @@ describe('SessionStatsCard', () => {
|
|||||||
const mockData: PaginatedSessions = {
|
const mockData: PaginatedSessions = {
|
||||||
sessions: [
|
sessions: [
|
||||||
createMockSession('/home/user/projects/my-app', 1500, 2500, 0.08),
|
createMockSession('/home/user/projects/my-app', 1500, 2500, 0.08),
|
||||||
createMockSession('/home/user/workspaces/repo-name/worktrees/feature-branch', 2000, 3000, 0.12),
|
createMockSession(
|
||||||
|
'/home/user/workspaces/repo-name/worktrees/feature-branch',
|
||||||
|
2000,
|
||||||
|
3000,
|
||||||
|
0.12
|
||||||
|
),
|
||||||
createMockSession('/Users/joe/Developer/share-pi', 1000, 2000, 0.05),
|
createMockSession('/Users/joe/Developer/share-pi', 1000, 2000, 0.05),
|
||||||
],
|
],
|
||||||
total: 3,
|
total: 3,
|
||||||
@@ -82,7 +90,12 @@ describe('SessionStatsCard', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockData.sessions = [
|
mockData.sessions = [
|
||||||
createMockSession('/home/user/projects/my-app', 1500, 2500, 0.08),
|
createMockSession('/home/user/projects/my-app', 1500, 2500, 0.08),
|
||||||
createMockSession('/home/user/workspaces/repo-name/worktrees/feature-branch', 2000, 3000, 0.12),
|
createMockSession(
|
||||||
|
'/home/user/workspaces/repo-name/worktrees/feature-branch',
|
||||||
|
2000,
|
||||||
|
3000,
|
||||||
|
0.12
|
||||||
|
),
|
||||||
createMockSession('/Users/joe/Developer/share-pi', 1000, 2000, 0.05),
|
createMockSession('/Users/joe/Developer/share-pi', 1000, 2000, 0.05),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@@ -104,7 +117,9 @@ describe('SessionStatsCard', () => {
|
|||||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||||
|
|
||||||
// Average cost: (0.08 + 0.12 + 0.05) / 3 = 0.0833 → $0.08
|
// Average cost: (0.08 + 0.12 + 0.05) / 3 = 0.0833 → $0.08
|
||||||
expect(screen.getByText('$0.08')).toBeInTheDocument();
|
// Use getAllByText since cost may appear multiple times (per session + average)
|
||||||
|
const costElements = screen.getAllByText('$0.08');
|
||||||
|
expect(costElements.length).toBeGreaterThan(0);
|
||||||
expect(screen.getByText('Avg Cost/Session')).toBeInTheDocument();
|
expect(screen.getByText('Avg Cost/Session')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,7 +141,7 @@ describe('SessionStatsCard', () => {
|
|||||||
outputTokens: 2000,
|
outputTokens: 2000,
|
||||||
cost: 0.05,
|
cost: 0.05,
|
||||||
lastActivity: new Date().toISOString(),
|
lastActivity: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
total: 1,
|
total: 1,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -144,12 +159,13 @@ describe('SessionStatsCard', () => {
|
|||||||
sessions: [
|
sessions: [
|
||||||
{
|
{
|
||||||
sessionId: '1',
|
sessionId: '1',
|
||||||
projectPath: '/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08',
|
projectPath:
|
||||||
|
'/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08',
|
||||||
inputTokens: 1000,
|
inputTokens: 1000,
|
||||||
outputTokens: 2000,
|
outputTokens: 2000,
|
||||||
cost: 0.05,
|
cost: 0.05,
|
||||||
lastActivity: new Date().toISOString(),
|
lastActivity: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
total: 1,
|
total: 1,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -159,8 +175,11 @@ describe('SessionStatsCard', () => {
|
|||||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||||
|
|
||||||
// Should show "2026-01-08" instead of just "08"
|
// Should show "2026-01-08" instead of just "08"
|
||||||
expect(screen.getByTitle('/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08'))
|
expect(
|
||||||
.toHaveTextContent('2026-01-08');
|
screen.getByTitle(
|
||||||
|
'/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08'
|
||||||
|
)
|
||||||
|
).toHaveTextContent('2026-01-08');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays correct project name for shared project', () => {
|
it('displays correct project name for shared project', () => {
|
||||||
@@ -173,7 +192,7 @@ describe('SessionStatsCard', () => {
|
|||||||
outputTokens: 2000,
|
outputTokens: 2000,
|
||||||
cost: 0.05,
|
cost: 0.05,
|
||||||
lastActivity: new Date().toISOString(),
|
lastActivity: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
total: 1,
|
total: 1,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -196,7 +215,7 @@ describe('SessionStatsCard', () => {
|
|||||||
outputTokens: 2000,
|
outputTokens: 2000,
|
||||||
cost: 0.05,
|
cost: 0.05,
|
||||||
lastActivity: new Date().toISOString(),
|
lastActivity: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
total: 1,
|
total: 1,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -219,7 +238,7 @@ describe('SessionStatsCard', () => {
|
|||||||
outputTokens: 2000,
|
outputTokens: 2000,
|
||||||
cost: 0.05,
|
cost: 0.05,
|
||||||
lastActivity: new Date().toISOString(),
|
lastActivity: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
total: 1,
|
total: 1,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -241,10 +260,10 @@ describe('SessionStatsCard', () => {
|
|||||||
sessionId: '1',
|
sessionId: '1',
|
||||||
projectPath: '/project/test',
|
projectPath: '/project/test',
|
||||||
inputTokens: 1500000, // 1.5M
|
inputTokens: 1500000, // 1.5M
|
||||||
outputTokens: 500000, // 500K
|
outputTokens: 500000, // 500K
|
||||||
cost: 0.10,
|
cost: 0.1,
|
||||||
lastActivity: new Date().toISOString(),
|
lastActivity: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
total: 1,
|
total: 1,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -261,13 +280,26 @@ describe('SessionStatsCard', () => {
|
|||||||
it('blurs cost information when privacy mode is enabled', () => {
|
it('blurs cost information when privacy mode is enabled', () => {
|
||||||
// This would require mocking the privacy context
|
// This would require mocking the privacy context
|
||||||
// For now, just ensure the component renders with privacy mode
|
// For now, just ensure the component renders with privacy mode
|
||||||
const { container } = render(
|
const testData: PaginatedSessions = {
|
||||||
<SessionStatsCard data={mockData} />,
|
sessions: [
|
||||||
{ wrapper: AllProviders }
|
{
|
||||||
);
|
sessionId: '1',
|
||||||
|
projectPath: '/home/user/project',
|
||||||
|
inputTokens: 1000,
|
||||||
|
outputTokens: 2000,
|
||||||
|
cost: 0.05,
|
||||||
|
lastActivity: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = render(<SessionStatsCard data={testData} />, { wrapper: AllProviders });
|
||||||
|
|
||||||
// Component should render without errors
|
// Component should render without errors
|
||||||
expect(container).toBeInTheDocument();
|
expect(container).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user