314 lines
9.4 KiB
TypeScript
314 lines
9.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { AdminUserViewModel, DashboardStatsViewModel, UserListViewModel } from './AdminUserViewModel';
|
|
import type { AdminUserViewData } from '@/lib/view-data/AdminUserViewData';
|
|
import type { DashboardStatsViewData } from '@/lib/view-data/DashboardStatsViewData';
|
|
|
|
describe('AdminUserViewModel', () => {
|
|
const createBaseViewData = (): AdminUserViewData => ({
|
|
id: 'user-123',
|
|
email: 'test@example.com',
|
|
displayName: 'Test User',
|
|
roles: ['user'],
|
|
status: 'active',
|
|
isSystemAdmin: false,
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
updatedAt: '2024-01-02T00:00:00Z',
|
|
lastLoginAt: '2024-01-15T10:30:00Z',
|
|
primaryDriverId: 'driver-456',
|
|
});
|
|
|
|
it('maps core fields from ViewData', () => {
|
|
const viewData = createBaseViewData();
|
|
const vm = new AdminUserViewModel(viewData);
|
|
|
|
expect(vm.id).toBe('user-123');
|
|
expect(vm.email).toBe('test@example.com');
|
|
expect(vm.displayName).toBe('Test User');
|
|
expect(vm.roles).toEqual(['user']);
|
|
expect(vm.status).toBe('active');
|
|
expect(vm.isSystemAdmin).toBe(false);
|
|
expect(vm.primaryDriverId).toBe('driver-456');
|
|
});
|
|
|
|
it('converts dates to Date objects', () => {
|
|
const viewData = createBaseViewData();
|
|
const vm = new AdminUserViewModel(viewData);
|
|
|
|
expect(vm.createdAt).toBeInstanceOf(Date);
|
|
expect(vm.updatedAt).toBeInstanceOf(Date);
|
|
expect(vm.lastLoginAt).toBeInstanceOf(Date);
|
|
expect(vm.createdAt.toISOString()).toBe('2024-01-01T00:00:00.000Z');
|
|
});
|
|
|
|
it('handles missing lastLoginAt', () => {
|
|
const viewData = createBaseViewData();
|
|
delete viewData.lastLoginAt;
|
|
const vm = new AdminUserViewModel(viewData);
|
|
|
|
expect(vm.lastLoginAt).toBeUndefined();
|
|
expect(vm.lastLoginFormatted).toBe('Never');
|
|
});
|
|
|
|
it('formats role badges correctly', () => {
|
|
const owner = new AdminUserViewModel({ ...createBaseViewData(), roles: ['owner'] });
|
|
const admin = new AdminUserViewModel({ ...createBaseViewData(), roles: ['admin'] });
|
|
const user = new AdminUserViewModel({ ...createBaseViewData(), roles: ['user'] });
|
|
const custom = new AdminUserViewModel({ ...createBaseViewData(), roles: ['custom-role'] });
|
|
|
|
expect(owner.roleBadges).toEqual(['Owner']);
|
|
expect(admin.roleBadges).toEqual(['Admin']);
|
|
expect(user.roleBadges).toEqual(['User']);
|
|
expect(custom.roleBadges).toEqual(['custom-role']);
|
|
});
|
|
|
|
it('derives status badge correctly', () => {
|
|
const active = new AdminUserViewModel({ ...createBaseViewData(), status: 'active' });
|
|
const suspended = new AdminUserViewModel({ ...createBaseViewData(), status: 'suspended' });
|
|
const deleted = new AdminUserViewModel({ ...createBaseViewData(), status: 'deleted' });
|
|
|
|
expect(active.statusBadgeLabel).toBe('Active');
|
|
expect(active.statusBadgeVariant).toBe('performance-green');
|
|
expect(suspended.statusBadgeLabel).toBe('Suspended');
|
|
expect(suspended.statusBadgeVariant).toBe('yellow-500');
|
|
expect(deleted.statusBadgeLabel).toBe('Deleted');
|
|
expect(deleted.statusBadgeVariant).toBe('racing-red');
|
|
});
|
|
|
|
it('formats dates for display', () => {
|
|
const viewData = createBaseViewData();
|
|
const vm = new AdminUserViewModel(viewData);
|
|
|
|
expect(vm.lastLoginFormatted).toBe('Jan 15, 2024');
|
|
expect(vm.createdAtFormatted).toBe('Jan 1, 2024');
|
|
});
|
|
|
|
it('handles multiple roles', () => {
|
|
const viewData = { ...createBaseViewData(), roles: ['owner', 'admin'] };
|
|
const vm = new AdminUserViewModel(viewData);
|
|
|
|
expect(vm.roleBadges).toEqual(['Owner', 'Admin']);
|
|
});
|
|
});
|
|
|
|
describe('DashboardStatsViewModel', () => {
|
|
const createBaseData = (): DashboardStatsViewData => ({
|
|
totalUsers: 100,
|
|
activeUsers: 70,
|
|
suspendedUsers: 10,
|
|
deletedUsers: 20,
|
|
systemAdmins: 5,
|
|
recentLogins: 25,
|
|
newUsersToday: 3,
|
|
userGrowth: [
|
|
{ label: 'Mon', value: 5, color: 'text-primary-blue' },
|
|
{ label: 'Tue', value: 8, color: 'text-primary-blue' },
|
|
],
|
|
roleDistribution: [
|
|
{ label: 'Owner', value: 2, color: 'text-purple-500' },
|
|
{ label: 'Admin', value: 3, color: 'text-blue-500' },
|
|
{ label: 'User', value: 95, color: 'text-gray-500' },
|
|
],
|
|
statusDistribution: {
|
|
active: 70,
|
|
suspended: 10,
|
|
deleted: 20,
|
|
},
|
|
activityTimeline: [
|
|
{ date: 'Mon', newUsers: 2, logins: 10 },
|
|
{ date: 'Tue', newUsers: 3, logins: 15 },
|
|
],
|
|
});
|
|
|
|
it('maps all core fields from data', () => {
|
|
const data = createBaseData();
|
|
const vm = new DashboardStatsViewModel(data);
|
|
|
|
expect(vm.totalUsers).toBe(100);
|
|
expect(vm.activeUsers).toBe(70);
|
|
expect(vm.suspendedUsers).toBe(10);
|
|
expect(vm.deletedUsers).toBe(20);
|
|
expect(vm.systemAdmins).toBe(5);
|
|
expect(vm.recentLogins).toBe(25);
|
|
expect(vm.newUsersToday).toBe(3);
|
|
});
|
|
|
|
it('computes active rate correctly', () => {
|
|
const vm = new DashboardStatsViewModel(createBaseData());
|
|
|
|
expect(vm.activeRate).toBe(70); // 70%
|
|
expect(vm.activeRateFormatted).toBe('70%');
|
|
});
|
|
|
|
it('computes admin ratio correctly', () => {
|
|
const vm = new DashboardStatsViewModel(createBaseData());
|
|
// 5 admins, 95 non-admins => 1:19
|
|
expect(vm.adminRatio).toBe('1:19');
|
|
});
|
|
|
|
it('derives activity level correctly', () => {
|
|
const lowEngagement = new DashboardStatsViewModel({
|
|
...createBaseData(),
|
|
totalUsers: 100,
|
|
recentLogins: 10, // 10% engagement
|
|
});
|
|
expect(lowEngagement.activityLevelLabel).toBe('Low');
|
|
expect(lowEngagement.activityLevelValue).toBe('low');
|
|
|
|
const mediumEngagement = new DashboardStatsViewModel({
|
|
...createBaseData(),
|
|
totalUsers: 100,
|
|
recentLogins: 35, // 35% engagement
|
|
});
|
|
expect(mediumEngagement.activityLevelLabel).toBe('Medium');
|
|
expect(mediumEngagement.activityLevelValue).toBe('medium');
|
|
|
|
const highEngagement = new DashboardStatsViewModel({
|
|
...createBaseData(),
|
|
totalUsers: 100,
|
|
recentLogins: 60, // 60% engagement
|
|
});
|
|
expect(highEngagement.activityLevelLabel).toBe('High');
|
|
expect(highEngagement.activityLevelValue).toBe('high');
|
|
});
|
|
|
|
it('handles zero users safely', () => {
|
|
const vm = new DashboardStatsViewModel({
|
|
...createBaseData(),
|
|
totalUsers: 0,
|
|
activeUsers: 0,
|
|
systemAdmins: 0,
|
|
recentLogins: 0,
|
|
});
|
|
|
|
expect(vm.activeRate).toBe(0);
|
|
expect(vm.activeRateFormatted).toBe('0%');
|
|
expect(vm.adminRatio).toBe('1:1');
|
|
expect(vm.activityLevelLabel).toBe('Low');
|
|
expect(vm.activityLevelValue).toBe('low');
|
|
});
|
|
|
|
it('preserves arrays from input', () => {
|
|
const data = createBaseData();
|
|
const vm = new DashboardStatsViewModel(data);
|
|
|
|
expect(vm.userGrowth).toEqual(data.userGrowth);
|
|
expect(vm.roleDistribution).toEqual(data.roleDistribution);
|
|
expect(vm.activityTimeline).toEqual(data.activityTimeline);
|
|
});
|
|
});
|
|
|
|
describe('UserListViewModel', () => {
|
|
const createViewData = (overrides: Partial<AdminUserViewData> = {}): AdminUserViewData => ({
|
|
id: 'user-1',
|
|
email: 'test@example.com',
|
|
displayName: 'Test User',
|
|
roles: ['user'],
|
|
status: 'active',
|
|
isSystemAdmin: false,
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
updatedAt: '2024-01-02T00:00:00Z',
|
|
...overrides,
|
|
});
|
|
|
|
it('wraps user ViewData in AdminUserViewModel instances', () => {
|
|
const data = {
|
|
users: [createViewData({ id: 'user-1' }), createViewData({ id: 'user-2' })],
|
|
total: 2,
|
|
page: 1,
|
|
limit: 10,
|
|
totalPages: 1,
|
|
};
|
|
|
|
const vm = new UserListViewModel(data);
|
|
|
|
expect(vm.users).toHaveLength(2);
|
|
expect(vm.users[0]).toBeInstanceOf(AdminUserViewModel);
|
|
expect(vm.users[0].id).toBe('user-1');
|
|
expect(vm.users[1].id).toBe('user-2');
|
|
});
|
|
|
|
it('exposes pagination metadata', () => {
|
|
const data = {
|
|
users: [createViewData()],
|
|
total: 50,
|
|
page: 2,
|
|
limit: 10,
|
|
totalPages: 5,
|
|
};
|
|
|
|
const vm = new UserListViewModel(data);
|
|
|
|
expect(vm.total).toBe(50);
|
|
expect(vm.page).toBe(2);
|
|
expect(vm.limit).toBe(10);
|
|
expect(vm.totalPages).toBe(5);
|
|
});
|
|
|
|
it('derives hasUsers correctly', () => {
|
|
const withUsers = new UserListViewModel({
|
|
users: [createViewData()],
|
|
total: 1,
|
|
page: 1,
|
|
limit: 10,
|
|
totalPages: 1,
|
|
});
|
|
|
|
const withoutUsers = new UserListViewModel({
|
|
users: [],
|
|
total: 0,
|
|
page: 1,
|
|
limit: 10,
|
|
totalPages: 0,
|
|
});
|
|
|
|
expect(withUsers.hasUsers).toBe(true);
|
|
expect(withoutUsers.hasUsers).toBe(false);
|
|
});
|
|
|
|
it('derives showPagination correctly', () => {
|
|
const withPagination = new UserListViewModel({
|
|
users: [createViewData()],
|
|
total: 20,
|
|
page: 1,
|
|
limit: 10,
|
|
totalPages: 2,
|
|
});
|
|
|
|
const withoutPagination = new UserListViewModel({
|
|
users: [createViewData()],
|
|
total: 5,
|
|
page: 1,
|
|
limit: 10,
|
|
totalPages: 1,
|
|
});
|
|
|
|
expect(withPagination.showPagination).toBe(true);
|
|
expect(withoutPagination.showPagination).toBe(false);
|
|
});
|
|
|
|
it('calculates start and end indices correctly', () => {
|
|
const vm = new UserListViewModel({
|
|
users: [createViewData(), createViewData(), createViewData()],
|
|
total: 50,
|
|
page: 2,
|
|
limit: 10,
|
|
totalPages: 5,
|
|
});
|
|
|
|
expect(vm.startIndex).toBe(11); // (2-1) * 10 + 1
|
|
expect(vm.endIndex).toBe(13); // min(2 * 10, 50)
|
|
});
|
|
|
|
it('handles empty list indices', () => {
|
|
const vm = new UserListViewModel({
|
|
users: [],
|
|
total: 0,
|
|
page: 1,
|
|
limit: 10,
|
|
totalPages: 0,
|
|
});
|
|
|
|
expect(vm.startIndex).toBe(0);
|
|
expect(vm.endIndex).toBe(0);
|
|
});
|
|
}); |