view data tests
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AdminDashboardViewDataBuilder } from './AdminDashboardViewDataBuilder';
|
||||
import type { DashboardStats } from '@/lib/types/admin';
|
||||
|
||||
describe('AdminDashboardViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform DashboardStats DTO to AdminDashboardViewData correctly', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 1000,
|
||||
activeUsers: 800,
|
||||
suspendedUsers: 50,
|
||||
deletedUsers: 150,
|
||||
systemAdmins: 5,
|
||||
recentLogins: 120,
|
||||
newUsersToday: 15,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result).toEqual({
|
||||
stats: {
|
||||
totalUsers: 1000,
|
||||
activeUsers: 800,
|
||||
suspendedUsers: 50,
|
||||
deletedUsers: 150,
|
||||
systemAdmins: 5,
|
||||
recentLogins: 120,
|
||||
newUsersToday: 15,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle zero values correctly', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
suspendedUsers: 0,
|
||||
deletedUsers: 0,
|
||||
systemAdmins: 0,
|
||||
recentLogins: 0,
|
||||
newUsersToday: 0,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result).toEqual({
|
||||
stats: {
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
suspendedUsers: 0,
|
||||
deletedUsers: 0,
|
||||
systemAdmins: 0,
|
||||
recentLogins: 0,
|
||||
newUsersToday: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle large numbers correctly', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 1000000,
|
||||
activeUsers: 750000,
|
||||
suspendedUsers: 25000,
|
||||
deletedUsers: 225000,
|
||||
systemAdmins: 50,
|
||||
recentLogins: 50000,
|
||||
newUsersToday: 1000,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result.stats.totalUsers).toBe(1000000);
|
||||
expect(result.stats.activeUsers).toBe(750000);
|
||||
expect(result.stats.systemAdmins).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 500,
|
||||
activeUsers: 400,
|
||||
suspendedUsers: 25,
|
||||
deletedUsers: 75,
|
||||
systemAdmins: 3,
|
||||
recentLogins: 80,
|
||||
newUsersToday: 10,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result.stats.totalUsers).toBe(dashboardStats.totalUsers);
|
||||
expect(result.stats.activeUsers).toBe(dashboardStats.activeUsers);
|
||||
expect(result.stats.suspendedUsers).toBe(dashboardStats.suspendedUsers);
|
||||
expect(result.stats.deletedUsers).toBe(dashboardStats.deletedUsers);
|
||||
expect(result.stats.systemAdmins).toBe(dashboardStats.systemAdmins);
|
||||
expect(result.stats.recentLogins).toBe(dashboardStats.recentLogins);
|
||||
expect(result.stats.newUsersToday).toBe(dashboardStats.newUsersToday);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 100,
|
||||
activeUsers: 80,
|
||||
suspendedUsers: 5,
|
||||
deletedUsers: 15,
|
||||
systemAdmins: 2,
|
||||
recentLogins: 20,
|
||||
newUsersToday: 5,
|
||||
};
|
||||
|
||||
const originalStats = { ...dashboardStats };
|
||||
AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(dashboardStats).toEqual(originalStats);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle negative values (if API returns them)', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: -1,
|
||||
activeUsers: -1,
|
||||
suspendedUsers: -1,
|
||||
deletedUsers: -1,
|
||||
systemAdmins: -1,
|
||||
recentLogins: -1,
|
||||
newUsersToday: -1,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result.stats.totalUsers).toBe(-1);
|
||||
expect(result.stats.activeUsers).toBe(-1);
|
||||
});
|
||||
|
||||
it('should handle very large numbers', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: Number.MAX_SAFE_INTEGER,
|
||||
activeUsers: Number.MAX_SAFE_INTEGER - 1000,
|
||||
suspendedUsers: 100,
|
||||
deletedUsers: 100,
|
||||
systemAdmins: 10,
|
||||
recentLogins: 1000,
|
||||
newUsersToday: 100,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result.stats.totalUsers).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(result.stats.activeUsers).toBe(Number.MAX_SAFE_INTEGER - 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,181 +1,7 @@
|
||||
/**
|
||||
* View Data Layer Tests - Admin Functionality
|
||||
*
|
||||
* This test file covers the view data layer for admin functionality.
|
||||
*
|
||||
* The view data layer is responsible for:
|
||||
* - DTO → UI model mapping
|
||||
* - Formatting, sorting, and grouping
|
||||
* - Derived fields and defaults
|
||||
* - UI-specific semantics
|
||||
*
|
||||
* This layer isolates the UI from API churn by providing a stable interface
|
||||
* between the API layer and the presentation layer.
|
||||
*
|
||||
* Test coverage includes:
|
||||
* - Admin dashboard data transformation
|
||||
* - User management view models
|
||||
* - Admin-specific formatting and validation
|
||||
* - Derived fields for admin UI components
|
||||
* - Default values and fallbacks for admin views
|
||||
*/
|
||||
|
||||
import { AdminDashboardViewDataBuilder } from '@/lib/builders/view-data/AdminDashboardViewDataBuilder';
|
||||
import { AdminUsersViewDataBuilder } from '@/lib/builders/view-data/AdminUsersViewDataBuilder';
|
||||
import type { DashboardStats } from '@/lib/types/admin';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AdminUsersViewDataBuilder } from './AdminUsersViewDataBuilder';
|
||||
import type { UserListResponse } from '@/lib/types/admin';
|
||||
|
||||
describe('AdminDashboardViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform DashboardStats DTO to AdminDashboardViewData correctly', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 1000,
|
||||
activeUsers: 800,
|
||||
suspendedUsers: 50,
|
||||
deletedUsers: 150,
|
||||
systemAdmins: 5,
|
||||
recentLogins: 120,
|
||||
newUsersToday: 15,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result).toEqual({
|
||||
stats: {
|
||||
totalUsers: 1000,
|
||||
activeUsers: 800,
|
||||
suspendedUsers: 50,
|
||||
deletedUsers: 150,
|
||||
systemAdmins: 5,
|
||||
recentLogins: 120,
|
||||
newUsersToday: 15,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle zero values correctly', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
suspendedUsers: 0,
|
||||
deletedUsers: 0,
|
||||
systemAdmins: 0,
|
||||
recentLogins: 0,
|
||||
newUsersToday: 0,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result).toEqual({
|
||||
stats: {
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
suspendedUsers: 0,
|
||||
deletedUsers: 0,
|
||||
systemAdmins: 0,
|
||||
recentLogins: 0,
|
||||
newUsersToday: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle large numbers correctly', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 1000000,
|
||||
activeUsers: 750000,
|
||||
suspendedUsers: 25000,
|
||||
deletedUsers: 225000,
|
||||
systemAdmins: 50,
|
||||
recentLogins: 50000,
|
||||
newUsersToday: 1000,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result.stats.totalUsers).toBe(1000000);
|
||||
expect(result.stats.activeUsers).toBe(750000);
|
||||
expect(result.stats.systemAdmins).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 500,
|
||||
activeUsers: 400,
|
||||
suspendedUsers: 25,
|
||||
deletedUsers: 75,
|
||||
systemAdmins: 3,
|
||||
recentLogins: 80,
|
||||
newUsersToday: 10,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result.stats.totalUsers).toBe(dashboardStats.totalUsers);
|
||||
expect(result.stats.activeUsers).toBe(dashboardStats.activeUsers);
|
||||
expect(result.stats.suspendedUsers).toBe(dashboardStats.suspendedUsers);
|
||||
expect(result.stats.deletedUsers).toBe(dashboardStats.deletedUsers);
|
||||
expect(result.stats.systemAdmins).toBe(dashboardStats.systemAdmins);
|
||||
expect(result.stats.recentLogins).toBe(dashboardStats.recentLogins);
|
||||
expect(result.stats.newUsersToday).toBe(dashboardStats.newUsersToday);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 100,
|
||||
activeUsers: 80,
|
||||
suspendedUsers: 5,
|
||||
deletedUsers: 15,
|
||||
systemAdmins: 2,
|
||||
recentLogins: 20,
|
||||
newUsersToday: 5,
|
||||
};
|
||||
|
||||
const originalStats = { ...dashboardStats };
|
||||
AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(dashboardStats).toEqual(originalStats);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle negative values (if API returns them)', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: -1,
|
||||
activeUsers: -1,
|
||||
suspendedUsers: -1,
|
||||
deletedUsers: -1,
|
||||
systemAdmins: -1,
|
||||
recentLogins: -1,
|
||||
newUsersToday: -1,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result.stats.totalUsers).toBe(-1);
|
||||
expect(result.stats.activeUsers).toBe(-1);
|
||||
});
|
||||
|
||||
it('should handle very large numbers', () => {
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: Number.MAX_SAFE_INTEGER,
|
||||
activeUsers: Number.MAX_SAFE_INTEGER - 1000,
|
||||
suspendedUsers: 100,
|
||||
deletedUsers: 100,
|
||||
systemAdmins: 10,
|
||||
recentLogins: 1000,
|
||||
newUsersToday: 100,
|
||||
};
|
||||
|
||||
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
|
||||
|
||||
expect(result.stats.totalUsers).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(result.stats.activeUsers).toBe(Number.MAX_SAFE_INTEGER - 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AdminUsersViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform UserListResponse DTO to AdminUsersViewData correctly', () => {
|
||||
@@ -0,0 +1,249 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LoginViewDataBuilder } from './LoginViewDataBuilder';
|
||||
import { SignupViewDataBuilder } from './SignupViewDataBuilder';
|
||||
import { ForgotPasswordViewDataBuilder } from './ForgotPasswordViewDataBuilder';
|
||||
import { ResetPasswordViewDataBuilder } from './ResetPasswordViewDataBuilder';
|
||||
import type { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
|
||||
import type { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO';
|
||||
import type { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO';
|
||||
import type { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO';
|
||||
|
||||
describe('Auth View Data - Cross-Builder Consistency', () => {
|
||||
describe('common patterns', () => {
|
||||
it('should all initialize with isSubmitting false', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.isSubmitting).toBe(false);
|
||||
expect(signupResult.isSubmitting).toBe(false);
|
||||
expect(forgotPasswordResult.isSubmitting).toBe(false);
|
||||
expect(resetPasswordResult.isSubmitting).toBe(false);
|
||||
});
|
||||
|
||||
it('should all initialize with submitError undefined', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.submitError).toBeUndefined();
|
||||
expect(signupResult.submitError).toBeUndefined();
|
||||
expect(forgotPasswordResult.submitError).toBeUndefined();
|
||||
expect(resetPasswordResult.submitError).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should all initialize formState.isValid as true', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.formState.isValid).toBe(true);
|
||||
expect(signupResult.formState.isValid).toBe(true);
|
||||
expect(forgotPasswordResult.formState.isValid).toBe(true);
|
||||
expect(resetPasswordResult.formState.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it('should all initialize formState.isSubmitting as false', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.formState.isSubmitting).toBe(false);
|
||||
expect(signupResult.formState.isSubmitting).toBe(false);
|
||||
expect(forgotPasswordResult.formState.isSubmitting).toBe(false);
|
||||
expect(resetPasswordResult.formState.isSubmitting).toBe(false);
|
||||
});
|
||||
|
||||
it('should all initialize formState.submitError as undefined', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.formState.submitError).toBeUndefined();
|
||||
expect(signupResult.formState.submitError).toBeUndefined();
|
||||
expect(forgotPasswordResult.formState.submitError).toBeUndefined();
|
||||
expect(resetPasswordResult.formState.submitError).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should all initialize formState.submitCount as 0', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.formState.submitCount).toBe(0);
|
||||
expect(signupResult.formState.submitCount).toBe(0);
|
||||
expect(forgotPasswordResult.formState.submitCount).toBe(0);
|
||||
expect(resetPasswordResult.formState.submitCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should all initialize form fields with touched false', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.formState.fields.email.touched).toBe(false);
|
||||
expect(loginResult.formState.fields.password.touched).toBe(false);
|
||||
expect(loginResult.formState.fields.rememberMe.touched).toBe(false);
|
||||
|
||||
expect(signupResult.formState.fields.firstName.touched).toBe(false);
|
||||
expect(signupResult.formState.fields.lastName.touched).toBe(false);
|
||||
expect(signupResult.formState.fields.email.touched).toBe(false);
|
||||
expect(signupResult.formState.fields.password.touched).toBe(false);
|
||||
expect(signupResult.formState.fields.confirmPassword.touched).toBe(false);
|
||||
|
||||
expect(forgotPasswordResult.formState.fields.email.touched).toBe(false);
|
||||
|
||||
expect(resetPasswordResult.formState.fields.newPassword.touched).toBe(false);
|
||||
expect(resetPasswordResult.formState.fields.confirmPassword.touched).toBe(false);
|
||||
});
|
||||
|
||||
it('should all initialize form fields with validating false', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.formState.fields.email.validating).toBe(false);
|
||||
expect(loginResult.formState.fields.password.validating).toBe(false);
|
||||
expect(loginResult.formState.fields.rememberMe.validating).toBe(false);
|
||||
|
||||
expect(signupResult.formState.fields.firstName.validating).toBe(false);
|
||||
expect(signupResult.formState.fields.lastName.validating).toBe(false);
|
||||
expect(signupResult.formState.fields.email.validating).toBe(false);
|
||||
expect(signupResult.formState.fields.password.validating).toBe(false);
|
||||
expect(signupResult.formState.fields.confirmPassword.validating).toBe(false);
|
||||
|
||||
expect(forgotPasswordResult.formState.fields.email.validating).toBe(false);
|
||||
|
||||
expect(resetPasswordResult.formState.fields.newPassword.validating).toBe(false);
|
||||
expect(resetPasswordResult.formState.fields.confirmPassword.validating).toBe(false);
|
||||
});
|
||||
|
||||
it('should all initialize form fields with error undefined', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.formState.fields.email.error).toBeUndefined();
|
||||
expect(loginResult.formState.fields.password.error).toBeUndefined();
|
||||
expect(loginResult.formState.fields.rememberMe.error).toBeUndefined();
|
||||
|
||||
expect(signupResult.formState.fields.firstName.error).toBeUndefined();
|
||||
expect(signupResult.formState.fields.lastName.error).toBeUndefined();
|
||||
expect(signupResult.formState.fields.email.error).toBeUndefined();
|
||||
expect(signupResult.formState.fields.password.error).toBeUndefined();
|
||||
expect(signupResult.formState.fields.confirmPassword.error).toBeUndefined();
|
||||
|
||||
expect(forgotPasswordResult.formState.fields.email.error).toBeUndefined();
|
||||
|
||||
expect(resetPasswordResult.formState.fields.newPassword.error).toBeUndefined();
|
||||
expect(resetPasswordResult.formState.fields.confirmPassword.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('common returnTo handling', () => {
|
||||
it('should all handle returnTo with query parameters', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard?welcome=true', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard?welcome=true' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard?welcome=true' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard?welcome=true' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.returnTo).toBe('/dashboard?welcome=true');
|
||||
expect(signupResult.returnTo).toBe('/dashboard?welcome=true');
|
||||
expect(forgotPasswordResult.returnTo).toBe('/dashboard?welcome=true');
|
||||
expect(resetPasswordResult.returnTo).toBe('/dashboard?welcome=true');
|
||||
});
|
||||
|
||||
it('should all handle returnTo with hash fragments', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard#section', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard#section' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard#section' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard#section' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.returnTo).toBe('/dashboard#section');
|
||||
expect(signupResult.returnTo).toBe('/dashboard#section');
|
||||
expect(forgotPasswordResult.returnTo).toBe('/dashboard#section');
|
||||
expect(resetPasswordResult.returnTo).toBe('/dashboard#section');
|
||||
});
|
||||
|
||||
it('should all handle returnTo with encoded characters', () => {
|
||||
const loginDTO: LoginPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin', hasInsufficientPermissions: false };
|
||||
const signupDTO: SignupPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin' };
|
||||
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin' };
|
||||
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard?redirect=%2Fadmin' };
|
||||
|
||||
const loginResult = LoginViewDataBuilder.build(loginDTO);
|
||||
const signupResult = SignupViewDataBuilder.build(signupDTO);
|
||||
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
|
||||
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
|
||||
|
||||
expect(loginResult.returnTo).toBe('/dashboard?redirect=%2Fadmin');
|
||||
expect(signupResult.returnTo).toBe('/dashboard?redirect=%2Fadmin');
|
||||
expect(forgotPasswordResult.returnTo).toBe('/dashboard?redirect=%2Fadmin');
|
||||
expect(resetPasswordResult.returnTo).toBe('/dashboard?redirect=%2Fadmin');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AvatarViewDataBuilder } from './AvatarViewDataBuilder';
|
||||
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
|
||||
describe('AvatarViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform MediaBinaryDTO to AvatarViewData correctly', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle JPEG images', () => {
|
||||
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/jpeg',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle GIF images', () => {
|
||||
const buffer = new Uint8Array([0x47, 0x49, 0x46, 0x38]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/gif',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/gif');
|
||||
});
|
||||
|
||||
it('should handle SVG images', () => {
|
||||
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"></svg>');
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/svg+xml',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/svg+xml');
|
||||
});
|
||||
|
||||
it('should handle WebP images', () => {
|
||||
const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/webp',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/webp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBeDefined();
|
||||
expect(result.contentType).toBe(mediaDto.contentType);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const originalDto = { ...mediaDto };
|
||||
AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(mediaDto).toEqual(originalDto);
|
||||
});
|
||||
|
||||
it('should convert buffer to base64 string', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(typeof result.buffer).toBe('string');
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty buffer', () => {
|
||||
const buffer = new Uint8Array([]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe('');
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle large buffer', () => {
|
||||
const buffer = new Uint8Array(1024 * 1024); // 1MB
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle buffer with all zeros', () => {
|
||||
const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle buffer with all ones', () => {
|
||||
const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle different content types', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const contentTypes = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
'image/bmp',
|
||||
'image/tiff',
|
||||
];
|
||||
|
||||
contentTypes.forEach((contentType) => {
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType,
|
||||
};
|
||||
|
||||
const result = AvatarViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.contentType).toBe(contentType);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CategoryIconViewDataBuilder } from './CategoryIconViewDataBuilder';
|
||||
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
|
||||
describe('CategoryIconViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform MediaBinaryDTO to CategoryIconViewData correctly', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = CategoryIconViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle SVG icons', () => {
|
||||
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><circle cx="10" cy="10" r="5"/></svg>');
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/svg+xml',
|
||||
};
|
||||
|
||||
const result = CategoryIconViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/svg+xml');
|
||||
});
|
||||
|
||||
it('should handle small icon files', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = CategoryIconViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = CategoryIconViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBeDefined();
|
||||
expect(result.contentType).toBe(mediaDto.contentType);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const originalDto = { ...mediaDto };
|
||||
CategoryIconViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(mediaDto).toEqual(originalDto);
|
||||
});
|
||||
|
||||
it('should convert buffer to base64 string', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = CategoryIconViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(typeof result.buffer).toBe('string');
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty buffer', () => {
|
||||
const buffer = new Uint8Array([]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = CategoryIconViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe('');
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle buffer with special characters', () => {
|
||||
const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = CategoryIconViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CompleteOnboardingViewDataBuilder } from './CompleteOnboardingViewDataBuilder';
|
||||
import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
|
||||
|
||||
describe('CompleteOnboardingViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform successful onboarding completion DTO to ViewData correctly', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
errorMessage: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle onboarding completion with error message', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: 'Failed to complete onboarding',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: 'Failed to complete onboarding',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle onboarding completion with only success field', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
driverId: undefined,
|
||||
errorMessage: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
errorMessage: undefined,
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.success).toBe(apiDto.success);
|
||||
expect(result.driverId).toBe(apiDto.driverId);
|
||||
expect(result.errorMessage).toBe(apiDto.errorMessage);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
errorMessage: undefined,
|
||||
};
|
||||
|
||||
const originalDto = { ...apiDto };
|
||||
CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(apiDto).toEqual(originalDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle false success value', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: 'Error occurred',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.driverId).toBeUndefined();
|
||||
expect(result.errorMessage).toBe('Error occurred');
|
||||
});
|
||||
|
||||
it('should handle empty string error message', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: '',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorMessage).toBe('');
|
||||
});
|
||||
|
||||
it('should handle very long driverId', () => {
|
||||
const longDriverId = 'driver-' + 'a'.repeat(1000);
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: longDriverId,
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.driverId).toBe(longDriverId);
|
||||
});
|
||||
|
||||
it('should handle special characters in error message', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: 'Error: "Failed to create driver" (code: 500)',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.errorMessage).toBe('Error: "Failed to create driver" (code: 500)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('derived fields calculation', () => {
|
||||
it('should calculate isSuccessful derived field correctly', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
// Note: The builder doesn't add derived fields, but we can verify the structure
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.driverId).toBe('driver-123');
|
||||
});
|
||||
|
||||
it('should handle success with no driverId', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: undefined,
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.driverId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle failure with driverId', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: false,
|
||||
driverId: 'driver-123',
|
||||
errorMessage: 'Partial failure',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.driverId).toBe('driver-123');
|
||||
expect(result.errorMessage).toBe('Partial failure');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,41 +1,6 @@
|
||||
/**
|
||||
* View Data Layer Tests - Dashboard Functionality
|
||||
*
|
||||
* This test file covers the view data layer for dashboard functionality.
|
||||
*
|
||||
* The view data layer is responsible for:
|
||||
* - DTO → UI model mapping
|
||||
* - Formatting, sorting, and grouping
|
||||
* - Derived fields and defaults
|
||||
* - UI-specific semantics
|
||||
*
|
||||
* This layer isolates the UI from API churn by providing a stable interface
|
||||
* between the API layer and the presentation layer.
|
||||
*
|
||||
* Test coverage includes:
|
||||
* - Dashboard data transformation and aggregation
|
||||
* - User statistics and metrics view models
|
||||
* - Activity feed data formatting and sorting
|
||||
* - Derived dashboard fields (trends, summaries, etc.)
|
||||
* - Default values and fallbacks for dashboard views
|
||||
* - Dashboard-specific formatting (dates, numbers, percentages, etc.)
|
||||
* - Data grouping and categorization for dashboard components
|
||||
* - Real-time data updates and state management
|
||||
*/
|
||||
|
||||
import { DashboardViewDataBuilder } from '@/lib/builders/view-data/DashboardViewDataBuilder';
|
||||
import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay';
|
||||
import { DashboardCountDisplay } from '@/lib/display-objects/DashboardCountDisplay';
|
||||
import { DashboardRankDisplay } from '@/lib/display-objects/DashboardRankDisplay';
|
||||
import { DashboardConsistencyDisplay } from '@/lib/display-objects/DashboardConsistencyDisplay';
|
||||
import { DashboardLeaguePositionDisplay } from '@/lib/display-objects/DashboardLeaguePositionDisplay';
|
||||
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DashboardViewDataBuilder } from './DashboardViewDataBuilder';
|
||||
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
|
||||
import type { DashboardDriverSummaryDTO } from '@/lib/types/generated/DashboardDriverSummaryDTO';
|
||||
import type { DashboardRaceSummaryDTO } from '@/lib/types/generated/DashboardRaceSummaryDTO';
|
||||
import type { DashboardFeedSummaryDTO } from '@/lib/types/generated/DashboardFeedSummaryDTO';
|
||||
import type { DashboardFriendSummaryDTO } from '@/lib/types/generated/DashboardFriendSummaryDTO';
|
||||
import type { DashboardLeagueStandingSummaryDTO } from '@/lib/types/generated/DashboardLeagueStandingSummaryDTO';
|
||||
|
||||
describe('DashboardViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
@@ -899,596 +864,3 @@ describe('DashboardViewDataBuilder', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardDateDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format future date correctly', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours from now
|
||||
|
||||
const result = DashboardDateDisplay.format(futureDate);
|
||||
|
||||
expect(result.date).toMatch(/^[A-Za-z]{3}, [A-Za-z]{3} \d{1,2}, \d{4}$/);
|
||||
expect(result.time).toMatch(/^\d{2}:\d{2}$/);
|
||||
expect(result.relative).toBe('1d');
|
||||
});
|
||||
|
||||
it('should format date less than 24 hours correctly', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 6 * 60 * 60 * 1000); // 6 hours from now
|
||||
|
||||
const result = DashboardDateDisplay.format(futureDate);
|
||||
|
||||
expect(result.relative).toBe('6h');
|
||||
});
|
||||
|
||||
it('should format date more than 24 hours correctly', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 48 * 60 * 60 * 1000); // 2 days from now
|
||||
|
||||
const result = DashboardDateDisplay.format(futureDate);
|
||||
|
||||
expect(result.relative).toBe('2d');
|
||||
});
|
||||
|
||||
it('should format past date correctly', () => {
|
||||
const now = new Date();
|
||||
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago
|
||||
|
||||
const result = DashboardDateDisplay.format(pastDate);
|
||||
|
||||
expect(result.relative).toBe('Past');
|
||||
});
|
||||
|
||||
it('should format current date correctly', () => {
|
||||
const now = new Date();
|
||||
|
||||
const result = DashboardDateDisplay.format(now);
|
||||
|
||||
expect(result.relative).toBe('Now');
|
||||
});
|
||||
|
||||
it('should format date with leading zeros in time', () => {
|
||||
const date = new Date('2024-01-15T05:03:00');
|
||||
|
||||
const result = DashboardDateDisplay.format(date);
|
||||
|
||||
expect(result.time).toBe('05:03');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle midnight correctly', () => {
|
||||
const date = new Date('2024-01-15T00:00:00');
|
||||
|
||||
const result = DashboardDateDisplay.format(date);
|
||||
|
||||
expect(result.time).toBe('00:00');
|
||||
});
|
||||
|
||||
it('should handle end of day correctly', () => {
|
||||
const date = new Date('2024-01-15T23:59:59');
|
||||
|
||||
const result = DashboardDateDisplay.format(date);
|
||||
|
||||
expect(result.time).toBe('23:59');
|
||||
});
|
||||
|
||||
it('should handle different days of week', () => {
|
||||
const date = new Date('2024-01-15'); // Monday
|
||||
|
||||
const result = DashboardDateDisplay.format(date);
|
||||
|
||||
expect(result.date).toContain('Mon');
|
||||
});
|
||||
|
||||
it('should handle different months', () => {
|
||||
const date = new Date('2024-01-15');
|
||||
|
||||
const result = DashboardDateDisplay.format(date);
|
||||
|
||||
expect(result.date).toContain('Jan');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardCountDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format positive numbers correctly', () => {
|
||||
expect(DashboardCountDisplay.format(0)).toBe('0');
|
||||
expect(DashboardCountDisplay.format(1)).toBe('1');
|
||||
expect(DashboardCountDisplay.format(100)).toBe('100');
|
||||
expect(DashboardCountDisplay.format(1000)).toBe('1000');
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
expect(DashboardCountDisplay.format(null)).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle undefined values', () => {
|
||||
expect(DashboardCountDisplay.format(undefined)).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle negative numbers', () => {
|
||||
expect(DashboardCountDisplay.format(-1)).toBe('-1');
|
||||
expect(DashboardCountDisplay.format(-100)).toBe('-100');
|
||||
});
|
||||
|
||||
it('should handle large numbers', () => {
|
||||
expect(DashboardCountDisplay.format(999999)).toBe('999999');
|
||||
expect(DashboardCountDisplay.format(1000000)).toBe('1000000');
|
||||
});
|
||||
|
||||
it('should handle decimal numbers', () => {
|
||||
expect(DashboardCountDisplay.format(1.5)).toBe('1.5');
|
||||
expect(DashboardCountDisplay.format(100.99)).toBe('100.99');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardRankDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format rank correctly', () => {
|
||||
expect(DashboardRankDisplay.format(1)).toBe('1');
|
||||
expect(DashboardRankDisplay.format(42)).toBe('42');
|
||||
expect(DashboardRankDisplay.format(100)).toBe('100');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle rank 0', () => {
|
||||
expect(DashboardRankDisplay.format(0)).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle large ranks', () => {
|
||||
expect(DashboardRankDisplay.format(999999)).toBe('999999');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardConsistencyDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format consistency correctly', () => {
|
||||
expect(DashboardConsistencyDisplay.format(0)).toBe('0%');
|
||||
expect(DashboardConsistencyDisplay.format(50)).toBe('50%');
|
||||
expect(DashboardConsistencyDisplay.format(100)).toBe('100%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle decimal consistency', () => {
|
||||
expect(DashboardConsistencyDisplay.format(85.5)).toBe('85.5%');
|
||||
expect(DashboardConsistencyDisplay.format(99.9)).toBe('99.9%');
|
||||
});
|
||||
|
||||
it('should handle negative consistency', () => {
|
||||
expect(DashboardConsistencyDisplay.format(-10)).toBe('-10%');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardLeaguePositionDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format position correctly', () => {
|
||||
expect(DashboardLeaguePositionDisplay.format(1)).toBe('#1');
|
||||
expect(DashboardLeaguePositionDisplay.format(5)).toBe('#5');
|
||||
expect(DashboardLeaguePositionDisplay.format(100)).toBe('#100');
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
expect(DashboardLeaguePositionDisplay.format(null)).toBe('-');
|
||||
});
|
||||
|
||||
it('should handle undefined values', () => {
|
||||
expect(DashboardLeaguePositionDisplay.format(undefined)).toBe('-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle position 0', () => {
|
||||
expect(DashboardLeaguePositionDisplay.format(0)).toBe('#0');
|
||||
});
|
||||
|
||||
it('should handle large positions', () => {
|
||||
expect(DashboardLeaguePositionDisplay.format(999)).toBe('#999');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('RatingDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format rating correctly', () => {
|
||||
expect(RatingDisplay.format(0)).toBe('0');
|
||||
expect(RatingDisplay.format(1234.56)).toBe('1,235');
|
||||
expect(RatingDisplay.format(9999.99)).toBe('10,000');
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
expect(RatingDisplay.format(null)).toBe('—');
|
||||
});
|
||||
|
||||
it('should handle undefined values', () => {
|
||||
expect(RatingDisplay.format(undefined)).toBe('—');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should round down correctly', () => {
|
||||
expect(RatingDisplay.format(1234.4)).toBe('1,234');
|
||||
});
|
||||
|
||||
it('should round up correctly', () => {
|
||||
expect(RatingDisplay.format(1234.6)).toBe('1,235');
|
||||
});
|
||||
|
||||
it('should handle decimal ratings', () => {
|
||||
expect(RatingDisplay.format(1234.5)).toBe('1,235');
|
||||
});
|
||||
|
||||
it('should handle large ratings', () => {
|
||||
expect(RatingDisplay.format(999999.99)).toBe('1,000,000');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dashboard View Data - Cross-Component Consistency', () => {
|
||||
describe('common patterns', () => {
|
||||
it('should all use consistent formatting for numeric values', () => {
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
rating: 1234.56,
|
||||
globalRank: 42,
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
consistency: 85,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 3,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [
|
||||
{
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Test League',
|
||||
position: 5,
|
||||
totalDrivers: 50,
|
||||
points: 1250,
|
||||
},
|
||||
],
|
||||
feedSummary: {
|
||||
notificationCount: 0,
|
||||
items: [],
|
||||
},
|
||||
friends: [
|
||||
{ id: 'friend-1', name: 'Alice', country: 'UK' },
|
||||
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
// All numeric values should be formatted as strings
|
||||
expect(typeof result.currentDriver.rating).toBe('string');
|
||||
expect(typeof result.currentDriver.rank).toBe('string');
|
||||
expect(typeof result.currentDriver.totalRaces).toBe('string');
|
||||
expect(typeof result.currentDriver.wins).toBe('string');
|
||||
expect(typeof result.currentDriver.podiums).toBe('string');
|
||||
expect(typeof result.currentDriver.consistency).toBe('string');
|
||||
expect(typeof result.activeLeaguesCount).toBe('string');
|
||||
expect(typeof result.friendCount).toBe('string');
|
||||
expect(typeof result.leagueStandings[0].position).toBe('string');
|
||||
expect(typeof result.leagueStandings[0].points).toBe('string');
|
||||
expect(typeof result.leagueStandings[0].totalDrivers).toBe('string');
|
||||
});
|
||||
|
||||
it('should all handle missing data gracefully', () => {
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 0,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 0,
|
||||
items: [],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
// All fields should have safe defaults
|
||||
expect(result.currentDriver.name).toBe('');
|
||||
expect(result.currentDriver.avatarUrl).toBe('');
|
||||
expect(result.currentDriver.country).toBe('');
|
||||
expect(result.currentDriver.rating).toBe('0.0');
|
||||
expect(result.currentDriver.rank).toBe('0');
|
||||
expect(result.currentDriver.totalRaces).toBe('0');
|
||||
expect(result.currentDriver.wins).toBe('0');
|
||||
expect(result.currentDriver.podiums).toBe('0');
|
||||
expect(result.currentDriver.consistency).toBe('0%');
|
||||
expect(result.nextRace).toBeNull();
|
||||
expect(result.upcomingRaces).toEqual([]);
|
||||
expect(result.leagueStandings).toEqual([]);
|
||||
expect(result.feedItems).toEqual([]);
|
||||
expect(result.friends).toEqual([]);
|
||||
expect(result.activeLeaguesCount).toBe('0');
|
||||
expect(result.friendCount).toBe('0');
|
||||
});
|
||||
|
||||
it('should all preserve ISO timestamps for serialization', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
const feedTimestamp = new Date(now.getTime() - 30 * 60 * 1000);
|
||||
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 1,
|
||||
nextRace: {
|
||||
id: 'race-1',
|
||||
track: 'Spa',
|
||||
car: 'Porsche',
|
||||
scheduledAt: futureDate.toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: true,
|
||||
},
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 1,
|
||||
items: [
|
||||
{
|
||||
id: 'feed-1',
|
||||
type: 'notification',
|
||||
headline: 'Test',
|
||||
timestamp: feedTimestamp.toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
// All timestamps should be preserved as ISO strings
|
||||
expect(result.nextRace?.scheduledAt).toBe(futureDate.toISOString());
|
||||
expect(result.feedItems[0].timestamp).toBe(feedTimestamp.toISOString());
|
||||
});
|
||||
|
||||
it('should all handle boolean flags correctly', () => {
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Spa',
|
||||
car: 'Porsche',
|
||||
scheduledAt: new Date().toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: true,
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
track: 'Monza',
|
||||
car: 'Ferrari',
|
||||
scheduledAt: new Date().toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: false,
|
||||
},
|
||||
],
|
||||
activeLeaguesCount: 1,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 0,
|
||||
items: [],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
expect(result.upcomingRaces[0].isMyLeague).toBe(true);
|
||||
expect(result.upcomingRaces[1].isMyLeague).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data integrity', () => {
|
||||
it('should maintain data consistency across transformations', () => {
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
rating: 1234.56,
|
||||
globalRank: 42,
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
consistency: 85,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 3,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 5,
|
||||
items: [],
|
||||
},
|
||||
friends: [
|
||||
{ id: 'friend-1', name: 'Alice', country: 'UK' },
|
||||
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
// Verify derived fields match their source data
|
||||
expect(result.friendCount).toBe(dashboardDTO.friends.length.toString());
|
||||
expect(result.activeLeaguesCount).toBe(dashboardDTO.activeLeaguesCount.toString());
|
||||
expect(result.hasFriends).toBe(dashboardDTO.friends.length > 0);
|
||||
expect(result.hasUpcomingRaces).toBe(dashboardDTO.upcomingRaces.length > 0);
|
||||
expect(result.hasLeagueStandings).toBe(dashboardDTO.leagueStandingsSummaries.length > 0);
|
||||
expect(result.hasFeedItems).toBe(dashboardDTO.feedSummary.items.length > 0);
|
||||
});
|
||||
|
||||
it('should handle complex real-world scenarios', () => {
|
||||
const now = new Date();
|
||||
const race1Date = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000);
|
||||
const race2Date = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
|
||||
const feedTimestamp = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
rating: 2456.78,
|
||||
globalRank: 15,
|
||||
totalRaces: 250,
|
||||
wins: 45,
|
||||
podiums: 120,
|
||||
consistency: 92.5,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [
|
||||
{
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Pro League',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
scheduledAt: race1Date.toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: true,
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
track: 'Monza',
|
||||
car: 'Ferrari 488 GT3',
|
||||
scheduledAt: race2Date.toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: false,
|
||||
},
|
||||
],
|
||||
activeLeaguesCount: 2,
|
||||
nextRace: {
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Pro League',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
scheduledAt: race1Date.toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: true,
|
||||
},
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [
|
||||
{
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Pro League',
|
||||
position: 3,
|
||||
totalDrivers: 100,
|
||||
points: 2450,
|
||||
},
|
||||
{
|
||||
leagueId: 'league-2',
|
||||
leagueName: 'Rookie League',
|
||||
position: 1,
|
||||
totalDrivers: 50,
|
||||
points: 1800,
|
||||
},
|
||||
],
|
||||
feedSummary: {
|
||||
notificationCount: 3,
|
||||
items: [
|
||||
{
|
||||
id: 'feed-1',
|
||||
type: 'race_result',
|
||||
headline: 'Race completed',
|
||||
body: 'You finished 3rd in the Pro League race',
|
||||
timestamp: feedTimestamp.toISOString(),
|
||||
ctaLabel: 'View Results',
|
||||
ctaHref: '/races/123',
|
||||
},
|
||||
{
|
||||
id: 'feed-2',
|
||||
type: 'league_update',
|
||||
headline: 'League standings updated',
|
||||
body: 'You moved up 2 positions',
|
||||
timestamp: feedTimestamp.toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
friends: [
|
||||
{ id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' },
|
||||
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
|
||||
{ id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
// Verify all transformations
|
||||
expect(result.currentDriver.name).toBe('John Doe');
|
||||
expect(result.currentDriver.rating).toBe('2,457');
|
||||
expect(result.currentDriver.rank).toBe('15');
|
||||
expect(result.currentDriver.totalRaces).toBe('250');
|
||||
expect(result.currentDriver.wins).toBe('45');
|
||||
expect(result.currentDriver.podiums).toBe('120');
|
||||
expect(result.currentDriver.consistency).toBe('92.5%');
|
||||
|
||||
expect(result.nextRace).not.toBeNull();
|
||||
expect(result.nextRace?.id).toBe('race-1');
|
||||
expect(result.nextRace?.track).toBe('Spa');
|
||||
expect(result.nextRace?.isMyLeague).toBe(true);
|
||||
|
||||
expect(result.upcomingRaces).toHaveLength(2);
|
||||
expect(result.upcomingRaces[0].isMyLeague).toBe(true);
|
||||
expect(result.upcomingRaces[1].isMyLeague).toBe(false);
|
||||
|
||||
expect(result.leagueStandings).toHaveLength(2);
|
||||
expect(result.leagueStandings[0].position).toBe('#3');
|
||||
expect(result.leagueStandings[0].points).toBe('2450');
|
||||
expect(result.leagueStandings[1].position).toBe('#1');
|
||||
expect(result.leagueStandings[1].points).toBe('1800');
|
||||
|
||||
expect(result.feedItems).toHaveLength(2);
|
||||
expect(result.feedItems[0].type).toBe('race_result');
|
||||
expect(result.feedItems[0].ctaLabel).toBe('View Results');
|
||||
expect(result.feedItems[1].type).toBe('league_update');
|
||||
expect(result.feedItems[1].ctaLabel).toBeUndefined();
|
||||
|
||||
expect(result.friends).toHaveLength(3);
|
||||
expect(result.friends[0].avatarUrl).toBe('https://example.com/alice.jpg');
|
||||
expect(result.friends[1].avatarUrl).toBe('');
|
||||
expect(result.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg');
|
||||
|
||||
expect(result.activeLeaguesCount).toBe('2');
|
||||
expect(result.friendCount).toBe('3');
|
||||
expect(result.hasUpcomingRaces).toBe(true);
|
||||
expect(result.hasLeagueStandings).toBe(true);
|
||||
expect(result.hasFeedItems).toBe(true);
|
||||
expect(result.hasFriends).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,456 +1,6 @@
|
||||
/**
|
||||
* View Data Layer Tests - Drivers Functionality
|
||||
*
|
||||
* This test file covers the view data layer for drivers functionality.
|
||||
*
|
||||
* The view data layer is responsible for:
|
||||
* - DTO → UI model mapping
|
||||
* - Formatting, sorting, and grouping
|
||||
* - Derived fields and defaults
|
||||
* - UI-specific semantics
|
||||
*
|
||||
* This layer isolates the UI from API churn by providing a stable interface
|
||||
* between the API layer and the presentation layer.
|
||||
*
|
||||
* Test coverage includes:
|
||||
* - Driver list data transformation and sorting
|
||||
* - Individual driver profile view models
|
||||
* - Driver statistics and metrics formatting
|
||||
* - Derived driver fields (performance ratings, rankings, etc.)
|
||||
* - Default values and fallbacks for driver views
|
||||
* - Driver-specific formatting (lap times, points, positions, etc.)
|
||||
* - Data grouping and categorization for driver components
|
||||
* - Driver search and filtering view models
|
||||
* - Driver comparison data transformation
|
||||
*/
|
||||
|
||||
import { DriversViewDataBuilder } from '@/lib/builders/view-data/DriversViewDataBuilder';
|
||||
import { DriverProfileViewDataBuilder } from '@/lib/builders/view-data/DriverProfileViewDataBuilder';
|
||||
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
|
||||
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
|
||||
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||
import { FinishDisplay } from '@/lib/display-objects/FinishDisplay';
|
||||
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay';
|
||||
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
|
||||
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DriverProfileViewDataBuilder } from './DriverProfileViewDataBuilder';
|
||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||
import type { DriverProfileDriverSummaryDTO } from '@/lib/types/generated/DriverProfileDriverSummaryDTO';
|
||||
import type { DriverProfileStatsDTO } from '@/lib/types/generated/DriverProfileStatsDTO';
|
||||
import type { DriverProfileFinishDistributionDTO } from '@/lib/types/generated/DriverProfileFinishDistributionDTO';
|
||||
import type { DriverProfileTeamMembershipDTO } from '@/lib/types/generated/DriverProfileTeamMembershipDTO';
|
||||
import type { DriverProfileSocialSummaryDTO } from '@/lib/types/generated/DriverProfileSocialSummaryDTO';
|
||||
import type { DriverProfileExtendedProfileDTO } from '@/lib/types/generated/DriverProfileExtendedProfileDTO';
|
||||
|
||||
describe('DriversViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform DriversLeaderboardDTO to DriversViewData correctly', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
category: 'Elite',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/john.jpg',
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Jane Smith',
|
||||
rating: 1100.75,
|
||||
skillLevel: 'Advanced',
|
||||
category: 'Pro',
|
||||
nationality: 'Canada',
|
||||
racesCompleted: 120,
|
||||
wins: 15,
|
||||
podiums: 45,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'https://example.com/jane.jpg',
|
||||
},
|
||||
],
|
||||
totalRaces: 270,
|
||||
totalWins: 40,
|
||||
activeCount: 2,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.drivers[0].id).toBe('driver-1');
|
||||
expect(result.drivers[0].name).toBe('John Doe');
|
||||
expect(result.drivers[0].rating).toBe(1234.56);
|
||||
expect(result.drivers[0].ratingLabel).toBe('1,235');
|
||||
expect(result.drivers[0].skillLevel).toBe('Pro');
|
||||
expect(result.drivers[0].category).toBe('Elite');
|
||||
expect(result.drivers[0].nationality).toBe('USA');
|
||||
expect(result.drivers[0].racesCompleted).toBe(150);
|
||||
expect(result.drivers[0].wins).toBe(25);
|
||||
expect(result.drivers[0].podiums).toBe(60);
|
||||
expect(result.drivers[0].isActive).toBe(true);
|
||||
expect(result.drivers[0].rank).toBe(1);
|
||||
expect(result.drivers[0].avatarUrl).toBe('https://example.com/john.jpg');
|
||||
|
||||
expect(result.drivers[1].id).toBe('driver-2');
|
||||
expect(result.drivers[1].name).toBe('Jane Smith');
|
||||
expect(result.drivers[1].rating).toBe(1100.75);
|
||||
expect(result.drivers[1].ratingLabel).toBe('1,101');
|
||||
expect(result.drivers[1].skillLevel).toBe('Advanced');
|
||||
expect(result.drivers[1].category).toBe('Pro');
|
||||
expect(result.drivers[1].nationality).toBe('Canada');
|
||||
expect(result.drivers[1].racesCompleted).toBe(120);
|
||||
expect(result.drivers[1].wins).toBe(15);
|
||||
expect(result.drivers[1].podiums).toBe(45);
|
||||
expect(result.drivers[1].isActive).toBe(true);
|
||||
expect(result.drivers[1].rank).toBe(2);
|
||||
expect(result.drivers[1].avatarUrl).toBe('https://example.com/jane.jpg');
|
||||
|
||||
expect(result.totalRaces).toBe(270);
|
||||
expect(result.totalRacesLabel).toBe('270');
|
||||
expect(result.totalWins).toBe(40);
|
||||
expect(result.totalWinsLabel).toBe('40');
|
||||
expect(result.activeCount).toBe(2);
|
||||
expect(result.activeCountLabel).toBe('2');
|
||||
expect(result.totalDriversLabel).toBe('2');
|
||||
});
|
||||
|
||||
it('should handle drivers with missing optional fields', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].category).toBeUndefined();
|
||||
expect(result.drivers[0].avatarUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty drivers array', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [],
|
||||
totalRaces: 0,
|
||||
totalWins: 0,
|
||||
activeCount: 0,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers).toEqual([]);
|
||||
expect(result.totalRaces).toBe(0);
|
||||
expect(result.totalRacesLabel).toBe('0');
|
||||
expect(result.totalWins).toBe(0);
|
||||
expect(result.totalWinsLabel).toBe('0');
|
||||
expect(result.activeCount).toBe(0);
|
||||
expect(result.activeCountLabel).toBe('0');
|
||||
expect(result.totalDriversLabel).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
category: 'Elite',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/john.jpg',
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].name).toBe(driversDTO.drivers[0].name);
|
||||
expect(result.drivers[0].nationality).toBe(driversDTO.drivers[0].nationality);
|
||||
expect(result.drivers[0].skillLevel).toBe(driversDTO.drivers[0].skillLevel);
|
||||
expect(result.totalRaces).toBe(driversDTO.totalRaces);
|
||||
expect(result.totalWins).toBe(driversDTO.totalWins);
|
||||
expect(result.activeCount).toBe(driversDTO.activeCount);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
category: 'Elite',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/john.jpg',
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const originalDTO = JSON.parse(JSON.stringify(driversDTO));
|
||||
DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(driversDTO).toEqual(originalDTO);
|
||||
});
|
||||
|
||||
it('should transform all numeric fields to formatted strings where appropriate', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
// Rating label should be a formatted string
|
||||
expect(typeof result.drivers[0].ratingLabel).toBe('string');
|
||||
expect(result.drivers[0].ratingLabel).toBe('1,235');
|
||||
|
||||
// Total counts should be formatted strings
|
||||
expect(typeof result.totalRacesLabel).toBe('string');
|
||||
expect(result.totalRacesLabel).toBe('150');
|
||||
expect(typeof result.totalWinsLabel).toBe('string');
|
||||
expect(result.totalWinsLabel).toBe('25');
|
||||
expect(typeof result.activeCountLabel).toBe('string');
|
||||
expect(result.activeCountLabel).toBe('1');
|
||||
expect(typeof result.totalDriversLabel).toBe('string');
|
||||
expect(result.totalDriversLabel).toBe('1');
|
||||
});
|
||||
|
||||
it('should handle large numbers correctly', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 999999.99,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 10000,
|
||||
wins: 2500,
|
||||
podiums: 5000,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 10000,
|
||||
totalWins: 2500,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].ratingLabel).toBe('1,000,000');
|
||||
expect(result.totalRacesLabel).toBe('10,000');
|
||||
expect(result.totalWinsLabel).toBe('2,500');
|
||||
expect(result.activeCountLabel).toBe('1');
|
||||
expect(result.totalDriversLabel).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle null/undefined rating', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 0,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].ratingLabel).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle drivers with no category', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].category).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle inactive drivers', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: false,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 0,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].isActive).toBe(false);
|
||||
expect(result.activeCount).toBe(0);
|
||||
expect(result.activeCountLabel).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('derived fields', () => {
|
||||
it('should correctly calculate total drivers label', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{ id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 },
|
||||
{ id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 },
|
||||
{ id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 },
|
||||
],
|
||||
totalRaces: 350,
|
||||
totalWins: 45,
|
||||
activeCount: 2,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.totalDriversLabel).toBe('3');
|
||||
});
|
||||
|
||||
it('should correctly calculate active count', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{ id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 },
|
||||
{ id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 },
|
||||
{ id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 },
|
||||
],
|
||||
totalRaces: 350,
|
||||
totalWins: 45,
|
||||
activeCount: 2,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.activeCount).toBe(2);
|
||||
expect(result.activeCountLabel).toBe('2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rating formatting', () => {
|
||||
it('should format ratings with thousands separators', () => {
|
||||
expect(RatingDisplay.format(1234.56)).toBe('1,235');
|
||||
expect(RatingDisplay.format(9999.99)).toBe('10,000');
|
||||
expect(RatingDisplay.format(100000.5)).toBe('100,001');
|
||||
});
|
||||
|
||||
it('should handle null/undefined ratings', () => {
|
||||
expect(RatingDisplay.format(null)).toBe('—');
|
||||
expect(RatingDisplay.format(undefined)).toBe('—');
|
||||
});
|
||||
|
||||
it('should round ratings correctly', () => {
|
||||
expect(RatingDisplay.format(1234.4)).toBe('1,234');
|
||||
expect(RatingDisplay.format(1234.6)).toBe('1,235');
|
||||
expect(RatingDisplay.format(1234.5)).toBe('1,235');
|
||||
});
|
||||
});
|
||||
|
||||
describe('number formatting', () => {
|
||||
it('should format numbers with thousands separators', () => {
|
||||
expect(NumberDisplay.format(1234567)).toBe('1,234,567');
|
||||
expect(NumberDisplay.format(1000)).toBe('1,000');
|
||||
expect(NumberDisplay.format(999)).toBe('999');
|
||||
});
|
||||
|
||||
it('should handle decimal numbers', () => {
|
||||
expect(NumberDisplay.format(1234.567)).toBe('1,234.567');
|
||||
expect(NumberDisplay.format(1000.5)).toBe('1,000.5');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DriverProfileViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
@@ -1643,531 +1193,4 @@ describe('DriverProfileViewDataBuilder', () => {
|
||||
expect(result.socialSummary.friends).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('date formatting', () => {
|
||||
it('should format dates correctly', () => {
|
||||
expect(DateDisplay.formatShort('2024-01-15T00:00:00Z')).toBe('Jan 15, 2024');
|
||||
expect(DateDisplay.formatMonthYear('2024-01-15T00:00:00Z')).toBe('Jan 2024');
|
||||
expect(DateDisplay.formatShort('2024-12-25T00:00:00Z')).toBe('Dec 25, 2024');
|
||||
expect(DateDisplay.formatMonthYear('2024-12-25T00:00:00Z')).toBe('Dec 2024');
|
||||
});
|
||||
});
|
||||
|
||||
describe('finish position formatting', () => {
|
||||
it('should format finish positions correctly', () => {
|
||||
expect(FinishDisplay.format(1)).toBe('P1');
|
||||
expect(FinishDisplay.format(5)).toBe('P5');
|
||||
expect(FinishDisplay.format(10)).toBe('P10');
|
||||
expect(FinishDisplay.format(100)).toBe('P100');
|
||||
});
|
||||
|
||||
it('should handle null/undefined finish positions', () => {
|
||||
expect(FinishDisplay.format(null)).toBe('—');
|
||||
expect(FinishDisplay.format(undefined)).toBe('—');
|
||||
});
|
||||
|
||||
it('should format average finish positions correctly', () => {
|
||||
expect(FinishDisplay.formatAverage(5.4)).toBe('P5.4');
|
||||
expect(FinishDisplay.formatAverage(1.5)).toBe('P1.5');
|
||||
expect(FinishDisplay.formatAverage(10.0)).toBe('P10.0');
|
||||
});
|
||||
|
||||
it('should handle null/undefined average finish positions', () => {
|
||||
expect(FinishDisplay.formatAverage(null)).toBe('—');
|
||||
expect(FinishDisplay.formatAverage(undefined)).toBe('—');
|
||||
});
|
||||
});
|
||||
|
||||
describe('percentage formatting', () => {
|
||||
it('should format percentages correctly', () => {
|
||||
expect(PercentDisplay.format(0.1234)).toBe('12.3%');
|
||||
expect(PercentDisplay.format(0.5)).toBe('50.0%');
|
||||
expect(PercentDisplay.format(1.0)).toBe('100.0%');
|
||||
});
|
||||
|
||||
it('should handle null/undefined percentages', () => {
|
||||
expect(PercentDisplay.format(null)).toBe('0.0%');
|
||||
expect(PercentDisplay.format(undefined)).toBe('0.0%');
|
||||
});
|
||||
|
||||
it('should format whole percentages correctly', () => {
|
||||
expect(PercentDisplay.formatWhole(85)).toBe('85%');
|
||||
expect(PercentDisplay.formatWhole(50)).toBe('50%');
|
||||
expect(PercentDisplay.formatWhole(100)).toBe('100%');
|
||||
});
|
||||
|
||||
it('should handle null/undefined whole percentages', () => {
|
||||
expect(PercentDisplay.formatWhole(null)).toBe('0%');
|
||||
expect(PercentDisplay.formatWhole(undefined)).toBe('0%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-component consistency', () => {
|
||||
it('should all use consistent formatting for numeric values', () => {
|
||||
const profileDTO: GetDriverProfileOutputDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
joinedAt: '2024-01-15T00:00:00Z',
|
||||
rating: 1234.56,
|
||||
globalRank: 42,
|
||||
consistency: 85,
|
||||
},
|
||||
stats: {
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
dnfs: 10,
|
||||
avgFinish: 5.4,
|
||||
bestFinish: 1,
|
||||
worstFinish: 25,
|
||||
finishRate: 0.933,
|
||||
winRate: 0.167,
|
||||
podiumRate: 0.4,
|
||||
percentile: 95,
|
||||
rating: 1234.56,
|
||||
consistency: 85,
|
||||
overallRank: 42,
|
||||
},
|
||||
finishDistribution: {
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
topTen: 100,
|
||||
dnfs: 10,
|
||||
other: 55,
|
||||
},
|
||||
teamMemberships: [],
|
||||
socialSummary: {
|
||||
friendsCount: 0,
|
||||
friends: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = DriverProfileViewDataBuilder.build(profileDTO);
|
||||
|
||||
// All numeric values should be formatted as strings
|
||||
expect(typeof result.currentDriver?.ratingLabel).toBe('string');
|
||||
expect(typeof result.currentDriver?.globalRankLabel).toBe('string');
|
||||
expect(typeof result.stats?.totalRacesLabel).toBe('string');
|
||||
expect(typeof result.stats?.winsLabel).toBe('string');
|
||||
expect(typeof result.stats?.podiumsLabel).toBe('string');
|
||||
expect(typeof result.stats?.dnfsLabel).toBe('string');
|
||||
expect(typeof result.stats?.avgFinishLabel).toBe('string');
|
||||
expect(typeof result.stats?.bestFinishLabel).toBe('string');
|
||||
expect(typeof result.stats?.worstFinishLabel).toBe('string');
|
||||
expect(typeof result.stats?.ratingLabel).toBe('string');
|
||||
expect(typeof result.stats?.consistencyLabel).toBe('string');
|
||||
});
|
||||
|
||||
it('should all handle missing data gracefully', () => {
|
||||
const profileDTO: GetDriverProfileOutputDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
joinedAt: '2024-01-15T00:00:00Z',
|
||||
},
|
||||
stats: {
|
||||
totalRaces: 0,
|
||||
wins: 0,
|
||||
podiums: 0,
|
||||
dnfs: 0,
|
||||
},
|
||||
finishDistribution: {
|
||||
totalRaces: 0,
|
||||
wins: 0,
|
||||
podiums: 0,
|
||||
topTen: 0,
|
||||
dnfs: 0,
|
||||
other: 0,
|
||||
},
|
||||
teamMemberships: [],
|
||||
socialSummary: {
|
||||
friendsCount: 0,
|
||||
friends: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = DriverProfileViewDataBuilder.build(profileDTO);
|
||||
|
||||
// All fields should have safe defaults
|
||||
expect(result.currentDriver?.avatarUrl).toBe('');
|
||||
expect(result.currentDriver?.iracingId).toBeNull();
|
||||
expect(result.currentDriver?.rating).toBeNull();
|
||||
expect(result.currentDriver?.ratingLabel).toBe('—');
|
||||
expect(result.currentDriver?.globalRank).toBeNull();
|
||||
expect(result.currentDriver?.globalRankLabel).toBe('—');
|
||||
expect(result.currentDriver?.consistency).toBeNull();
|
||||
expect(result.currentDriver?.bio).toBeNull();
|
||||
expect(result.currentDriver?.totalDrivers).toBeNull();
|
||||
expect(result.stats?.avgFinish).toBeNull();
|
||||
expect(result.stats?.avgFinishLabel).toBe('—');
|
||||
expect(result.stats?.bestFinish).toBeNull();
|
||||
expect(result.stats?.bestFinishLabel).toBe('—');
|
||||
expect(result.stats?.worstFinish).toBeNull();
|
||||
expect(result.stats?.worstFinishLabel).toBe('—');
|
||||
expect(result.stats?.finishRate).toBeNull();
|
||||
expect(result.stats?.winRate).toBeNull();
|
||||
expect(result.stats?.podiumRate).toBeNull();
|
||||
expect(result.stats?.percentile).toBeNull();
|
||||
expect(result.stats?.rating).toBeNull();
|
||||
expect(result.stats?.ratingLabel).toBe('—');
|
||||
expect(result.stats?.consistency).toBeNull();
|
||||
expect(result.stats?.consistencyLabel).toBe('0%');
|
||||
expect(result.stats?.overallRank).toBeNull();
|
||||
expect(result.finishDistribution).not.toBeNull();
|
||||
expect(result.teamMemberships).toEqual([]);
|
||||
expect(result.socialSummary.friends).toEqual([]);
|
||||
expect(result.extendedProfile).toBeNull();
|
||||
});
|
||||
|
||||
it('should all preserve ISO timestamps for serialization', () => {
|
||||
const profileDTO: GetDriverProfileOutputDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
joinedAt: '2024-01-15T00:00:00Z',
|
||||
},
|
||||
stats: {
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
dnfs: 10,
|
||||
},
|
||||
finishDistribution: {
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
topTen: 100,
|
||||
dnfs: 10,
|
||||
other: 55,
|
||||
},
|
||||
teamMemberships: [
|
||||
{
|
||||
teamId: 'team-1',
|
||||
teamName: 'Elite Racing',
|
||||
teamTag: 'ER',
|
||||
role: 'Driver',
|
||||
joinedAt: '2024-01-15T00:00:00Z',
|
||||
isCurrent: true,
|
||||
},
|
||||
],
|
||||
socialSummary: {
|
||||
friendsCount: 0,
|
||||
friends: [],
|
||||
},
|
||||
extendedProfile: {
|
||||
socialHandles: [],
|
||||
achievements: [
|
||||
{
|
||||
id: 'ach-1',
|
||||
title: 'Champion',
|
||||
description: 'Won the championship',
|
||||
icon: 'trophy',
|
||||
rarity: 'Legendary',
|
||||
earnedAt: '2024-01-15T00:00:00Z',
|
||||
},
|
||||
],
|
||||
racingStyle: 'Aggressive',
|
||||
favoriteTrack: 'Spa',
|
||||
favoriteCar: 'Porsche 911 GT3',
|
||||
timezone: 'America/New_York',
|
||||
availableHours: 'Evenings',
|
||||
lookingForTeam: false,
|
||||
openToRequests: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = DriverProfileViewDataBuilder.build(profileDTO);
|
||||
|
||||
// All timestamps should be preserved as ISO strings
|
||||
expect(result.currentDriver?.joinedAt).toBe('2024-01-15T00:00:00Z');
|
||||
expect(result.teamMemberships[0].joinedAt).toBe('2024-01-15T00:00:00Z');
|
||||
expect(result.extendedProfile?.achievements[0].earnedAt).toBe('2024-01-15T00:00:00Z');
|
||||
});
|
||||
|
||||
it('should all handle boolean flags correctly', () => {
|
||||
const profileDTO: GetDriverProfileOutputDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
joinedAt: '2024-01-15T00:00:00Z',
|
||||
},
|
||||
stats: {
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
dnfs: 10,
|
||||
},
|
||||
finishDistribution: {
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
topTen: 100,
|
||||
dnfs: 10,
|
||||
other: 55,
|
||||
},
|
||||
teamMemberships: [
|
||||
{
|
||||
teamId: 'team-1',
|
||||
teamName: 'Elite Racing',
|
||||
teamTag: 'ER',
|
||||
role: 'Driver',
|
||||
joinedAt: '2024-01-15T00:00:00Z',
|
||||
isCurrent: true,
|
||||
},
|
||||
{
|
||||
teamId: 'team-2',
|
||||
teamName: 'Old Team',
|
||||
teamTag: 'OT',
|
||||
role: 'Driver',
|
||||
joinedAt: '2023-01-15T00:00:00Z',
|
||||
isCurrent: false,
|
||||
},
|
||||
],
|
||||
socialSummary: {
|
||||
friendsCount: 0,
|
||||
friends: [],
|
||||
},
|
||||
extendedProfile: {
|
||||
socialHandles: [],
|
||||
achievements: [],
|
||||
racingStyle: 'Aggressive',
|
||||
favoriteTrack: 'Spa',
|
||||
favoriteCar: 'Porsche 911 GT3',
|
||||
timezone: 'America/New_York',
|
||||
availableHours: 'Evenings',
|
||||
lookingForTeam: true,
|
||||
openToRequests: false,
|
||||
},
|
||||
};
|
||||
|
||||
const result = DriverProfileViewDataBuilder.build(profileDTO);
|
||||
|
||||
expect(result.teamMemberships[0].isCurrent).toBe(true);
|
||||
expect(result.teamMemberships[1].isCurrent).toBe(false);
|
||||
expect(result.extendedProfile?.lookingForTeam).toBe(true);
|
||||
expect(result.extendedProfile?.openToRequests).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data integrity', () => {
|
||||
it('should maintain data consistency across transformations', () => {
|
||||
const profileDTO: GetDriverProfileOutputDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
iracingId: '12345',
|
||||
joinedAt: '2024-01-15T00:00:00Z',
|
||||
rating: 1234.56,
|
||||
globalRank: 42,
|
||||
consistency: 85,
|
||||
bio: 'Professional sim racer.',
|
||||
totalDrivers: 1000,
|
||||
},
|
||||
stats: {
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
dnfs: 10,
|
||||
avgFinish: 5.4,
|
||||
bestFinish: 1,
|
||||
worstFinish: 25,
|
||||
finishRate: 0.933,
|
||||
winRate: 0.167,
|
||||
podiumRate: 0.4,
|
||||
percentile: 95,
|
||||
rating: 1234.56,
|
||||
consistency: 85,
|
||||
overallRank: 42,
|
||||
},
|
||||
finishDistribution: {
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
topTen: 100,
|
||||
dnfs: 10,
|
||||
other: 55,
|
||||
},
|
||||
teamMemberships: [
|
||||
{
|
||||
teamId: 'team-1',
|
||||
teamName: 'Elite Racing',
|
||||
teamTag: 'ER',
|
||||
role: 'Driver',
|
||||
joinedAt: '2024-01-15T00:00:00Z',
|
||||
isCurrent: true,
|
||||
},
|
||||
],
|
||||
socialSummary: {
|
||||
friendsCount: 2,
|
||||
friends: [
|
||||
{ id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' },
|
||||
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
|
||||
],
|
||||
},
|
||||
extendedProfile: {
|
||||
socialHandles: [
|
||||
{ platform: 'Twitter', handle: '@johndoe', url: 'https://twitter.com/johndoe' },
|
||||
],
|
||||
achievements: [
|
||||
{ id: 'ach-1', title: 'Champion', description: 'Won the championship', icon: 'trophy', rarity: 'Legendary', earnedAt: '2024-01-15T00:00:00Z' },
|
||||
],
|
||||
racingStyle: 'Aggressive',
|
||||
favoriteTrack: 'Spa',
|
||||
favoriteCar: 'Porsche 911 GT3',
|
||||
timezone: 'America/New_York',
|
||||
availableHours: 'Evenings',
|
||||
lookingForTeam: false,
|
||||
openToRequests: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = DriverProfileViewDataBuilder.build(profileDTO);
|
||||
|
||||
// Verify derived fields match their source data
|
||||
expect(result.socialSummary.friendsCount).toBe(profileDTO.socialSummary.friends.length);
|
||||
expect(result.teamMemberships.length).toBe(profileDTO.teamMemberships.length);
|
||||
expect(result.extendedProfile?.achievements.length).toBe(profileDTO.extendedProfile?.achievements.length);
|
||||
});
|
||||
|
||||
it('should handle complex real-world scenarios', () => {
|
||||
const profileDTO: GetDriverProfileOutputDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
iracingId: '12345',
|
||||
joinedAt: '2024-01-15T00:00:00Z',
|
||||
rating: 2456.78,
|
||||
globalRank: 15,
|
||||
consistency: 92.5,
|
||||
bio: 'Professional sim racer with 5 years of experience. Specializes in GT3 racing.',
|
||||
totalDrivers: 1000,
|
||||
},
|
||||
stats: {
|
||||
totalRaces: 250,
|
||||
wins: 45,
|
||||
podiums: 120,
|
||||
dnfs: 15,
|
||||
avgFinish: 4.2,
|
||||
bestFinish: 1,
|
||||
worstFinish: 30,
|
||||
finishRate: 0.94,
|
||||
winRate: 0.18,
|
||||
podiumRate: 0.48,
|
||||
percentile: 98,
|
||||
rating: 2456.78,
|
||||
consistency: 92.5,
|
||||
overallRank: 15,
|
||||
},
|
||||
finishDistribution: {
|
||||
totalRaces: 250,
|
||||
wins: 45,
|
||||
podiums: 120,
|
||||
topTen: 180,
|
||||
dnfs: 15,
|
||||
other: 55,
|
||||
},
|
||||
teamMemberships: [
|
||||
{
|
||||
teamId: 'team-1',
|
||||
teamName: 'Elite Racing',
|
||||
teamTag: 'ER',
|
||||
role: 'Driver',
|
||||
joinedAt: '2024-01-15T00:00:00Z',
|
||||
isCurrent: true,
|
||||
},
|
||||
{
|
||||
teamId: 'team-2',
|
||||
teamName: 'Pro Team',
|
||||
teamTag: 'PT',
|
||||
role: 'Reserve Driver',
|
||||
joinedAt: '2023-06-15T00:00:00Z',
|
||||
isCurrent: false,
|
||||
},
|
||||
],
|
||||
socialSummary: {
|
||||
friendsCount: 50,
|
||||
friends: [
|
||||
{ id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' },
|
||||
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
|
||||
{ id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' },
|
||||
],
|
||||
},
|
||||
extendedProfile: {
|
||||
socialHandles: [
|
||||
{ platform: 'Twitter', handle: '@johndoe', url: 'https://twitter.com/johndoe' },
|
||||
{ platform: 'Discord', handle: 'johndoe#1234', url: '' },
|
||||
],
|
||||
achievements: [
|
||||
{ id: 'ach-1', title: 'Champion', description: 'Won the championship', icon: 'trophy', rarity: 'Legendary', earnedAt: '2024-01-15T00:00:00Z' },
|
||||
{ id: 'ach-2', title: 'Podium Finisher', description: 'Finished on podium 100 times', icon: 'medal', rarity: 'Rare', earnedAt: '2023-12-01T00:00:00Z' },
|
||||
],
|
||||
racingStyle: 'Aggressive',
|
||||
favoriteTrack: 'Spa',
|
||||
favoriteCar: 'Porsche 911 GT3',
|
||||
timezone: 'America/New_York',
|
||||
availableHours: 'Evenings and Weekends',
|
||||
lookingForTeam: false,
|
||||
openToRequests: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = DriverProfileViewDataBuilder.build(profileDTO);
|
||||
|
||||
// Verify all transformations
|
||||
expect(result.currentDriver?.name).toBe('John Doe');
|
||||
expect(result.currentDriver?.ratingLabel).toBe('2,457');
|
||||
expect(result.currentDriver?.globalRankLabel).toBe('#15');
|
||||
expect(result.currentDriver?.consistency).toBe(92.5);
|
||||
expect(result.currentDriver?.bio).toBe('Professional sim racer with 5 years of experience. Specializes in GT3 racing.');
|
||||
|
||||
expect(result.stats?.totalRacesLabel).toBe('250');
|
||||
expect(result.stats?.winsLabel).toBe('45');
|
||||
expect(result.stats?.podiumsLabel).toBe('120');
|
||||
expect(result.stats?.dnfsLabel).toBe('15');
|
||||
expect(result.stats?.avgFinishLabel).toBe('P4.2');
|
||||
expect(result.stats?.bestFinishLabel).toBe('P1');
|
||||
expect(result.stats?.worstFinishLabel).toBe('P30');
|
||||
expect(result.stats?.finishRate).toBe(0.94);
|
||||
expect(result.stats?.winRate).toBe(0.18);
|
||||
expect(result.stats?.podiumRate).toBe(0.48);
|
||||
expect(result.stats?.percentile).toBe(98);
|
||||
expect(result.stats?.ratingLabel).toBe('2,457');
|
||||
expect(result.stats?.consistencyLabel).toBe('93%');
|
||||
expect(result.stats?.overallRank).toBe(15);
|
||||
|
||||
expect(result.finishDistribution?.totalRaces).toBe(250);
|
||||
expect(result.finishDistribution?.wins).toBe(45);
|
||||
expect(result.finishDistribution?.podiums).toBe(120);
|
||||
expect(result.finishDistribution?.topTen).toBe(180);
|
||||
expect(result.finishDistribution?.dnfs).toBe(15);
|
||||
expect(result.finishDistribution?.other).toBe(55);
|
||||
|
||||
expect(result.teamMemberships).toHaveLength(2);
|
||||
expect(result.teamMemberships[0].isCurrent).toBe(true);
|
||||
expect(result.teamMemberships[1].isCurrent).toBe(false);
|
||||
|
||||
expect(result.socialSummary.friendsCount).toBe(50);
|
||||
expect(result.socialSummary.friends).toHaveLength(3);
|
||||
expect(result.socialSummary.friends[0].avatarUrl).toBe('https://example.com/alice.jpg');
|
||||
expect(result.socialSummary.friends[1].avatarUrl).toBe('');
|
||||
expect(result.socialSummary.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg');
|
||||
|
||||
expect(result.extendedProfile?.socialHandles).toHaveLength(2);
|
||||
expect(result.extendedProfile?.achievements).toHaveLength(2);
|
||||
expect(result.extendedProfile?.achievements[0].rarityLabel).toBe('Legendary');
|
||||
expect(result.extendedProfile?.achievements[1].rarityLabel).toBe('Rare');
|
||||
expect(result.extendedProfile?.lookingForTeam).toBe(false);
|
||||
expect(result.extendedProfile?.openToRequests).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,441 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DriverRankingsViewDataBuilder } from './DriverRankingsViewDataBuilder';
|
||||
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
|
||||
|
||||
describe('DriverRankingsViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform DriverLeaderboardItemDTO array to DriverRankingsViewData correctly', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar1.jpg',
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Jane Smith',
|
||||
rating: 1100.0,
|
||||
skillLevel: 'advanced',
|
||||
nationality: 'Canada',
|
||||
racesCompleted: 100,
|
||||
wins: 15,
|
||||
podiums: 40,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'https://example.com/avatar2.jpg',
|
||||
},
|
||||
{
|
||||
id: 'driver-3',
|
||||
name: 'Bob Johnson',
|
||||
rating: 950.0,
|
||||
skillLevel: 'intermediate',
|
||||
nationality: 'UK',
|
||||
racesCompleted: 80,
|
||||
wins: 10,
|
||||
podiums: 30,
|
||||
isActive: true,
|
||||
rank: 3,
|
||||
avatarUrl: 'https://example.com/avatar3.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
// Verify drivers
|
||||
expect(result.drivers).toHaveLength(3);
|
||||
expect(result.drivers[0].id).toBe('driver-1');
|
||||
expect(result.drivers[0].name).toBe('John Doe');
|
||||
expect(result.drivers[0].rating).toBe(1234.56);
|
||||
expect(result.drivers[0].skillLevel).toBe('pro');
|
||||
expect(result.drivers[0].nationality).toBe('USA');
|
||||
expect(result.drivers[0].racesCompleted).toBe(150);
|
||||
expect(result.drivers[0].wins).toBe(25);
|
||||
expect(result.drivers[0].podiums).toBe(60);
|
||||
expect(result.drivers[0].rank).toBe(1);
|
||||
expect(result.drivers[0].avatarUrl).toBe('https://example.com/avatar1.jpg');
|
||||
expect(result.drivers[0].winRate).toBe('16.7');
|
||||
expect(result.drivers[0].medalBg).toBe('bg-warning-amber');
|
||||
expect(result.drivers[0].medalColor).toBe('text-warning-amber');
|
||||
|
||||
// Verify podium (top 3 with special ordering: 2nd, 1st, 3rd)
|
||||
expect(result.podium).toHaveLength(3);
|
||||
expect(result.podium[0].id).toBe('driver-1');
|
||||
expect(result.podium[0].name).toBe('John Doe');
|
||||
expect(result.podium[0].rating).toBe(1234.56);
|
||||
expect(result.podium[0].wins).toBe(25);
|
||||
expect(result.podium[0].podiums).toBe(60);
|
||||
expect(result.podium[0].avatarUrl).toBe('https://example.com/avatar1.jpg');
|
||||
expect(result.podium[0].position).toBe(2); // 2nd place
|
||||
|
||||
expect(result.podium[1].id).toBe('driver-2');
|
||||
expect(result.podium[1].position).toBe(1); // 1st place
|
||||
|
||||
expect(result.podium[2].id).toBe('driver-3');
|
||||
expect(result.podium[2].position).toBe(3); // 3rd place
|
||||
|
||||
// Verify default values
|
||||
expect(result.searchQuery).toBe('');
|
||||
expect(result.selectedSkill).toBe('all');
|
||||
expect(result.sortBy).toBe('rank');
|
||||
expect(result.showFilters).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty driver array', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers).toEqual([]);
|
||||
expect(result.podium).toEqual([]);
|
||||
expect(result.searchQuery).toBe('');
|
||||
expect(result.selectedSkill).toBe('all');
|
||||
expect(result.sortBy).toBe('rank');
|
||||
expect(result.showFilters).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle less than 3 drivers for podium', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar1.jpg',
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Jane Smith',
|
||||
rating: 1100.0,
|
||||
skillLevel: 'advanced',
|
||||
nationality: 'Canada',
|
||||
racesCompleted: 100,
|
||||
wins: 15,
|
||||
podiums: 40,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'https://example.com/avatar2.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.podium).toHaveLength(2);
|
||||
expect(result.podium[0].position).toBe(2); // 2nd place
|
||||
expect(result.podium[1].position).toBe(1); // 1st place
|
||||
});
|
||||
|
||||
it('should handle missing avatar URLs with empty string fallback', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers[0].avatarUrl).toBe('');
|
||||
expect(result.podium[0].avatarUrl).toBe('');
|
||||
});
|
||||
|
||||
it('should calculate win rate correctly', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 100,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Jane Smith',
|
||||
rating: 1100.0,
|
||||
skillLevel: 'advanced',
|
||||
nationality: 'Canada',
|
||||
racesCompleted: 50,
|
||||
wins: 10,
|
||||
podiums: 25,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
},
|
||||
{
|
||||
id: 'driver-3',
|
||||
name: 'Bob Johnson',
|
||||
rating: 950.0,
|
||||
skillLevel: 'intermediate',
|
||||
nationality: 'UK',
|
||||
racesCompleted: 0,
|
||||
wins: 0,
|
||||
podiums: 0,
|
||||
isActive: true,
|
||||
rank: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers[0].winRate).toBe('25.0');
|
||||
expect(result.drivers[1].winRate).toBe('20.0');
|
||||
expect(result.drivers[2].winRate).toBe('0.0');
|
||||
});
|
||||
|
||||
it('should assign correct medal colors based on position', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Jane Smith',
|
||||
rating: 1100.0,
|
||||
skillLevel: 'advanced',
|
||||
nationality: 'Canada',
|
||||
racesCompleted: 100,
|
||||
wins: 15,
|
||||
podiums: 40,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
},
|
||||
{
|
||||
id: 'driver-3',
|
||||
name: 'Bob Johnson',
|
||||
rating: 950.0,
|
||||
skillLevel: 'intermediate',
|
||||
nationality: 'UK',
|
||||
racesCompleted: 80,
|
||||
wins: 10,
|
||||
podiums: 30,
|
||||
isActive: true,
|
||||
rank: 3,
|
||||
},
|
||||
{
|
||||
id: 'driver-4',
|
||||
name: 'Alice Brown',
|
||||
rating: 800.0,
|
||||
skillLevel: 'beginner',
|
||||
nationality: 'Germany',
|
||||
racesCompleted: 60,
|
||||
wins: 5,
|
||||
podiums: 15,
|
||||
isActive: true,
|
||||
rank: 4,
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers[0].medalBg).toBe('bg-warning-amber');
|
||||
expect(result.drivers[0].medalColor).toBe('text-warning-amber');
|
||||
expect(result.drivers[1].medalBg).toBe('bg-gray-300');
|
||||
expect(result.drivers[1].medalColor).toBe('text-gray-300');
|
||||
expect(result.drivers[2].medalBg).toBe('bg-orange-700');
|
||||
expect(result.drivers[2].medalColor).toBe('text-orange-700');
|
||||
expect(result.drivers[3].medalBg).toBe('bg-gray-800');
|
||||
expect(result.drivers[3].medalColor).toBe('text-gray-400');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers[0].name).toBe(driverDTOs[0].name);
|
||||
expect(result.drivers[0].nationality).toBe(driverDTOs[0].nationality);
|
||||
expect(result.drivers[0].avatarUrl).toBe(driverDTOs[0].avatarUrl);
|
||||
expect(result.drivers[0].skillLevel).toBe(driverDTOs[0].skillLevel);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
const originalDTO = JSON.parse(JSON.stringify(driverDTOs));
|
||||
DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(driverDTOs).toEqual(originalDTO);
|
||||
});
|
||||
|
||||
it('should handle large numbers correctly', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 999999.99,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 10000,
|
||||
wins: 2500,
|
||||
podiums: 5000,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers[0].rating).toBe(999999.99);
|
||||
expect(result.drivers[0].wins).toBe(2500);
|
||||
expect(result.drivers[0].podiums).toBe(5000);
|
||||
expect(result.drivers[0].racesCompleted).toBe(10000);
|
||||
expect(result.drivers[0].winRate).toBe('25.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle null/undefined avatar URLs', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: null as any,
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers[0].avatarUrl).toBe('');
|
||||
expect(result.podium[0].avatarUrl).toBe('');
|
||||
});
|
||||
|
||||
it('should handle null/undefined rating', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: null as any,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers[0].rating).toBeNull();
|
||||
expect(result.podium[0].rating).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle zero races completed for win rate calculation', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 0,
|
||||
wins: 0,
|
||||
podiums: 0,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers[0].winRate).toBe('0.0');
|
||||
});
|
||||
|
||||
it('should handle rank 0', () => {
|
||||
const driverDTOs: DriverLeaderboardItemDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
|
||||
|
||||
expect(result.drivers[0].rank).toBe(0);
|
||||
expect(result.drivers[0].medalBg).toBe('bg-gray-800');
|
||||
expect(result.drivers[0].medalColor).toBe('text-gray-400');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,382 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DriversViewDataBuilder } from './DriversViewDataBuilder';
|
||||
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
|
||||
|
||||
describe('DriversViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform DriversLeaderboardDTO to DriversViewData correctly', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
category: 'Elite',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/john.jpg',
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Jane Smith',
|
||||
rating: 1100.75,
|
||||
skillLevel: 'Advanced',
|
||||
category: 'Pro',
|
||||
nationality: 'Canada',
|
||||
racesCompleted: 120,
|
||||
wins: 15,
|
||||
podiums: 45,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'https://example.com/jane.jpg',
|
||||
},
|
||||
],
|
||||
totalRaces: 270,
|
||||
totalWins: 40,
|
||||
activeCount: 2,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.drivers[0].id).toBe('driver-1');
|
||||
expect(result.drivers[0].name).toBe('John Doe');
|
||||
expect(result.drivers[0].rating).toBe(1234.56);
|
||||
expect(result.drivers[0].ratingLabel).toBe('1,235');
|
||||
expect(result.drivers[0].skillLevel).toBe('Pro');
|
||||
expect(result.drivers[0].category).toBe('Elite');
|
||||
expect(result.drivers[0].nationality).toBe('USA');
|
||||
expect(result.drivers[0].racesCompleted).toBe(150);
|
||||
expect(result.drivers[0].wins).toBe(25);
|
||||
expect(result.drivers[0].podiums).toBe(60);
|
||||
expect(result.drivers[0].isActive).toBe(true);
|
||||
expect(result.drivers[0].rank).toBe(1);
|
||||
expect(result.drivers[0].avatarUrl).toBe('https://example.com/john.jpg');
|
||||
|
||||
expect(result.drivers[1].id).toBe('driver-2');
|
||||
expect(result.drivers[1].name).toBe('Jane Smith');
|
||||
expect(result.drivers[1].rating).toBe(1100.75);
|
||||
expect(result.drivers[1].ratingLabel).toBe('1,101');
|
||||
expect(result.drivers[1].skillLevel).toBe('Advanced');
|
||||
expect(result.drivers[1].category).toBe('Pro');
|
||||
expect(result.drivers[1].nationality).toBe('Canada');
|
||||
expect(result.drivers[1].racesCompleted).toBe(120);
|
||||
expect(result.drivers[1].wins).toBe(15);
|
||||
expect(result.drivers[1].podiums).toBe(45);
|
||||
expect(result.drivers[1].isActive).toBe(true);
|
||||
expect(result.drivers[1].rank).toBe(2);
|
||||
expect(result.drivers[1].avatarUrl).toBe('https://example.com/jane.jpg');
|
||||
|
||||
expect(result.totalRaces).toBe(270);
|
||||
expect(result.totalRacesLabel).toBe('270');
|
||||
expect(result.totalWins).toBe(40);
|
||||
expect(result.totalWinsLabel).toBe('40');
|
||||
expect(result.activeCount).toBe(2);
|
||||
expect(result.activeCountLabel).toBe('2');
|
||||
expect(result.totalDriversLabel).toBe('2');
|
||||
});
|
||||
|
||||
it('should handle drivers with missing optional fields', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].category).toBeUndefined();
|
||||
expect(result.drivers[0].avatarUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty drivers array', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [],
|
||||
totalRaces: 0,
|
||||
totalWins: 0,
|
||||
activeCount: 0,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers).toEqual([]);
|
||||
expect(result.totalRaces).toBe(0);
|
||||
expect(result.totalRacesLabel).toBe('0');
|
||||
expect(result.totalWins).toBe(0);
|
||||
expect(result.totalWinsLabel).toBe('0');
|
||||
expect(result.activeCount).toBe(0);
|
||||
expect(result.activeCountLabel).toBe('0');
|
||||
expect(result.totalDriversLabel).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
category: 'Elite',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/john.jpg',
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].name).toBe(driversDTO.drivers[0].name);
|
||||
expect(result.drivers[0].nationality).toBe(driversDTO.drivers[0].nationality);
|
||||
expect(result.drivers[0].skillLevel).toBe(driversDTO.drivers[0].skillLevel);
|
||||
expect(result.totalRaces).toBe(driversDTO.totalRaces);
|
||||
expect(result.totalWins).toBe(driversDTO.totalWins);
|
||||
expect(result.activeCount).toBe(driversDTO.activeCount);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
category: 'Elite',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/john.jpg',
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const originalDTO = JSON.parse(JSON.stringify(driversDTO));
|
||||
DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(driversDTO).toEqual(originalDTO);
|
||||
});
|
||||
|
||||
it('should transform all numeric fields to formatted strings where appropriate', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
// Rating label should be a formatted string
|
||||
expect(typeof result.drivers[0].ratingLabel).toBe('string');
|
||||
expect(result.drivers[0].ratingLabel).toBe('1,235');
|
||||
|
||||
// Total counts should be formatted strings
|
||||
expect(typeof result.totalRacesLabel).toBe('string');
|
||||
expect(result.totalRacesLabel).toBe('150');
|
||||
expect(typeof result.totalWinsLabel).toBe('string');
|
||||
expect(result.totalWinsLabel).toBe('25');
|
||||
expect(typeof result.activeCountLabel).toBe('string');
|
||||
expect(result.activeCountLabel).toBe('1');
|
||||
expect(typeof result.totalDriversLabel).toBe('string');
|
||||
expect(result.totalDriversLabel).toBe('1');
|
||||
});
|
||||
|
||||
it('should handle large numbers correctly', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 999999.99,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 10000,
|
||||
wins: 2500,
|
||||
podiums: 5000,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 10000,
|
||||
totalWins: 2500,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].ratingLabel).toBe('1,000,000');
|
||||
expect(result.totalRacesLabel).toBe('10,000');
|
||||
expect(result.totalWinsLabel).toBe('2,500');
|
||||
expect(result.activeCountLabel).toBe('1');
|
||||
expect(result.totalDriversLabel).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle null/undefined rating', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 0,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].ratingLabel).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle drivers with no category', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].category).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle inactive drivers', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: false,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 0,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.drivers[0].isActive).toBe(false);
|
||||
expect(result.activeCount).toBe(0);
|
||||
expect(result.activeCountLabel).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('derived fields', () => {
|
||||
it('should correctly calculate total drivers label', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{ id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 },
|
||||
{ id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 },
|
||||
{ id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 },
|
||||
],
|
||||
totalRaces: 350,
|
||||
totalWins: 45,
|
||||
activeCount: 2,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.totalDriversLabel).toBe('3');
|
||||
});
|
||||
|
||||
it('should correctly calculate active count', () => {
|
||||
const driversDTO: DriversLeaderboardDTO = {
|
||||
drivers: [
|
||||
{ id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 },
|
||||
{ id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 },
|
||||
{ id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 },
|
||||
],
|
||||
totalRaces: 350,
|
||||
totalWins: 45,
|
||||
activeCount: 2,
|
||||
};
|
||||
|
||||
const result = DriversViewDataBuilder.build(driversDTO);
|
||||
|
||||
expect(result.activeCount).toBe(2);
|
||||
expect(result.activeCountLabel).toBe('2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ForgotPasswordViewDataBuilder } from './ForgotPasswordViewDataBuilder';
|
||||
import type { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO';
|
||||
|
||||
describe('ForgotPasswordViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform ForgotPasswordPageDTO to ForgotPasswordViewData correctly', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(result).toEqual({
|
||||
returnTo: '/login',
|
||||
showSuccess: false,
|
||||
formState: {
|
||||
fields: {
|
||||
email: { value: '', error: undefined, touched: false, validating: false },
|
||||
},
|
||||
isValid: true,
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
submitCount: 0,
|
||||
},
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty returnTo path', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('');
|
||||
});
|
||||
|
||||
it('should handle returnTo with query parameters', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login?error=expired',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/login?error=expired');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe(forgotPasswordPageDTO.returnTo);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const originalDTO = { ...forgotPasswordPageDTO };
|
||||
ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(forgotPasswordPageDTO).toEqual(originalDTO);
|
||||
});
|
||||
|
||||
it('should initialize form field with default values', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(result.formState.fields.email.value).toBe('');
|
||||
expect(result.formState.fields.email.error).toBeUndefined();
|
||||
expect(result.formState.fields.email.touched).toBe(false);
|
||||
expect(result.formState.fields.email.validating).toBe(false);
|
||||
});
|
||||
|
||||
it('should initialize form state with default values', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(result.formState.isValid).toBe(true);
|
||||
expect(result.formState.isSubmitting).toBe(false);
|
||||
expect(result.formState.submitError).toBeUndefined();
|
||||
expect(result.formState.submitCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should initialize UI state flags correctly', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(result.showSuccess).toBe(false);
|
||||
expect(result.isSubmitting).toBe(false);
|
||||
expect(result.submitError).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle returnTo with encoded characters', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login?redirect=%2Fdashboard',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/login?redirect=%2Fdashboard');
|
||||
});
|
||||
|
||||
it('should handle returnTo with hash fragment', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login#section',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/login#section');
|
||||
});
|
||||
});
|
||||
|
||||
describe('form state structure', () => {
|
||||
it('should have email field', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
expect(result.formState.fields).toHaveProperty('email');
|
||||
});
|
||||
|
||||
it('should have consistent field state structure', () => {
|
||||
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
|
||||
|
||||
const field = result.formState.fields.email;
|
||||
expect(field).toHaveProperty('value');
|
||||
expect(field).toHaveProperty('error');
|
||||
expect(field).toHaveProperty('touched');
|
||||
expect(field).toHaveProperty('validating');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,553 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { HealthViewDataBuilder, HealthDTO } from './HealthViewDataBuilder';
|
||||
|
||||
describe('HealthViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform HealthDTO to HealthViewData correctly', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: 99.95,
|
||||
responseTime: 150,
|
||||
errorRate: 0.05,
|
||||
lastCheck: new Date().toISOString(),
|
||||
checksPassed: 995,
|
||||
checksFailed: 5,
|
||||
components: [
|
||||
{
|
||||
name: 'Database',
|
||||
status: 'ok',
|
||||
lastCheck: new Date().toISOString(),
|
||||
responseTime: 50,
|
||||
errorRate: 0.01,
|
||||
},
|
||||
{
|
||||
name: 'API',
|
||||
status: 'ok',
|
||||
lastCheck: new Date().toISOString(),
|
||||
responseTime: 100,
|
||||
errorRate: 0.02,
|
||||
},
|
||||
],
|
||||
alerts: [
|
||||
{
|
||||
id: 'alert-1',
|
||||
type: 'info',
|
||||
title: 'System Update',
|
||||
message: 'System updated successfully',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.overallStatus.status).toBe('ok');
|
||||
expect(result.overallStatus.statusLabel).toBe('Healthy');
|
||||
expect(result.overallStatus.statusColor).toBe('#10b981');
|
||||
expect(result.overallStatus.statusIcon).toBe('✓');
|
||||
expect(result.metrics.uptime).toBe('99.95%');
|
||||
expect(result.metrics.responseTime).toBe('150ms');
|
||||
expect(result.metrics.errorRate).toBe('0.05%');
|
||||
expect(result.metrics.checksPassed).toBe(995);
|
||||
expect(result.metrics.checksFailed).toBe(5);
|
||||
expect(result.metrics.totalChecks).toBe(1000);
|
||||
expect(result.metrics.successRate).toBe('99.5%');
|
||||
expect(result.components).toHaveLength(2);
|
||||
expect(result.components[0].name).toBe('Database');
|
||||
expect(result.components[0].status).toBe('ok');
|
||||
expect(result.components[0].statusLabel).toBe('Healthy');
|
||||
expect(result.alerts).toHaveLength(1);
|
||||
expect(result.alerts[0].id).toBe('alert-1');
|
||||
expect(result.alerts[0].type).toBe('info');
|
||||
expect(result.hasAlerts).toBe(true);
|
||||
expect(result.hasDegradedComponents).toBe(false);
|
||||
expect(result.hasErrorComponents).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle missing optional fields gracefully', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.overallStatus.status).toBe('ok');
|
||||
expect(result.metrics.uptime).toBe('N/A');
|
||||
expect(result.metrics.responseTime).toBe('N/A');
|
||||
expect(result.metrics.errorRate).toBe('N/A');
|
||||
expect(result.metrics.checksPassed).toBe(0);
|
||||
expect(result.metrics.checksFailed).toBe(0);
|
||||
expect(result.metrics.totalChecks).toBe(0);
|
||||
expect(result.metrics.successRate).toBe('N/A');
|
||||
expect(result.components).toEqual([]);
|
||||
expect(result.alerts).toEqual([]);
|
||||
expect(result.hasAlerts).toBe(false);
|
||||
expect(result.hasDegradedComponents).toBe(false);
|
||||
expect(result.hasErrorComponents).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle degraded status correctly', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'degraded',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: 95.5,
|
||||
responseTime: 500,
|
||||
errorRate: 4.5,
|
||||
components: [
|
||||
{
|
||||
name: 'Database',
|
||||
status: 'degraded',
|
||||
lastCheck: new Date().toISOString(),
|
||||
responseTime: 200,
|
||||
errorRate: 2.0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.overallStatus.status).toBe('degraded');
|
||||
expect(result.overallStatus.statusLabel).toBe('Degraded');
|
||||
expect(result.overallStatus.statusColor).toBe('#f59e0b');
|
||||
expect(result.overallStatus.statusIcon).toBe('⚠');
|
||||
expect(result.metrics.uptime).toBe('95.50%');
|
||||
expect(result.metrics.responseTime).toBe('500ms');
|
||||
expect(result.metrics.errorRate).toBe('4.50%');
|
||||
expect(result.hasDegradedComponents).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle error status correctly', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'error',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: 85.2,
|
||||
responseTime: 2000,
|
||||
errorRate: 14.8,
|
||||
components: [
|
||||
{
|
||||
name: 'Database',
|
||||
status: 'error',
|
||||
lastCheck: new Date().toISOString(),
|
||||
responseTime: 1500,
|
||||
errorRate: 10.0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.overallStatus.status).toBe('error');
|
||||
expect(result.overallStatus.statusLabel).toBe('Error');
|
||||
expect(result.overallStatus.statusColor).toBe('#ef4444');
|
||||
expect(result.overallStatus.statusIcon).toBe('✕');
|
||||
expect(result.metrics.uptime).toBe('85.20%');
|
||||
expect(result.metrics.responseTime).toBe('2.00s');
|
||||
expect(result.metrics.errorRate).toBe('14.80%');
|
||||
expect(result.hasErrorComponents).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle multiple components with mixed statuses', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'degraded',
|
||||
timestamp: new Date().toISOString(),
|
||||
components: [
|
||||
{
|
||||
name: 'Database',
|
||||
status: 'ok',
|
||||
lastCheck: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
name: 'API',
|
||||
status: 'degraded',
|
||||
lastCheck: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
name: 'Cache',
|
||||
status: 'error',
|
||||
lastCheck: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.components).toHaveLength(3);
|
||||
expect(result.hasDegradedComponents).toBe(true);
|
||||
expect(result.hasErrorComponents).toBe(true);
|
||||
expect(result.components[0].statusLabel).toBe('Healthy');
|
||||
expect(result.components[1].statusLabel).toBe('Degraded');
|
||||
expect(result.components[2].statusLabel).toBe('Error');
|
||||
});
|
||||
|
||||
it('should handle multiple alerts with different severities', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
alerts: [
|
||||
{
|
||||
id: 'alert-1',
|
||||
type: 'critical',
|
||||
title: 'Critical Alert',
|
||||
message: 'Critical issue detected',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'alert-2',
|
||||
type: 'warning',
|
||||
title: 'Warning Alert',
|
||||
message: 'Warning message',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'alert-3',
|
||||
type: 'info',
|
||||
title: 'Info Alert',
|
||||
message: 'Informational message',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.alerts).toHaveLength(3);
|
||||
expect(result.hasAlerts).toBe(true);
|
||||
expect(result.alerts[0].severity).toBe('Critical');
|
||||
expect(result.alerts[0].severityColor).toBe('#ef4444');
|
||||
expect(result.alerts[1].severity).toBe('Warning');
|
||||
expect(result.alerts[1].severityColor).toBe('#f59e0b');
|
||||
expect(result.alerts[2].severity).toBe('Info');
|
||||
expect(result.alerts[2].severityColor).toBe('#3b82f6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const now = new Date();
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: now.toISOString(),
|
||||
uptime: 99.99,
|
||||
responseTime: 100,
|
||||
errorRate: 0.01,
|
||||
lastCheck: now.toISOString(),
|
||||
checksPassed: 9999,
|
||||
checksFailed: 1,
|
||||
components: [
|
||||
{
|
||||
name: 'Test Component',
|
||||
status: 'ok',
|
||||
lastCheck: now.toISOString(),
|
||||
responseTime: 50,
|
||||
errorRate: 0.005,
|
||||
},
|
||||
],
|
||||
alerts: [
|
||||
{
|
||||
id: 'test-alert',
|
||||
type: 'info',
|
||||
title: 'Test Alert',
|
||||
message: 'Test message',
|
||||
timestamp: now.toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.overallStatus.status).toBe(healthDTO.status);
|
||||
expect(result.overallStatus.timestamp).toBe(healthDTO.timestamp);
|
||||
expect(result.metrics.uptime).toBe('99.99%');
|
||||
expect(result.metrics.responseTime).toBe('100ms');
|
||||
expect(result.metrics.errorRate).toBe('0.01%');
|
||||
expect(result.metrics.lastCheck).toBe(healthDTO.lastCheck);
|
||||
expect(result.metrics.checksPassed).toBe(healthDTO.checksPassed);
|
||||
expect(result.metrics.checksFailed).toBe(healthDTO.checksFailed);
|
||||
expect(result.components[0].name).toBe(healthDTO.components![0].name);
|
||||
expect(result.components[0].status).toBe(healthDTO.components![0].status);
|
||||
expect(result.alerts[0].id).toBe(healthDTO.alerts![0].id);
|
||||
expect(result.alerts[0].type).toBe(healthDTO.alerts![0].type);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: 99.95,
|
||||
responseTime: 150,
|
||||
errorRate: 0.05,
|
||||
components: [
|
||||
{
|
||||
name: 'Database',
|
||||
status: 'ok',
|
||||
lastCheck: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const originalDTO = JSON.parse(JSON.stringify(healthDTO));
|
||||
HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(healthDTO).toEqual(originalDTO);
|
||||
});
|
||||
|
||||
it('should transform all numeric fields to formatted strings', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: 99.95,
|
||||
responseTime: 150,
|
||||
errorRate: 0.05,
|
||||
checksPassed: 995,
|
||||
checksFailed: 5,
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(typeof result.metrics.uptime).toBe('string');
|
||||
expect(typeof result.metrics.responseTime).toBe('string');
|
||||
expect(typeof result.metrics.errorRate).toBe('string');
|
||||
expect(typeof result.metrics.successRate).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle large numbers correctly', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: 99.999,
|
||||
responseTime: 5000,
|
||||
errorRate: 0.001,
|
||||
checksPassed: 999999,
|
||||
checksFailed: 1,
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.metrics.uptime).toBe('100.00%');
|
||||
expect(result.metrics.responseTime).toBe('5.00s');
|
||||
expect(result.metrics.errorRate).toBe('0.00%');
|
||||
expect(result.metrics.successRate).toBe('100.0%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle null/undefined numeric fields', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: null as any,
|
||||
responseTime: undefined,
|
||||
errorRate: null as any,
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.metrics.uptime).toBe('N/A');
|
||||
expect(result.metrics.responseTime).toBe('N/A');
|
||||
expect(result.metrics.errorRate).toBe('N/A');
|
||||
});
|
||||
|
||||
it('should handle negative numeric values', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: -1,
|
||||
responseTime: -100,
|
||||
errorRate: -0.5,
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.metrics.uptime).toBe('N/A');
|
||||
expect(result.metrics.responseTime).toBe('N/A');
|
||||
expect(result.metrics.errorRate).toBe('N/A');
|
||||
});
|
||||
|
||||
it('should handle empty components and alerts arrays', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
components: [],
|
||||
alerts: [],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.components).toEqual([]);
|
||||
expect(result.alerts).toEqual([]);
|
||||
expect(result.hasAlerts).toBe(false);
|
||||
expect(result.hasDegradedComponents).toBe(false);
|
||||
expect(result.hasErrorComponents).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle component with missing optional fields', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
components: [
|
||||
{
|
||||
name: 'Test Component',
|
||||
status: 'ok',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.components[0].lastCheck).toBeDefined();
|
||||
expect(result.components[0].formattedLastCheck).toBeDefined();
|
||||
expect(result.components[0].responseTime).toBe('N/A');
|
||||
expect(result.components[0].errorRate).toBe('N/A');
|
||||
});
|
||||
|
||||
it('should handle alert with missing optional fields', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
alerts: [
|
||||
{
|
||||
id: 'alert-1',
|
||||
type: 'info',
|
||||
title: 'Test Alert',
|
||||
message: 'Test message',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.alerts[0].id).toBe('alert-1');
|
||||
expect(result.alerts[0].type).toBe('info');
|
||||
expect(result.alerts[0].title).toBe('Test Alert');
|
||||
expect(result.alerts[0].message).toBe('Test message');
|
||||
expect(result.alerts[0].timestamp).toBeDefined();
|
||||
expect(result.alerts[0].formattedTimestamp).toBeDefined();
|
||||
expect(result.alerts[0].relativeTime).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle unknown status', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'unknown',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.overallStatus.status).toBe('unknown');
|
||||
expect(result.overallStatus.statusLabel).toBe('Unknown');
|
||||
expect(result.overallStatus.statusColor).toBe('#6b7280');
|
||||
expect(result.overallStatus.statusIcon).toBe('?');
|
||||
});
|
||||
});
|
||||
|
||||
describe('derived fields', () => {
|
||||
it('should correctly calculate hasAlerts', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
alerts: [
|
||||
{
|
||||
id: 'alert-1',
|
||||
type: 'info',
|
||||
title: 'Test',
|
||||
message: 'Test message',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.hasAlerts).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly calculate hasDegradedComponents', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
components: [
|
||||
{
|
||||
name: 'Component 1',
|
||||
status: 'ok',
|
||||
lastCheck: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
name: 'Component 2',
|
||||
status: 'degraded',
|
||||
lastCheck: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.hasDegradedComponents).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly calculate hasErrorComponents', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
components: [
|
||||
{
|
||||
name: 'Component 1',
|
||||
status: 'ok',
|
||||
lastCheck: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
name: 'Component 2',
|
||||
status: 'error',
|
||||
lastCheck: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.hasErrorComponents).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly calculate totalChecks', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
checksPassed: 100,
|
||||
checksFailed: 20,
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.metrics.totalChecks).toBe(120);
|
||||
});
|
||||
|
||||
it('should correctly calculate successRate', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
checksPassed: 90,
|
||||
checksFailed: 10,
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.metrics.successRate).toBe('90.0%');
|
||||
});
|
||||
|
||||
it('should handle zero checks correctly', () => {
|
||||
const healthDTO: HealthDTO = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
checksPassed: 0,
|
||||
checksFailed: 0,
|
||||
};
|
||||
|
||||
const result = HealthViewDataBuilder.build(healthDTO);
|
||||
|
||||
expect(result.metrics.totalChecks).toBe(0);
|
||||
expect(result.metrics.successRate).toBe('N/A');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,600 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeaderboardsViewDataBuilder } from './LeaderboardsViewDataBuilder';
|
||||
|
||||
describe('LeaderboardsViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform Leaderboards DTO to LeaderboardsViewData correctly', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar1.jpg',
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Jane Smith',
|
||||
rating: 1100.0,
|
||||
skillLevel: 'advanced',
|
||||
nationality: 'Canada',
|
||||
racesCompleted: 100,
|
||||
wins: 15,
|
||||
podiums: 40,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'https://example.com/avatar2.jpg',
|
||||
},
|
||||
],
|
||||
totalRaces: 250,
|
||||
totalWins: 40,
|
||||
activeCount: 2,
|
||||
},
|
||||
teams: {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo1.jpg',
|
||||
memberCount: 15,
|
||||
rating: 1500,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
{
|
||||
id: 'team-2',
|
||||
name: 'Speed Demons',
|
||||
tag: 'SD',
|
||||
logoUrl: 'https://example.com/logo2.jpg',
|
||||
memberCount: 8,
|
||||
rating: 1200,
|
||||
totalWins: 20,
|
||||
totalRaces: 150,
|
||||
performanceLevel: 'advanced',
|
||||
isRecruiting: true,
|
||||
createdAt: '2023-06-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 5,
|
||||
groupsBySkillLevel: 'pro,advanced,intermediate',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo1.jpg',
|
||||
memberCount: 15,
|
||||
rating: 1500,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
{
|
||||
id: 'team-2',
|
||||
name: 'Speed Demons',
|
||||
tag: 'SD',
|
||||
logoUrl: 'https://example.com/logo2.jpg',
|
||||
memberCount: 8,
|
||||
rating: 1200,
|
||||
totalWins: 20,
|
||||
totalRaces: 150,
|
||||
performanceLevel: 'advanced',
|
||||
isRecruiting: true,
|
||||
createdAt: '2023-06-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
// Verify drivers
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.drivers[0].id).toBe('driver-1');
|
||||
expect(result.drivers[0].name).toBe('John Doe');
|
||||
expect(result.drivers[0].rating).toBe(1234.56);
|
||||
expect(result.drivers[0].skillLevel).toBe('pro');
|
||||
expect(result.drivers[0].nationality).toBe('USA');
|
||||
expect(result.drivers[0].wins).toBe(25);
|
||||
expect(result.drivers[0].podiums).toBe(60);
|
||||
expect(result.drivers[0].racesCompleted).toBe(150);
|
||||
expect(result.drivers[0].rank).toBe(1);
|
||||
expect(result.drivers[0].avatarUrl).toBe('https://example.com/avatar1.jpg');
|
||||
expect(result.drivers[0].position).toBe(1);
|
||||
|
||||
// Verify teams
|
||||
expect(result.teams).toHaveLength(2);
|
||||
expect(result.teams[0].id).toBe('team-1');
|
||||
expect(result.teams[0].name).toBe('Racing Team Alpha');
|
||||
expect(result.teams[0].tag).toBe('RTA');
|
||||
expect(result.teams[0].memberCount).toBe(15);
|
||||
expect(result.teams[0].totalWins).toBe(50);
|
||||
expect(result.teams[0].totalRaces).toBe(200);
|
||||
expect(result.teams[0].logoUrl).toBe('https://example.com/logo1.jpg');
|
||||
expect(result.teams[0].position).toBe(1);
|
||||
expect(result.teams[0].isRecruiting).toBe(false);
|
||||
expect(result.teams[0].performanceLevel).toBe('elite');
|
||||
expect(result.teams[0].rating).toBe(1500);
|
||||
expect(result.teams[0].category).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty driver and team arrays', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [],
|
||||
totalRaces: 0,
|
||||
totalWins: 0,
|
||||
activeCount: 0,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(result.drivers).toEqual([]);
|
||||
expect(result.teams).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle missing avatar URLs with empty string fallback', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
memberCount: 15,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(result.drivers[0].avatarUrl).toBe('');
|
||||
expect(result.teams[0].logoUrl).toBe('');
|
||||
});
|
||||
|
||||
it('should handle missing optional team fields with defaults', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [],
|
||||
totalRaces: 0,
|
||||
totalWins: 0,
|
||||
activeCount: 0,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
memberCount: 15,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(result.teams[0].rating).toBe(0);
|
||||
expect(result.teams[0].logoUrl).toBe('');
|
||||
});
|
||||
|
||||
it('should calculate position based on index', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [
|
||||
{ id: 'driver-1', name: 'Driver 1', rating: 1000, skillLevel: 'pro', nationality: 'USA', racesCompleted: 100, wins: 10, podiums: 30, isActive: true, rank: 1 },
|
||||
{ id: 'driver-2', name: 'Driver 2', rating: 900, skillLevel: 'advanced', nationality: 'Canada', racesCompleted: 80, wins: 8, podiums: 25, isActive: true, rank: 2 },
|
||||
{ id: 'driver-3', name: 'Driver 3', rating: 800, skillLevel: 'intermediate', nationality: 'UK', racesCompleted: 60, wins: 5, podiums: 15, isActive: true, rank: 3 },
|
||||
],
|
||||
totalRaces: 240,
|
||||
totalWins: 23,
|
||||
activeCount: 3,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 1,
|
||||
groupsBySkillLevel: 'elite,advanced,intermediate',
|
||||
topTeams: [
|
||||
{ id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' },
|
||||
{ id: 'team-2', name: 'Team 2', tag: 'T2', memberCount: 8, totalWins: 20, totalRaces: 120, performanceLevel: 'advanced', isRecruiting: true, createdAt: '2023-02-01' },
|
||||
{ id: 'team-3', name: 'Team 3', tag: 'T3', memberCount: 6, totalWins: 10, totalRaces: 80, performanceLevel: 'intermediate', isRecruiting: false, createdAt: '2023-03-01' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(result.drivers[0].position).toBe(1);
|
||||
expect(result.drivers[1].position).toBe(2);
|
||||
expect(result.drivers[2].position).toBe(3);
|
||||
|
||||
expect(result.teams[0].position).toBe(1);
|
||||
expect(result.teams[1].position).toBe(2);
|
||||
expect(result.teams[2].position).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 5,
|
||||
groupsBySkillLevel: 'pro,advanced',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-123',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo.jpg',
|
||||
memberCount: 15,
|
||||
rating: 1500,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(result.drivers[0].name).toBe(leaderboardsDTO.drivers.drivers[0].name);
|
||||
expect(result.drivers[0].nationality).toBe(leaderboardsDTO.drivers.drivers[0].nationality);
|
||||
expect(result.drivers[0].avatarUrl).toBe(leaderboardsDTO.drivers.drivers[0].avatarUrl);
|
||||
expect(result.teams[0].name).toBe(leaderboardsDTO.teams.topTeams[0].name);
|
||||
expect(result.teams[0].tag).toBe(leaderboardsDTO.teams.topTeams[0].tag);
|
||||
expect(result.teams[0].logoUrl).toBe(leaderboardsDTO.teams.topTeams[0].logoUrl);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 5,
|
||||
groupsBySkillLevel: 'pro,advanced',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-123',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo.jpg',
|
||||
memberCount: 15,
|
||||
rating: 1500,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const originalDTO = JSON.parse(JSON.stringify(leaderboardsDTO));
|
||||
LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(leaderboardsDTO).toEqual(originalDTO);
|
||||
});
|
||||
|
||||
it('should handle large numbers correctly', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 999999.99,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 10000,
|
||||
wins: 2500,
|
||||
podiums: 5000,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
},
|
||||
],
|
||||
totalRaces: 10000,
|
||||
totalWins: 2500,
|
||||
activeCount: 1,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo.jpg',
|
||||
memberCount: 100,
|
||||
rating: 999999,
|
||||
totalWins: 5000,
|
||||
totalRaces: 10000,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(result.drivers[0].rating).toBe(999999.99);
|
||||
expect(result.drivers[0].wins).toBe(2500);
|
||||
expect(result.drivers[0].podiums).toBe(5000);
|
||||
expect(result.drivers[0].racesCompleted).toBe(10000);
|
||||
expect(result.teams[0].rating).toBe(999999);
|
||||
expect(result.teams[0].totalWins).toBe(5000);
|
||||
expect(result.teams[0].totalRaces).toBe(10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle null/undefined avatar URLs', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: 1234.56,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: null as any,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: undefined as any,
|
||||
memberCount: 15,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(result.drivers[0].avatarUrl).toBe('');
|
||||
expect(result.teams[0].logoUrl).toBe('');
|
||||
});
|
||||
|
||||
it('should handle null/undefined rating', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'John Doe',
|
||||
rating: null as any,
|
||||
skillLevel: 'pro',
|
||||
nationality: 'USA',
|
||||
racesCompleted: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
totalRaces: 150,
|
||||
totalWins: 25,
|
||||
activeCount: 1,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
memberCount: 15,
|
||||
rating: null as any,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(result.drivers[0].rating).toBeNull();
|
||||
expect(result.teams[0].rating).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle null/undefined totalWins and totalRaces', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [],
|
||||
totalRaces: 0,
|
||||
totalWins: 0,
|
||||
activeCount: 0,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
memberCount: 15,
|
||||
totalWins: null as any,
|
||||
totalRaces: null as any,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(result.teams[0].totalWins).toBe(0);
|
||||
expect(result.teams[0].totalRaces).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle empty performance level', () => {
|
||||
const leaderboardsDTO = {
|
||||
drivers: {
|
||||
drivers: [],
|
||||
totalRaces: 0,
|
||||
totalWins: 0,
|
||||
activeCount: 0,
|
||||
},
|
||||
teams: {
|
||||
teams: [],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
memberCount: 15,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: '',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
|
||||
|
||||
expect(result.teams[0].performanceLevel).toBe('N/A');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueCoverViewDataBuilder } from './LeagueCoverViewDataBuilder';
|
||||
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
|
||||
describe('LeagueCoverViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform MediaBinaryDTO to LeagueCoverViewData correctly', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueCoverViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle JPEG cover images', () => {
|
||||
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/jpeg',
|
||||
};
|
||||
|
||||
const result = LeagueCoverViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle WebP cover images', () => {
|
||||
const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/webp',
|
||||
};
|
||||
|
||||
const result = LeagueCoverViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/webp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueCoverViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBeDefined();
|
||||
expect(result.contentType).toBe(mediaDto.contentType);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const originalDto = { ...mediaDto };
|
||||
LeagueCoverViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(mediaDto).toEqual(originalDto);
|
||||
});
|
||||
|
||||
it('should convert buffer to base64 string', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueCoverViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(typeof result.buffer).toBe('string');
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty buffer', () => {
|
||||
const buffer = new Uint8Array([]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueCoverViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe('');
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle large cover images', () => {
|
||||
const buffer = new Uint8Array(2 * 1024 * 1024); // 2MB
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/jpeg',
|
||||
};
|
||||
|
||||
const result = LeagueCoverViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle buffer with all zeros', () => {
|
||||
const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueCoverViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle buffer with all ones', () => {
|
||||
const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueCoverViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,577 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueDetailViewDataBuilder } from './LeagueDetailViewDataBuilder';
|
||||
import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
|
||||
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
|
||||
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
|
||||
import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
|
||||
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
||||
|
||||
describe('LeagueDetailViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform league DTOs to LeagueDetailViewData correctly', () => {
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Pro League',
|
||||
description: 'A competitive league for experienced drivers',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo • 32 max',
|
||||
},
|
||||
usedSlots: 25,
|
||||
category: 'competitive',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'iRacing',
|
||||
primaryChampionshipType: 'Single Championship',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Weekly races on Sundays',
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
pendingJoinRequestsCount: 3,
|
||||
pendingProtestsCount: 1,
|
||||
walletBalance: 1000,
|
||||
};
|
||||
|
||||
const owner: GetDriverOutputDTO = {
|
||||
id: 'owner-1',
|
||||
name: 'John Doe',
|
||||
iracingId: '12345',
|
||||
country: 'USA',
|
||||
bio: 'Experienced driver',
|
||||
joinedAt: '2023-01-01T00:00:00.000Z',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
};
|
||||
|
||||
const scoringConfig: LeagueScoringConfigDTO = {
|
||||
id: 'config-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'game-1',
|
||||
gameName: 'iRacing',
|
||||
primaryChampionshipType: 'Single Championship',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
dropRaces: 2,
|
||||
pointsPerRace: 100,
|
||||
pointsForWin: 25,
|
||||
pointsForPodium: [20, 15, 10],
|
||||
};
|
||||
|
||||
const memberships: LeagueMembershipsDTO = {
|
||||
members: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'admin',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
driverId: 'driver-2',
|
||||
driver: {
|
||||
id: 'driver-2',
|
||||
name: 'Bob',
|
||||
iracingId: '22222',
|
||||
country: 'Germany',
|
||||
joinedAt: '2023-07-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'steward',
|
||||
joinedAt: '2023-07-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
driverId: 'driver-3',
|
||||
driver: {
|
||||
id: 'driver-3',
|
||||
name: 'Charlie',
|
||||
iracingId: '33333',
|
||||
country: 'France',
|
||||
joinedAt: '2023-08-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'member',
|
||||
joinedAt: '2023-08-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-01-15T14:00:00.000Z',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
sessionType: 'race',
|
||||
strengthOfField: 1500,
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
name: 'Race 2',
|
||||
date: '2024-01-22T14:00:00.000Z',
|
||||
track: 'Monza',
|
||||
car: 'Ferrari 488 GT3',
|
||||
sessionType: 'race',
|
||||
strengthOfField: 1600,
|
||||
},
|
||||
];
|
||||
|
||||
const sponsors: any[] = [
|
||||
{
|
||||
id: 'sponsor-1',
|
||||
name: 'Sponsor A',
|
||||
tier: 'main',
|
||||
logoUrl: 'https://example.com/sponsor-a.png',
|
||||
websiteUrl: 'https://sponsor-a.com',
|
||||
tagline: 'Premium racing gear',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner,
|
||||
scoringConfig,
|
||||
memberships,
|
||||
races,
|
||||
sponsors,
|
||||
});
|
||||
|
||||
expect(result.leagueId).toBe('league-1');
|
||||
expect(result.name).toBe('Pro League');
|
||||
expect(result.description).toBe('A competitive league for experienced drivers');
|
||||
expect(result.logoUrl).toBe('https://example.com/logo.png');
|
||||
expect(result.info.name).toBe('Pro League');
|
||||
expect(result.info.description).toBe('A competitive league for experienced drivers');
|
||||
expect(result.info.membersCount).toBe(3);
|
||||
expect(result.info.racesCount).toBe(2);
|
||||
expect(result.info.avgSOF).toBe(1550);
|
||||
expect(result.info.structure).toBe('Solo • 32 max');
|
||||
expect(result.info.scoring).toBe('preset-1');
|
||||
expect(result.info.createdAt).toBe('2024-01-01T00:00:00.000Z');
|
||||
expect(result.info.discordUrl).toBeUndefined();
|
||||
expect(result.info.youtubeUrl).toBeUndefined();
|
||||
expect(result.info.websiteUrl).toBeUndefined();
|
||||
expect(result.ownerSummary).not.toBeNull();
|
||||
expect(result.ownerSummary?.driverId).toBe('owner-1');
|
||||
expect(result.ownerSummary?.driverName).toBe('John Doe');
|
||||
expect(result.ownerSummary?.avatarUrl).toBe('https://example.com/avatar.jpg');
|
||||
expect(result.ownerSummary?.roleBadgeText).toBe('Owner');
|
||||
expect(result.adminSummaries).toHaveLength(1);
|
||||
expect(result.adminSummaries[0].driverId).toBe('driver-1');
|
||||
expect(result.adminSummaries[0].driverName).toBe('Alice');
|
||||
expect(result.adminSummaries[0].roleBadgeText).toBe('Admin');
|
||||
expect(result.stewardSummaries).toHaveLength(1);
|
||||
expect(result.stewardSummaries[0].driverId).toBe('driver-2');
|
||||
expect(result.stewardSummaries[0].driverName).toBe('Bob');
|
||||
expect(result.stewardSummaries[0].roleBadgeText).toBe('Steward');
|
||||
expect(result.memberSummaries).toHaveLength(1);
|
||||
expect(result.memberSummaries[0].driverId).toBe('driver-3');
|
||||
expect(result.memberSummaries[0].driverName).toBe('Charlie');
|
||||
expect(result.memberSummaries[0].roleBadgeText).toBe('Member');
|
||||
expect(result.sponsors).toHaveLength(1);
|
||||
expect(result.sponsors[0].id).toBe('sponsor-1');
|
||||
expect(result.sponsors[0].name).toBe('Sponsor A');
|
||||
expect(result.sponsors[0].tier).toBe('main');
|
||||
expect(result.walletBalance).toBe(1000);
|
||||
expect(result.pendingProtestsCount).toBe(1);
|
||||
expect(result.pendingJoinRequestsCount).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle league with no owner', () => {
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 10,
|
||||
};
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner: null,
|
||||
scoringConfig: null,
|
||||
memberships: { members: [] },
|
||||
races: [],
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(result.ownerSummary).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle league with no scoring config', () => {
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 10,
|
||||
};
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner: null,
|
||||
scoringConfig: null,
|
||||
memberships: { members: [] },
|
||||
races: [],
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(result.info.scoring).toBe('Standard');
|
||||
});
|
||||
|
||||
it('should handle league with no races', () => {
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 10,
|
||||
};
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner: null,
|
||||
scoringConfig: null,
|
||||
memberships: { members: [] },
|
||||
races: [],
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(result.info.racesCount).toBe(0);
|
||||
expect(result.info.avgSOF).toBeNull();
|
||||
expect(result.runningRaces).toEqual([]);
|
||||
expect(result.nextRace).toBeUndefined();
|
||||
expect(result.seasonProgress).toEqual({
|
||||
completedRaces: 0,
|
||||
totalRaces: 0,
|
||||
percentage: 0,
|
||||
});
|
||||
expect(result.recentResults).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo • 32 max',
|
||||
},
|
||||
usedSlots: 20,
|
||||
category: 'test',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Test Type',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Test Preset',
|
||||
dropPolicySummary: 'Test drop policy',
|
||||
scoringPatternSummary: 'Test pattern',
|
||||
},
|
||||
timingSummary: 'Test timing',
|
||||
logoUrl: 'https://example.com/test.png',
|
||||
pendingJoinRequestsCount: 5,
|
||||
pendingProtestsCount: 2,
|
||||
walletBalance: 500,
|
||||
};
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner: null,
|
||||
scoringConfig: null,
|
||||
memberships: { members: [] },
|
||||
races: [],
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(result.leagueId).toBe(league.id);
|
||||
expect(result.name).toBe(league.name);
|
||||
expect(result.description).toBe(league.description);
|
||||
expect(result.logoUrl).toBe(league.logoUrl);
|
||||
expect(result.walletBalance).toBe(league.walletBalance);
|
||||
expect(result.pendingProtestsCount).toBe(league.pendingProtestsCount);
|
||||
expect(result.pendingJoinRequestsCount).toBe(league.pendingJoinRequestsCount);
|
||||
});
|
||||
|
||||
it('should not modify the input DTOs', () => {
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 20,
|
||||
};
|
||||
|
||||
const originalLeague = JSON.parse(JSON.stringify(league));
|
||||
LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner: null,
|
||||
scoringConfig: null,
|
||||
memberships: { members: [] },
|
||||
races: [],
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(league).toEqual(originalLeague);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle league with missing optional fields', () => {
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Minimal League',
|
||||
description: '',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 10,
|
||||
};
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner: null,
|
||||
scoringConfig: null,
|
||||
memberships: { members: [] },
|
||||
races: [],
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(result.description).toBe('');
|
||||
expect(result.logoUrl).toBeUndefined();
|
||||
expect(result.info.description).toBe('');
|
||||
expect(result.info.discordUrl).toBeUndefined();
|
||||
expect(result.info.youtubeUrl).toBeUndefined();
|
||||
expect(result.info.websiteUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle races with missing strengthOfField', () => {
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 10,
|
||||
};
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-01-15T14:00:00.000Z',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
sessionType: 'race',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner: null,
|
||||
scoringConfig: null,
|
||||
memberships: { members: [] },
|
||||
races,
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(result.info.avgSOF).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle races with zero strengthOfField', () => {
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 10,
|
||||
};
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: '2024-01-15T14:00:00.000Z',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
sessionType: 'race',
|
||||
strengthOfField: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner: null,
|
||||
scoringConfig: null,
|
||||
memberships: { members: [] },
|
||||
races,
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(result.info.avgSOF).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle races with different dates for next race calculation', () => {
|
||||
const now = new Date();
|
||||
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now
|
||||
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 10,
|
||||
};
|
||||
|
||||
const races: RaceDTO[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Past Race',
|
||||
date: pastDate.toISOString(),
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
sessionType: 'race',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
name: 'Future Race',
|
||||
date: futureDate.toISOString(),
|
||||
track: 'Monza',
|
||||
car: 'Ferrari 488 GT3',
|
||||
sessionType: 'race',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner: null,
|
||||
scoringConfig: null,
|
||||
memberships: { members: [] },
|
||||
races,
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(result.nextRace).toBeDefined();
|
||||
expect(result.nextRace?.id).toBe('race-2');
|
||||
expect(result.nextRace?.name).toBe('Future Race');
|
||||
expect(result.seasonProgress.completedRaces).toBe(1);
|
||||
expect(result.seasonProgress.totalRaces).toBe(2);
|
||||
expect(result.seasonProgress.percentage).toBe(50);
|
||||
expect(result.recentResults).toHaveLength(1);
|
||||
expect(result.recentResults[0].raceId).toBe('race-1');
|
||||
});
|
||||
|
||||
it('should handle members with different roles', () => {
|
||||
const league: LeagueWithCapacityAndScoringDTO = {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 10,
|
||||
};
|
||||
|
||||
const memberships: LeagueMembershipsDTO = {
|
||||
members: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Admin',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'admin',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
driverId: 'driver-2',
|
||||
driver: {
|
||||
id: 'driver-2',
|
||||
name: 'Steward',
|
||||
iracingId: '22222',
|
||||
country: 'Germany',
|
||||
joinedAt: '2023-07-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'steward',
|
||||
joinedAt: '2023-07-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
driverId: 'driver-3',
|
||||
driver: {
|
||||
id: 'driver-3',
|
||||
name: 'Member',
|
||||
iracingId: '33333',
|
||||
country: 'France',
|
||||
joinedAt: '2023-08-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'member',
|
||||
joinedAt: '2023-08-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueDetailViewDataBuilder.build({
|
||||
league,
|
||||
owner: null,
|
||||
scoringConfig: null,
|
||||
memberships,
|
||||
races: [],
|
||||
sponsors: [],
|
||||
});
|
||||
|
||||
expect(result.adminSummaries).toHaveLength(1);
|
||||
expect(result.stewardSummaries).toHaveLength(1);
|
||||
expect(result.memberSummaries).toHaveLength(1);
|
||||
expect(result.info.membersCount).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueLogoViewDataBuilder } from './LeagueLogoViewDataBuilder';
|
||||
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
|
||||
describe('LeagueLogoViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform MediaBinaryDTO to LeagueLogoViewData correctly', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle SVG league logos', () => {
|
||||
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100"/></svg>');
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/svg+xml',
|
||||
};
|
||||
|
||||
const result = LeagueLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/svg+xml');
|
||||
});
|
||||
|
||||
it('should handle transparent PNG logos', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBeDefined();
|
||||
expect(result.contentType).toBe(mediaDto.contentType);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const originalDto = { ...mediaDto };
|
||||
LeagueLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(mediaDto).toEqual(originalDto);
|
||||
});
|
||||
|
||||
it('should convert buffer to base64 string', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(typeof result.buffer).toBe('string');
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty buffer', () => {
|
||||
const buffer = new Uint8Array([]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe('');
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle small logo files', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle buffer with special characters', () => {
|
||||
const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = LeagueLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,255 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueRosterAdminViewDataBuilder } from './LeagueRosterAdminViewDataBuilder';
|
||||
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
||||
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
|
||||
|
||||
describe('LeagueRosterAdminViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform roster DTOs to LeagueRosterAdminViewData correctly', () => {
|
||||
const members: LeagueRosterMemberDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'admin',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
driverId: 'driver-2',
|
||||
driver: {
|
||||
id: 'driver-2',
|
||||
name: 'Bob',
|
||||
iracingId: '22222',
|
||||
country: 'Germany',
|
||||
joinedAt: '2023-07-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'member',
|
||||
joinedAt: '2023-07-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const joinRequests: LeagueRosterJoinRequestDTO[] = [
|
||||
{
|
||||
id: 'request-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-3',
|
||||
requestedAt: '2024-01-15T10:00:00.000Z',
|
||||
message: 'I would like to join this league',
|
||||
driver: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueRosterAdminViewDataBuilder.build({
|
||||
leagueId: 'league-1',
|
||||
members,
|
||||
joinRequests,
|
||||
});
|
||||
|
||||
expect(result.leagueId).toBe('league-1');
|
||||
expect(result.members).toHaveLength(2);
|
||||
expect(result.members[0].driverId).toBe('driver-1');
|
||||
expect(result.members[0].driver.id).toBe('driver-1');
|
||||
expect(result.members[0].driver.name).toBe('Alice');
|
||||
expect(result.members[0].role).toBe('admin');
|
||||
expect(result.members[0].joinedAt).toBe('2023-06-01T00:00:00.000Z');
|
||||
expect(result.members[0].formattedJoinedAt).toBeDefined();
|
||||
expect(result.members[1].driverId).toBe('driver-2');
|
||||
expect(result.members[1].driver.id).toBe('driver-2');
|
||||
expect(result.members[1].driver.name).toBe('Bob');
|
||||
expect(result.members[1].role).toBe('member');
|
||||
expect(result.members[1].joinedAt).toBe('2023-07-01T00:00:00.000Z');
|
||||
expect(result.members[1].formattedJoinedAt).toBeDefined();
|
||||
expect(result.joinRequests).toHaveLength(1);
|
||||
expect(result.joinRequests[0].id).toBe('request-1');
|
||||
expect(result.joinRequests[0].driver.id).toBe('driver-3');
|
||||
expect(result.joinRequests[0].driver.name).toBe('Unknown Driver');
|
||||
expect(result.joinRequests[0].requestedAt).toBe('2024-01-15T10:00:00.000Z');
|
||||
expect(result.joinRequests[0].formattedRequestedAt).toBeDefined();
|
||||
expect(result.joinRequests[0].message).toBe('I would like to join this league');
|
||||
});
|
||||
|
||||
it('should handle empty members and join requests', () => {
|
||||
const result = LeagueRosterAdminViewDataBuilder.build({
|
||||
leagueId: 'league-1',
|
||||
members: [],
|
||||
joinRequests: [],
|
||||
});
|
||||
|
||||
expect(result.leagueId).toBe('league-1');
|
||||
expect(result.members).toHaveLength(0);
|
||||
expect(result.joinRequests).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle members without driver details', () => {
|
||||
const members: LeagueRosterMemberDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: undefined as any,
|
||||
role: 'member',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueRosterAdminViewDataBuilder.build({
|
||||
leagueId: 'league-1',
|
||||
members,
|
||||
joinRequests: [],
|
||||
});
|
||||
|
||||
expect(result.members[0].driver.name).toBe('Unknown Driver');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const members: LeagueRosterMemberDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'admin',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const joinRequests: LeagueRosterJoinRequestDTO[] = [
|
||||
{
|
||||
id: 'request-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-3',
|
||||
requestedAt: '2024-01-15T10:00:00.000Z',
|
||||
message: 'I would like to join this league',
|
||||
driver: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueRosterAdminViewDataBuilder.build({
|
||||
leagueId: 'league-1',
|
||||
members,
|
||||
joinRequests,
|
||||
});
|
||||
|
||||
expect(result.leagueId).toBe('league-1');
|
||||
expect(result.members[0].driverId).toBe(members[0].driverId);
|
||||
expect(result.members[0].driver.id).toBe(members[0].driver.id);
|
||||
expect(result.members[0].driver.name).toBe(members[0].driver.name);
|
||||
expect(result.members[0].role).toBe(members[0].role);
|
||||
expect(result.members[0].joinedAt).toBe(members[0].joinedAt);
|
||||
expect(result.joinRequests[0].id).toBe(joinRequests[0].id);
|
||||
expect(result.joinRequests[0].requestedAt).toBe(joinRequests[0].requestedAt);
|
||||
expect(result.joinRequests[0].message).toBe(joinRequests[0].message);
|
||||
});
|
||||
|
||||
it('should not modify the input DTOs', () => {
|
||||
const members: LeagueRosterMemberDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'admin',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const joinRequests: LeagueRosterJoinRequestDTO[] = [
|
||||
{
|
||||
id: 'request-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-3',
|
||||
requestedAt: '2024-01-15T10:00:00.000Z',
|
||||
message: 'I would like to join this league',
|
||||
driver: {},
|
||||
},
|
||||
];
|
||||
|
||||
const originalMembers = JSON.parse(JSON.stringify(members));
|
||||
const originalRequests = JSON.parse(JSON.stringify(joinRequests));
|
||||
|
||||
LeagueRosterAdminViewDataBuilder.build({
|
||||
leagueId: 'league-1',
|
||||
members,
|
||||
joinRequests,
|
||||
});
|
||||
|
||||
expect(members).toEqual(originalMembers);
|
||||
expect(joinRequests).toEqual(originalRequests);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle members with missing driver field', () => {
|
||||
const members: LeagueRosterMemberDTO[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: undefined as any,
|
||||
role: 'member',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueRosterAdminViewDataBuilder.build({
|
||||
leagueId: 'league-1',
|
||||
members,
|
||||
joinRequests: [],
|
||||
});
|
||||
|
||||
expect(result.members[0].driver.name).toBe('Unknown Driver');
|
||||
});
|
||||
|
||||
it('should handle join requests with missing driver field', () => {
|
||||
const joinRequests: LeagueRosterJoinRequestDTO[] = [
|
||||
{
|
||||
id: 'request-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-3',
|
||||
requestedAt: '2024-01-15T10:00:00.000Z',
|
||||
message: 'I would like to join this league',
|
||||
driver: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueRosterAdminViewDataBuilder.build({
|
||||
leagueId: 'league-1',
|
||||
members: [],
|
||||
joinRequests,
|
||||
});
|
||||
|
||||
expect(result.joinRequests[0].driver.name).toBe('Unknown Driver');
|
||||
});
|
||||
|
||||
it('should handle join requests without message', () => {
|
||||
const joinRequests: LeagueRosterJoinRequestDTO[] = [
|
||||
{
|
||||
id: 'request-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-3',
|
||||
requestedAt: '2024-01-15T10:00:00.000Z',
|
||||
driver: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = LeagueRosterAdminViewDataBuilder.build({
|
||||
leagueId: 'league-1',
|
||||
members: [],
|
||||
joinRequests,
|
||||
});
|
||||
|
||||
expect(result.joinRequests[0].message).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueScheduleViewDataBuilder } from './LeagueScheduleViewDataBuilder';
|
||||
|
||||
describe('LeagueScheduleViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform schedule DTO to LeagueScheduleViewData correctly', () => {
|
||||
const now = new Date();
|
||||
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now
|
||||
|
||||
const apiDto = {
|
||||
leagueId: 'league-1',
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Past Race',
|
||||
date: pastDate.toISOString(),
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
sessionType: 'race',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
name: 'Future Race',
|
||||
date: futureDate.toISOString(),
|
||||
track: 'Monza',
|
||||
car: 'Ferrari 488 GT3',
|
||||
sessionType: 'race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto, 'driver-1', true);
|
||||
|
||||
expect(result.leagueId).toBe('league-1');
|
||||
expect(result.races).toHaveLength(2);
|
||||
expect(result.races[0].id).toBe('race-1');
|
||||
expect(result.races[0].name).toBe('Past Race');
|
||||
expect(result.races[0].scheduledAt).toBe(pastDate.toISOString());
|
||||
expect(result.races[0].track).toBe('Spa');
|
||||
expect(result.races[0].car).toBe('Porsche 911 GT3');
|
||||
expect(result.races[0].sessionType).toBe('race');
|
||||
expect(result.races[0].isPast).toBe(true);
|
||||
expect(result.races[0].isUpcoming).toBe(false);
|
||||
expect(result.races[0].status).toBe('completed');
|
||||
expect(result.races[0].isUserRegistered).toBe(false);
|
||||
expect(result.races[0].canRegister).toBe(false);
|
||||
expect(result.races[0].canEdit).toBe(true);
|
||||
expect(result.races[0].canReschedule).toBe(true);
|
||||
expect(result.races[1].id).toBe('race-2');
|
||||
expect(result.races[1].name).toBe('Future Race');
|
||||
expect(result.races[1].scheduledAt).toBe(futureDate.toISOString());
|
||||
expect(result.races[1].track).toBe('Monza');
|
||||
expect(result.races[1].car).toBe('Ferrari 488 GT3');
|
||||
expect(result.races[1].sessionType).toBe('race');
|
||||
expect(result.races[1].isPast).toBe(false);
|
||||
expect(result.races[1].isUpcoming).toBe(true);
|
||||
expect(result.races[1].status).toBe('scheduled');
|
||||
expect(result.races[1].isUserRegistered).toBe(false);
|
||||
expect(result.races[1].canRegister).toBe(true);
|
||||
expect(result.races[1].canEdit).toBe(true);
|
||||
expect(result.races[1].canReschedule).toBe(true);
|
||||
expect(result.currentDriverId).toBe('driver-1');
|
||||
expect(result.isAdmin).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty races list', () => {
|
||||
const apiDto = {
|
||||
leagueId: 'league-1',
|
||||
races: [],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagueId).toBe('league-1');
|
||||
expect(result.races).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle non-admin user', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
const apiDto = {
|
||||
leagueId: 'league-1',
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Future Race',
|
||||
date: futureDate.toISOString(),
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
sessionType: 'race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto, 'driver-1', false);
|
||||
|
||||
expect(result.races[0].canEdit).toBe(false);
|
||||
expect(result.races[0].canReschedule).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
const apiDto = {
|
||||
leagueId: 'league-1',
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Test Race',
|
||||
date: futureDate.toISOString(),
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
sessionType: 'race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.leagueId).toBe(apiDto.leagueId);
|
||||
expect(result.races[0].id).toBe(apiDto.races[0].id);
|
||||
expect(result.races[0].name).toBe(apiDto.races[0].name);
|
||||
expect(result.races[0].scheduledAt).toBe(apiDto.races[0].date);
|
||||
expect(result.races[0].track).toBe(apiDto.races[0].track);
|
||||
expect(result.races[0].car).toBe(apiDto.races[0].car);
|
||||
expect(result.races[0].sessionType).toBe(apiDto.races[0].sessionType);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
const apiDto = {
|
||||
leagueId: 'league-1',
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Test Race',
|
||||
date: futureDate.toISOString(),
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
sessionType: 'race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const originalDto = JSON.parse(JSON.stringify(apiDto));
|
||||
LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(apiDto).toEqual(originalDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle races with missing optional fields', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
const apiDto = {
|
||||
leagueId: 'league-1',
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Test Race',
|
||||
date: futureDate.toISOString(),
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
sessionType: 'race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].track).toBe('Spa');
|
||||
expect(result.races[0].car).toBe('Porsche 911 GT3');
|
||||
expect(result.races[0].sessionType).toBe('race');
|
||||
});
|
||||
|
||||
it('should handle races at exactly the current time', () => {
|
||||
const now = new Date();
|
||||
const currentRaceDate = new Date(now.getTime());
|
||||
|
||||
const apiDto = {
|
||||
leagueId: 'league-1',
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Current Race',
|
||||
date: currentRaceDate.toISOString(),
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
sessionType: 'race',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
||||
|
||||
// Race at current time should be considered past
|
||||
expect(result.races[0].isPast).toBe(true);
|
||||
expect(result.races[0].isUpcoming).toBe(false);
|
||||
expect(result.races[0].status).toBe('completed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,464 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueStandingsViewDataBuilder } from './LeagueStandingsViewDataBuilder';
|
||||
|
||||
describe('LeagueStandingsViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform standings DTOs to LeagueStandingsViewData correctly', () => {
|
||||
const standingsDto = {
|
||||
standings: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
},
|
||||
points: 1250,
|
||||
position: 1,
|
||||
wins: 5,
|
||||
podiums: 10,
|
||||
races: 15,
|
||||
positionChange: 2,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: ['race-1', 'race-2'],
|
||||
},
|
||||
{
|
||||
driverId: 'driver-2',
|
||||
driver: {
|
||||
id: 'driver-2',
|
||||
name: 'Bob',
|
||||
iracingId: '22222',
|
||||
country: 'Germany',
|
||||
},
|
||||
points: 1100,
|
||||
position: 2,
|
||||
wins: 3,
|
||||
podiums: 8,
|
||||
races: 15,
|
||||
positionChange: -1,
|
||||
lastRacePoints: 15,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const membershipsDto = {
|
||||
members: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'member',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
driverId: 'driver-2',
|
||||
driver: {
|
||||
id: 'driver-2',
|
||||
name: 'Bob',
|
||||
iracingId: '22222',
|
||||
country: 'Germany',
|
||||
joinedAt: '2023-07-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'member',
|
||||
joinedAt: '2023-07-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
standingsDto,
|
||||
membershipsDto,
|
||||
'league-1',
|
||||
false
|
||||
);
|
||||
|
||||
expect(result.leagueId).toBe('league-1');
|
||||
expect(result.isTeamChampionship).toBe(false);
|
||||
expect(result.currentDriverId).toBeNull();
|
||||
expect(result.isAdmin).toBe(false);
|
||||
expect(result.standings).toHaveLength(2);
|
||||
expect(result.standings[0].driverId).toBe('driver-1');
|
||||
expect(result.standings[0].position).toBe(1);
|
||||
expect(result.standings[0].totalPoints).toBe(1250);
|
||||
expect(result.standings[0].racesFinished).toBe(15);
|
||||
expect(result.standings[0].racesStarted).toBe(15);
|
||||
expect(result.standings[0].avgFinish).toBeNull();
|
||||
expect(result.standings[0].penaltyPoints).toBe(0);
|
||||
expect(result.standings[0].bonusPoints).toBe(0);
|
||||
expect(result.standings[0].positionChange).toBe(2);
|
||||
expect(result.standings[0].lastRacePoints).toBe(25);
|
||||
expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']);
|
||||
expect(result.standings[0].wins).toBe(5);
|
||||
expect(result.standings[0].podiums).toBe(10);
|
||||
expect(result.standings[1].driverId).toBe('driver-2');
|
||||
expect(result.standings[1].position).toBe(2);
|
||||
expect(result.standings[1].totalPoints).toBe(1100);
|
||||
expect(result.standings[1].racesFinished).toBe(15);
|
||||
expect(result.standings[1].racesStarted).toBe(15);
|
||||
expect(result.standings[1].avgFinish).toBeNull();
|
||||
expect(result.standings[1].penaltyPoints).toBe(0);
|
||||
expect(result.standings[1].bonusPoints).toBe(0);
|
||||
expect(result.standings[1].positionChange).toBe(-1);
|
||||
expect(result.standings[1].lastRacePoints).toBe(15);
|
||||
expect(result.standings[1].droppedRaceIds).toEqual([]);
|
||||
expect(result.standings[1].wins).toBe(3);
|
||||
expect(result.standings[1].podiums).toBe(8);
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.drivers[0].id).toBe('driver-1');
|
||||
expect(result.drivers[0].name).toBe('Alice');
|
||||
expect(result.drivers[0].iracingId).toBe('11111');
|
||||
expect(result.drivers[0].country).toBe('UK');
|
||||
expect(result.drivers[0].avatarUrl).toBeNull();
|
||||
expect(result.drivers[1].id).toBe('driver-2');
|
||||
expect(result.drivers[1].name).toBe('Bob');
|
||||
expect(result.drivers[1].iracingId).toBe('22222');
|
||||
expect(result.drivers[1].country).toBe('Germany');
|
||||
expect(result.drivers[1].avatarUrl).toBeNull();
|
||||
expect(result.memberships).toHaveLength(2);
|
||||
expect(result.memberships[0].driverId).toBe('driver-1');
|
||||
expect(result.memberships[0].leagueId).toBe('league-1');
|
||||
expect(result.memberships[0].role).toBe('member');
|
||||
expect(result.memberships[0].joinedAt).toBe('2023-06-01T00:00:00.000Z');
|
||||
expect(result.memberships[0].status).toBe('active');
|
||||
expect(result.memberships[1].driverId).toBe('driver-2');
|
||||
expect(result.memberships[1].leagueId).toBe('league-1');
|
||||
expect(result.memberships[1].role).toBe('member');
|
||||
expect(result.memberships[1].joinedAt).toBe('2023-07-01T00:00:00.000Z');
|
||||
expect(result.memberships[1].status).toBe('active');
|
||||
});
|
||||
|
||||
it('should handle empty standings and memberships', () => {
|
||||
const standingsDto = {
|
||||
standings: [],
|
||||
};
|
||||
|
||||
const membershipsDto = {
|
||||
members: [],
|
||||
};
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
standingsDto,
|
||||
membershipsDto,
|
||||
'league-1',
|
||||
false
|
||||
);
|
||||
|
||||
expect(result.standings).toHaveLength(0);
|
||||
expect(result.drivers).toHaveLength(0);
|
||||
expect(result.memberships).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle team championship mode', () => {
|
||||
const standingsDto = {
|
||||
standings: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
},
|
||||
points: 1250,
|
||||
position: 1,
|
||||
wins: 5,
|
||||
podiums: 10,
|
||||
races: 15,
|
||||
positionChange: 2,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const membershipsDto = {
|
||||
members: [],
|
||||
};
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
standingsDto,
|
||||
membershipsDto,
|
||||
'league-1',
|
||||
true
|
||||
);
|
||||
|
||||
expect(result.isTeamChampionship).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const standingsDto = {
|
||||
standings: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
},
|
||||
points: 1250,
|
||||
position: 1,
|
||||
wins: 5,
|
||||
podiums: 10,
|
||||
races: 15,
|
||||
positionChange: 2,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: ['race-1'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const membershipsDto = {
|
||||
members: [],
|
||||
};
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
standingsDto,
|
||||
membershipsDto,
|
||||
'league-1',
|
||||
false
|
||||
);
|
||||
|
||||
expect(result.standings[0].driverId).toBe(standingsDto.standings[0].driverId);
|
||||
expect(result.standings[0].position).toBe(standingsDto.standings[0].position);
|
||||
expect(result.standings[0].totalPoints).toBe(standingsDto.standings[0].points);
|
||||
expect(result.standings[0].racesFinished).toBe(standingsDto.standings[0].races);
|
||||
expect(result.standings[0].racesStarted).toBe(standingsDto.standings[0].races);
|
||||
expect(result.standings[0].positionChange).toBe(standingsDto.standings[0].positionChange);
|
||||
expect(result.standings[0].lastRacePoints).toBe(standingsDto.standings[0].lastRacePoints);
|
||||
expect(result.standings[0].droppedRaceIds).toEqual(standingsDto.standings[0].droppedRaceIds);
|
||||
expect(result.standings[0].wins).toBe(standingsDto.standings[0].wins);
|
||||
expect(result.standings[0].podiums).toBe(standingsDto.standings[0].podiums);
|
||||
expect(result.drivers[0].id).toBe(standingsDto.standings[0].driver.id);
|
||||
expect(result.drivers[0].name).toBe(standingsDto.standings[0].driver.name);
|
||||
expect(result.drivers[0].iracingId).toBe(standingsDto.standings[0].driver.iracingId);
|
||||
expect(result.drivers[0].country).toBe(standingsDto.standings[0].driver.country);
|
||||
});
|
||||
|
||||
it('should not modify the input DTOs', () => {
|
||||
const standingsDto = {
|
||||
standings: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
},
|
||||
points: 1250,
|
||||
position: 1,
|
||||
wins: 5,
|
||||
podiums: 10,
|
||||
races: 15,
|
||||
positionChange: 2,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: ['race-1'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const membershipsDto = {
|
||||
members: [],
|
||||
};
|
||||
|
||||
const originalStandings = JSON.parse(JSON.stringify(standingsDto));
|
||||
const originalMemberships = JSON.parse(JSON.stringify(membershipsDto));
|
||||
|
||||
LeagueStandingsViewDataBuilder.build(
|
||||
standingsDto,
|
||||
membershipsDto,
|
||||
'league-1',
|
||||
false
|
||||
);
|
||||
|
||||
expect(standingsDto).toEqual(originalStandings);
|
||||
expect(membershipsDto).toEqual(originalMemberships);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle standings with missing optional fields', () => {
|
||||
const standingsDto = {
|
||||
standings: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
},
|
||||
points: 1250,
|
||||
position: 1,
|
||||
wins: 5,
|
||||
podiums: 10,
|
||||
races: 15,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const membershipsDto = {
|
||||
members: [],
|
||||
};
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
standingsDto,
|
||||
membershipsDto,
|
||||
'league-1',
|
||||
false
|
||||
);
|
||||
|
||||
expect(result.standings[0].positionChange).toBe(0);
|
||||
expect(result.standings[0].lastRacePoints).toBe(0);
|
||||
expect(result.standings[0].droppedRaceIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle standings with missing driver field', () => {
|
||||
const standingsDto = {
|
||||
standings: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: undefined as any,
|
||||
points: 1250,
|
||||
position: 1,
|
||||
wins: 5,
|
||||
podiums: 10,
|
||||
races: 15,
|
||||
positionChange: 2,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const membershipsDto = {
|
||||
members: [],
|
||||
};
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
standingsDto,
|
||||
membershipsDto,
|
||||
'league-1',
|
||||
false
|
||||
);
|
||||
|
||||
expect(result.drivers).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle duplicate drivers in standings', () => {
|
||||
const standingsDto = {
|
||||
standings: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
},
|
||||
points: 1250,
|
||||
position: 1,
|
||||
wins: 5,
|
||||
podiums: 10,
|
||||
races: 15,
|
||||
positionChange: 2,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
},
|
||||
points: 1100,
|
||||
position: 2,
|
||||
wins: 3,
|
||||
podiums: 8,
|
||||
races: 15,
|
||||
positionChange: -1,
|
||||
lastRacePoints: 15,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const membershipsDto = {
|
||||
members: [],
|
||||
};
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
standingsDto,
|
||||
membershipsDto,
|
||||
'league-1',
|
||||
false
|
||||
);
|
||||
|
||||
// Should only have one driver entry
|
||||
expect(result.drivers).toHaveLength(1);
|
||||
expect(result.drivers[0].id).toBe('driver-1');
|
||||
});
|
||||
|
||||
it('should handle members with different roles', () => {
|
||||
const standingsDto = {
|
||||
standings: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
},
|
||||
points: 1250,
|
||||
position: 1,
|
||||
wins: 5,
|
||||
podiums: 10,
|
||||
races: 15,
|
||||
positionChange: 2,
|
||||
lastRacePoints: 25,
|
||||
droppedRaceIds: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const membershipsDto = {
|
||||
members: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Alice',
|
||||
iracingId: '11111',
|
||||
country: 'UK',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
role: 'admin',
|
||||
joinedAt: '2023-06-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = LeagueStandingsViewDataBuilder.build(
|
||||
standingsDto,
|
||||
membershipsDto,
|
||||
'league-1',
|
||||
false
|
||||
);
|
||||
|
||||
expect(result.memberships[0].role).toBe('admin');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,351 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeaguesViewDataBuilder } from './LeaguesViewDataBuilder';
|
||||
import type { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO';
|
||||
|
||||
describe('LeaguesViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform AllLeaguesWithCapacityAndScoringDTO to LeaguesViewData correctly', () => {
|
||||
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Pro League',
|
||||
description: 'A competitive league for experienced drivers',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo • 32 max',
|
||||
},
|
||||
usedSlots: 25,
|
||||
category: 'competitive',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'iRacing',
|
||||
primaryChampionshipType: 'Single Championship',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Weekly races on Sundays',
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
pendingJoinRequestsCount: 3,
|
||||
pendingProtestsCount: 1,
|
||||
walletBalance: 1000,
|
||||
},
|
||||
{
|
||||
id: 'league-2',
|
||||
name: 'Rookie League',
|
||||
description: null,
|
||||
ownerId: 'owner-2',
|
||||
createdAt: '2024-02-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 16,
|
||||
qualifyingFormat: 'Solo • 16 max',
|
||||
},
|
||||
usedSlots: 10,
|
||||
category: 'rookie',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'iRacing',
|
||||
primaryChampionshipType: 'Single Championship',
|
||||
scoringPresetId: 'preset-2',
|
||||
scoringPresetName: 'Rookie',
|
||||
dropPolicySummary: 'No drops',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
timingSummary: 'Bi-weekly races',
|
||||
logoUrl: null,
|
||||
pendingJoinRequestsCount: 0,
|
||||
pendingProtestsCount: 0,
|
||||
walletBalance: 0,
|
||||
},
|
||||
],
|
||||
totalCount: 2,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
||||
|
||||
expect(result.leagues).toHaveLength(2);
|
||||
expect(result.leagues[0]).toEqual({
|
||||
id: 'league-1',
|
||||
name: 'Pro League',
|
||||
description: 'A competitive league for experienced drivers',
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
maxDrivers: 32,
|
||||
usedDriverSlots: 25,
|
||||
activeDriversCount: undefined,
|
||||
nextRaceAt: undefined,
|
||||
maxTeams: undefined,
|
||||
usedTeamSlots: undefined,
|
||||
structureSummary: 'Solo • 32 max',
|
||||
timingSummary: 'Weekly races on Sundays',
|
||||
category: 'competitive',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'iRacing',
|
||||
primaryChampionshipType: 'Single Championship',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Standard',
|
||||
dropPolicySummary: 'Drop 2 worst races',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
});
|
||||
expect(result.leagues[1]).toEqual({
|
||||
id: 'league-2',
|
||||
name: 'Rookie League',
|
||||
description: null,
|
||||
logoUrl: null,
|
||||
ownerId: 'owner-2',
|
||||
createdAt: '2024-02-01T00:00:00.000Z',
|
||||
maxDrivers: 16,
|
||||
usedDriverSlots: 10,
|
||||
activeDriversCount: undefined,
|
||||
nextRaceAt: undefined,
|
||||
maxTeams: undefined,
|
||||
usedTeamSlots: undefined,
|
||||
structureSummary: 'Solo • 16 max',
|
||||
timingSummary: 'Bi-weekly races',
|
||||
category: 'rookie',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'iRacing',
|
||||
primaryChampionshipType: 'Single Championship',
|
||||
scoringPresetId: 'preset-2',
|
||||
scoringPresetName: 'Rookie',
|
||||
dropPolicySummary: 'No drops',
|
||||
scoringPatternSummary: 'Points based on finish position',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty leagues list', () => {
|
||||
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
||||
|
||||
expect(result.leagues).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle leagues with missing optional fields', () => {
|
||||
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Minimal League',
|
||||
description: '',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 20,
|
||||
},
|
||||
usedSlots: 5,
|
||||
},
|
||||
],
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
||||
|
||||
expect(result.leagues[0].description).toBe(null);
|
||||
expect(result.leagues[0].logoUrl).toBe(null);
|
||||
expect(result.leagues[0].category).toBe(null);
|
||||
expect(result.leagues[0].scoring).toBeUndefined();
|
||||
expect(result.leagues[0].timingSummary).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo • 32 max',
|
||||
},
|
||||
usedSlots: 20,
|
||||
category: 'test',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Test Type',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Test Preset',
|
||||
dropPolicySummary: 'Test drop policy',
|
||||
scoringPatternSummary: 'Test pattern',
|
||||
},
|
||||
timingSummary: 'Test timing',
|
||||
logoUrl: 'https://example.com/test.png',
|
||||
pendingJoinRequestsCount: 5,
|
||||
pendingProtestsCount: 2,
|
||||
walletBalance: 500,
|
||||
},
|
||||
],
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
||||
|
||||
expect(result.leagues[0].id).toBe(leaguesDTO.leagues[0].id);
|
||||
expect(result.leagues[0].name).toBe(leaguesDTO.leagues[0].name);
|
||||
expect(result.leagues[0].description).toBe(leaguesDTO.leagues[0].description);
|
||||
expect(result.leagues[0].logoUrl).toBe(leaguesDTO.leagues[0].logoUrl);
|
||||
expect(result.leagues[0].ownerId).toBe(leaguesDTO.leagues[0].ownerId);
|
||||
expect(result.leagues[0].createdAt).toBe(leaguesDTO.leagues[0].createdAt);
|
||||
expect(result.leagues[0].maxDrivers).toBe(leaguesDTO.leagues[0].settings.maxDrivers);
|
||||
expect(result.leagues[0].usedDriverSlots).toBe(leaguesDTO.leagues[0].usedSlots);
|
||||
expect(result.leagues[0].structureSummary).toBe(leaguesDTO.leagues[0].settings.qualifyingFormat);
|
||||
expect(result.leagues[0].timingSummary).toBe(leaguesDTO.leagues[0].timingSummary);
|
||||
expect(result.leagues[0].category).toBe(leaguesDTO.leagues[0].category);
|
||||
expect(result.leagues[0].scoring).toEqual(leaguesDTO.leagues[0].scoring);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test description',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
qualifyingFormat: 'Solo • 32 max',
|
||||
},
|
||||
usedSlots: 20,
|
||||
category: 'test',
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'Test Type',
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Test Preset',
|
||||
dropPolicySummary: 'Test drop policy',
|
||||
scoringPatternSummary: 'Test pattern',
|
||||
},
|
||||
timingSummary: 'Test timing',
|
||||
logoUrl: 'https://example.com/test.png',
|
||||
pendingJoinRequestsCount: 5,
|
||||
pendingProtestsCount: 2,
|
||||
walletBalance: 500,
|
||||
},
|
||||
],
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const originalDTO = JSON.parse(JSON.stringify(leaguesDTO));
|
||||
LeaguesViewDataBuilder.build(leaguesDTO);
|
||||
|
||||
expect(leaguesDTO).toEqual(originalDTO);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle leagues with very long descriptions', () => {
|
||||
const longDescription = 'A'.repeat(1000);
|
||||
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: longDescription,
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 20,
|
||||
},
|
||||
],
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
||||
|
||||
expect(result.leagues[0].description).toBe(longDescription);
|
||||
});
|
||||
|
||||
it('should handle leagues with special characters in name', () => {
|
||||
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'League & Co. (2024)',
|
||||
description: 'Test league',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 20,
|
||||
},
|
||||
],
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
||||
|
||||
expect(result.leagues[0].name).toBe('League & Co. (2024)');
|
||||
});
|
||||
|
||||
it('should handle leagues with zero used slots', () => {
|
||||
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Empty League',
|
||||
description: 'No members yet',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 0,
|
||||
},
|
||||
],
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
||||
|
||||
expect(result.leagues[0].usedDriverSlots).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle leagues with maximum capacity', () => {
|
||||
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Full League',
|
||||
description: 'At maximum capacity',
|
||||
ownerId: 'owner-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
settings: {
|
||||
maxDrivers: 32,
|
||||
},
|
||||
usedSlots: 32,
|
||||
},
|
||||
],
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
||||
|
||||
expect(result.leagues[0].usedDriverSlots).toBe(32);
|
||||
expect(result.leagues[0].maxDrivers).toBe(32);
|
||||
});
|
||||
});
|
||||
});
|
||||
205
apps/website/lib/builders/view-data/LoginViewDataBuilder.test.ts
Normal file
205
apps/website/lib/builders/view-data/LoginViewDataBuilder.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LoginViewDataBuilder } from './LoginViewDataBuilder';
|
||||
import type { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
|
||||
|
||||
describe('LoginViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform LoginPageDTO to LoginViewData correctly', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result).toEqual({
|
||||
returnTo: '/dashboard',
|
||||
hasInsufficientPermissions: false,
|
||||
showPassword: false,
|
||||
showErrorDetails: false,
|
||||
formState: {
|
||||
fields: {
|
||||
email: { value: '', error: undefined, touched: false, validating: false },
|
||||
password: { value: '', error: undefined, touched: false, validating: false },
|
||||
rememberMe: { value: false, error: undefined, touched: false, validating: false },
|
||||
},
|
||||
isValid: true,
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
submitCount: 0,
|
||||
},
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle insufficient permissions flag correctly', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/admin',
|
||||
hasInsufficientPermissions: true,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result.hasInsufficientPermissions).toBe(true);
|
||||
expect(result.returnTo).toBe('/admin');
|
||||
});
|
||||
|
||||
it('should handle empty returnTo path', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('');
|
||||
expect(result.hasInsufficientPermissions).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe(loginPageDTO.returnTo);
|
||||
expect(result.hasInsufficientPermissions).toBe(loginPageDTO.hasInsufficientPermissions);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const originalDTO = { ...loginPageDTO };
|
||||
LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(loginPageDTO).toEqual(originalDTO);
|
||||
});
|
||||
|
||||
it('should initialize form fields with default values', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result.formState.fields.email.value).toBe('');
|
||||
expect(result.formState.fields.email.error).toBeUndefined();
|
||||
expect(result.formState.fields.email.touched).toBe(false);
|
||||
expect(result.formState.fields.email.validating).toBe(false);
|
||||
|
||||
expect(result.formState.fields.password.value).toBe('');
|
||||
expect(result.formState.fields.password.error).toBeUndefined();
|
||||
expect(result.formState.fields.password.touched).toBe(false);
|
||||
expect(result.formState.fields.password.validating).toBe(false);
|
||||
|
||||
expect(result.formState.fields.rememberMe.value).toBe(false);
|
||||
expect(result.formState.fields.rememberMe.error).toBeUndefined();
|
||||
expect(result.formState.fields.rememberMe.touched).toBe(false);
|
||||
expect(result.formState.fields.rememberMe.validating).toBe(false);
|
||||
});
|
||||
|
||||
it('should initialize form state with default values', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result.formState.isValid).toBe(true);
|
||||
expect(result.formState.isSubmitting).toBe(false);
|
||||
expect(result.formState.submitError).toBeUndefined();
|
||||
expect(result.formState.submitCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should initialize UI state flags correctly', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result.showPassword).toBe(false);
|
||||
expect(result.showErrorDetails).toBe(false);
|
||||
expect(result.isSubmitting).toBe(false);
|
||||
expect(result.submitError).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle special characters in returnTo path', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard?param=value&other=test',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/dashboard?param=value&other=test');
|
||||
});
|
||||
|
||||
it('should handle returnTo with hash fragment', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard#section',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/dashboard#section');
|
||||
});
|
||||
|
||||
it('should handle returnTo with encoded characters', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard?redirect=%2Fadmin',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/dashboard?redirect=%2Fadmin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('form state structure', () => {
|
||||
it('should have all required form fields', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
expect(result.formState.fields).toHaveProperty('email');
|
||||
expect(result.formState.fields).toHaveProperty('password');
|
||||
expect(result.formState.fields).toHaveProperty('rememberMe');
|
||||
});
|
||||
|
||||
it('should have consistent field state structure', () => {
|
||||
const loginPageDTO: LoginPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
hasInsufficientPermissions: false,
|
||||
};
|
||||
|
||||
const result = LoginViewDataBuilder.build(loginPageDTO);
|
||||
|
||||
const fields = result.formState.fields;
|
||||
Object.values(fields).forEach((field) => {
|
||||
expect(field).toHaveProperty('value');
|
||||
expect(field).toHaveProperty('error');
|
||||
expect(field).toHaveProperty('touched');
|
||||
expect(field).toHaveProperty('validating');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { OnboardingPageViewDataBuilder } from './OnboardingPageViewDataBuilder';
|
||||
|
||||
describe('OnboardingPageViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform driver data to ViewData correctly when driver exists', () => {
|
||||
const apiDto = { id: 'driver-123', name: 'Test Driver' };
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty object as driver data', () => {
|
||||
const apiDto = {};
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null driver data', () => {
|
||||
const apiDto = null;
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined driver data', () => {
|
||||
const apiDto = undefined;
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all driver data fields in the output', () => {
|
||||
const apiDto = {
|
||||
id: 'driver-123',
|
||||
name: 'Test Driver',
|
||||
email: 'test@example.com',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isAlreadyOnboarded).toBe(true);
|
||||
});
|
||||
|
||||
it('should not modify the input driver data', () => {
|
||||
const apiDto = { id: 'driver-123', name: 'Test Driver' };
|
||||
const originalDto = { ...apiDto };
|
||||
|
||||
OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(apiDto).toEqual(originalDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty string as driver data', () => {
|
||||
const apiDto = '';
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle zero as driver data', () => {
|
||||
const apiDto = 0;
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle false as driver data', () => {
|
||||
const apiDto = false;
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle array as driver data', () => {
|
||||
const apiDto = ['driver-123'];
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle function as driver data', () => {
|
||||
const apiDto = () => {};
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { OnboardingViewDataBuilder } from './OnboardingViewDataBuilder';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
|
||||
describe('OnboardingViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform successful onboarding check to ViewData correctly', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle already onboarded user correctly', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing isAlreadyOnboarded field with default false', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded?: boolean }, any> = Result.ok({});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should propagate unauthorized error', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('unauthorized');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('unauthorized');
|
||||
});
|
||||
|
||||
it('should propagate notFound error', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('notFound');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('notFound');
|
||||
});
|
||||
|
||||
it('should propagate serverError', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('serverError');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('serverError');
|
||||
});
|
||||
|
||||
it('should propagate networkError', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('networkError');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('networkError');
|
||||
});
|
||||
|
||||
it('should propagate validationError', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('validationError');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('validationError');
|
||||
});
|
||||
|
||||
it('should propagate unknown error', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('unknown');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.unwrap().isAlreadyOnboarded).toBe(false);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
|
||||
const originalDto = { ...apiDto.unwrap() };
|
||||
OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(apiDto.unwrap()).toEqual(originalDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle null isAlreadyOnboarded as false', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean | null }, any> = Result.ok({
|
||||
isAlreadyOnboarded: null,
|
||||
});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined isAlreadyOnboarded as false', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean | undefined }, any> = Result.ok({
|
||||
isAlreadyOnboarded: undefined,
|
||||
});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
187
apps/website/lib/builders/view-data/RacesViewDataBuilder.test.ts
Normal file
187
apps/website/lib/builders/view-data/RacesViewDataBuilder.test.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RacesViewDataBuilder } from './RacesViewDataBuilder';
|
||||
import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO';
|
||||
|
||||
describe('RacesViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform RacesPageDataDTO to RacesViewData correctly', () => {
|
||||
const now = new Date();
|
||||
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
const apiDto: RacesPageDataDTO = {
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
scheduledAt: pastDate.toISOString(),
|
||||
status: 'completed',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Pro League',
|
||||
strengthOfField: 1500,
|
||||
isUpcoming: false,
|
||||
isLive: false,
|
||||
isPast: true,
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
track: 'Monza',
|
||||
car: 'Ferrari 488 GT3',
|
||||
scheduledAt: futureDate.toISOString(),
|
||||
status: 'scheduled',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Pro League',
|
||||
strengthOfField: 1600,
|
||||
isUpcoming: true,
|
||||
isLive: false,
|
||||
isPast: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = RacesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races).toHaveLength(2);
|
||||
expect(result.totalCount).toBe(2);
|
||||
expect(result.completedCount).toBe(1);
|
||||
expect(result.scheduledCount).toBe(1);
|
||||
expect(result.leagues).toHaveLength(1);
|
||||
expect(result.leagues[0]).toEqual({ id: 'league-1', name: 'Pro League' });
|
||||
expect(result.upcomingRaces).toHaveLength(1);
|
||||
expect(result.upcomingRaces[0].id).toBe('race-2');
|
||||
expect(result.recentResults).toHaveLength(1);
|
||||
expect(result.recentResults[0].id).toBe('race-1');
|
||||
expect(result.racesByDate).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle empty races list', () => {
|
||||
const apiDto: RacesPageDataDTO = {
|
||||
races: [],
|
||||
};
|
||||
|
||||
const result = RacesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races).toHaveLength(0);
|
||||
expect(result.totalCount).toBe(0);
|
||||
expect(result.leagues).toHaveLength(0);
|
||||
expect(result.racesByDate).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const now = new Date();
|
||||
const apiDto: RacesPageDataDTO = {
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
scheduledAt: now.toISOString(),
|
||||
status: 'scheduled',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Pro League',
|
||||
strengthOfField: 1500,
|
||||
isUpcoming: true,
|
||||
isLive: false,
|
||||
isPast: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = RacesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].id).toBe(apiDto.races[0].id);
|
||||
expect(result.races[0].track).toBe(apiDto.races[0].track);
|
||||
expect(result.races[0].car).toBe(apiDto.races[0].car);
|
||||
expect(result.races[0].scheduledAt).toBe(apiDto.races[0].scheduledAt);
|
||||
expect(result.races[0].status).toBe(apiDto.races[0].status);
|
||||
expect(result.races[0].leagueId).toBe(apiDto.races[0].leagueId);
|
||||
expect(result.races[0].leagueName).toBe(apiDto.races[0].leagueName);
|
||||
expect(result.races[0].strengthOfField).toBe(apiDto.races[0].strengthOfField);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const now = new Date();
|
||||
const apiDto: RacesPageDataDTO = {
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
scheduledAt: now.toISOString(),
|
||||
status: 'scheduled',
|
||||
isUpcoming: true,
|
||||
isLive: false,
|
||||
isPast: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const originalDto = JSON.parse(JSON.stringify(apiDto));
|
||||
RacesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(apiDto).toEqual(originalDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle races with missing optional fields', () => {
|
||||
const now = new Date();
|
||||
const apiDto: RacesPageDataDTO = {
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
scheduledAt: now.toISOString(),
|
||||
status: 'scheduled',
|
||||
isUpcoming: true,
|
||||
isLive: false,
|
||||
isPast: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = RacesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.races[0].leagueId).toBeUndefined();
|
||||
expect(result.races[0].leagueName).toBeUndefined();
|
||||
expect(result.races[0].strengthOfField).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle multiple races on the same date', () => {
|
||||
const date = '2024-01-15T14:00:00.000Z';
|
||||
const apiDto: RacesPageDataDTO = {
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Spa',
|
||||
car: 'Porsche',
|
||||
scheduledAt: date,
|
||||
status: 'scheduled',
|
||||
isUpcoming: true,
|
||||
isLive: false,
|
||||
isPast: false,
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
track: 'Monza',
|
||||
car: 'Ferrari',
|
||||
scheduledAt: date,
|
||||
status: 'scheduled',
|
||||
isUpcoming: true,
|
||||
isLive: false,
|
||||
isPast: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = RacesViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.racesByDate).toHaveLength(1);
|
||||
expect(result.racesByDate[0].races).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ResetPasswordViewDataBuilder } from './ResetPasswordViewDataBuilder';
|
||||
import type { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO';
|
||||
|
||||
describe('ResetPasswordViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform ResetPasswordPageDTO to ResetPasswordViewData correctly', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result).toEqual({
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login',
|
||||
showSuccess: false,
|
||||
formState: {
|
||||
fields: {
|
||||
newPassword: { value: '', error: undefined, touched: false, validating: false },
|
||||
confirmPassword: { value: '', error: undefined, touched: false, validating: false },
|
||||
},
|
||||
isValid: true,
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
submitCount: 0,
|
||||
},
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty returnTo path', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('');
|
||||
});
|
||||
|
||||
it('should handle returnTo with query parameters', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login?success=true',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/login?success=true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.token).toBe(resetPasswordPageDTO.token);
|
||||
expect(result.returnTo).toBe(resetPasswordPageDTO.returnTo);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const originalDTO = { ...resetPasswordPageDTO };
|
||||
ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(resetPasswordPageDTO).toEqual(originalDTO);
|
||||
});
|
||||
|
||||
it('should initialize form fields with default values', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.formState.fields.newPassword.value).toBe('');
|
||||
expect(result.formState.fields.newPassword.error).toBeUndefined();
|
||||
expect(result.formState.fields.newPassword.touched).toBe(false);
|
||||
expect(result.formState.fields.newPassword.validating).toBe(false);
|
||||
|
||||
expect(result.formState.fields.confirmPassword.value).toBe('');
|
||||
expect(result.formState.fields.confirmPassword.error).toBeUndefined();
|
||||
expect(result.formState.fields.confirmPassword.touched).toBe(false);
|
||||
expect(result.formState.fields.confirmPassword.validating).toBe(false);
|
||||
});
|
||||
|
||||
it('should initialize form state with default values', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.formState.isValid).toBe(true);
|
||||
expect(result.formState.isSubmitting).toBe(false);
|
||||
expect(result.formState.submitError).toBeUndefined();
|
||||
expect(result.formState.submitCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should initialize UI state flags correctly', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.showSuccess).toBe(false);
|
||||
expect(result.isSubmitting).toBe(false);
|
||||
expect(result.submitError).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle token with special characters', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc-123_def.456',
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.token).toBe('abc-123_def.456');
|
||||
});
|
||||
|
||||
it('should handle token with URL-encoded characters', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc%20123%40def',
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.token).toBe('abc%20123%40def');
|
||||
});
|
||||
|
||||
it('should handle returnTo with encoded characters', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login?redirect=%2Fdashboard',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/login?redirect=%2Fdashboard');
|
||||
});
|
||||
|
||||
it('should handle returnTo with hash fragment', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login#section',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/login#section');
|
||||
});
|
||||
});
|
||||
|
||||
describe('form state structure', () => {
|
||||
it('should have all required form fields', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
expect(result.formState.fields).toHaveProperty('newPassword');
|
||||
expect(result.formState.fields).toHaveProperty('confirmPassword');
|
||||
});
|
||||
|
||||
it('should have consistent field state structure', () => {
|
||||
const resetPasswordPageDTO: ResetPasswordPageDTO = {
|
||||
token: 'abc123def456',
|
||||
returnTo: '/login',
|
||||
};
|
||||
|
||||
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
|
||||
|
||||
const fields = result.formState.fields;
|
||||
Object.values(fields).forEach((field) => {
|
||||
expect(field).toHaveProperty('value');
|
||||
expect(field).toHaveProperty('error');
|
||||
expect(field).toHaveProperty('touched');
|
||||
expect(field).toHaveProperty('validating');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SignupViewDataBuilder } from './SignupViewDataBuilder';
|
||||
import type { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO';
|
||||
|
||||
describe('SignupViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform SignupPageDTO to SignupViewData correctly', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(result).toEqual({
|
||||
returnTo: '/dashboard',
|
||||
formState: {
|
||||
fields: {
|
||||
firstName: { value: '', error: undefined, touched: false, validating: false },
|
||||
lastName: { value: '', error: undefined, touched: false, validating: false },
|
||||
email: { value: '', error: undefined, touched: false, validating: false },
|
||||
password: { value: '', error: undefined, touched: false, validating: false },
|
||||
confirmPassword: { value: '', error: undefined, touched: false, validating: false },
|
||||
},
|
||||
isValid: true,
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
submitCount: 0,
|
||||
},
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty returnTo path', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('');
|
||||
});
|
||||
|
||||
it('should handle returnTo with query parameters', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard?welcome=true',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/dashboard?welcome=true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe(signupPageDTO.returnTo);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
const originalDTO = { ...signupPageDTO };
|
||||
SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(signupPageDTO).toEqual(originalDTO);
|
||||
});
|
||||
|
||||
it('should initialize all signup form fields with default values', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(result.formState.fields.firstName.value).toBe('');
|
||||
expect(result.formState.fields.firstName.error).toBeUndefined();
|
||||
expect(result.formState.fields.firstName.touched).toBe(false);
|
||||
expect(result.formState.fields.firstName.validating).toBe(false);
|
||||
|
||||
expect(result.formState.fields.lastName.value).toBe('');
|
||||
expect(result.formState.fields.lastName.error).toBeUndefined();
|
||||
expect(result.formState.fields.lastName.touched).toBe(false);
|
||||
expect(result.formState.fields.lastName.validating).toBe(false);
|
||||
|
||||
expect(result.formState.fields.email.value).toBe('');
|
||||
expect(result.formState.fields.email.error).toBeUndefined();
|
||||
expect(result.formState.fields.email.touched).toBe(false);
|
||||
expect(result.formState.fields.email.validating).toBe(false);
|
||||
|
||||
expect(result.formState.fields.password.value).toBe('');
|
||||
expect(result.formState.fields.password.error).toBeUndefined();
|
||||
expect(result.formState.fields.password.touched).toBe(false);
|
||||
expect(result.formState.fields.password.validating).toBe(false);
|
||||
|
||||
expect(result.formState.fields.confirmPassword.value).toBe('');
|
||||
expect(result.formState.fields.confirmPassword.error).toBeUndefined();
|
||||
expect(result.formState.fields.confirmPassword.touched).toBe(false);
|
||||
expect(result.formState.fields.confirmPassword.validating).toBe(false);
|
||||
});
|
||||
|
||||
it('should initialize form state with default values', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(result.formState.isValid).toBe(true);
|
||||
expect(result.formState.isSubmitting).toBe(false);
|
||||
expect(result.formState.submitError).toBeUndefined();
|
||||
expect(result.formState.submitCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should initialize UI state flags correctly', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(result.isSubmitting).toBe(false);
|
||||
expect(result.submitError).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle returnTo with encoded characters', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard?redirect=%2Fadmin',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/dashboard?redirect=%2Fadmin');
|
||||
});
|
||||
|
||||
it('should handle returnTo with hash fragment', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard#section',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(result.returnTo).toBe('/dashboard#section');
|
||||
});
|
||||
});
|
||||
|
||||
describe('form state structure', () => {
|
||||
it('should have all required form fields', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
expect(result.formState.fields).toHaveProperty('firstName');
|
||||
expect(result.formState.fields).toHaveProperty('lastName');
|
||||
expect(result.formState.fields).toHaveProperty('email');
|
||||
expect(result.formState.fields).toHaveProperty('password');
|
||||
expect(result.formState.fields).toHaveProperty('confirmPassword');
|
||||
});
|
||||
|
||||
it('should have consistent field state structure', () => {
|
||||
const signupPageDTO: SignupPageDTO = {
|
||||
returnTo: '/dashboard',
|
||||
};
|
||||
|
||||
const result = SignupViewDataBuilder.build(signupPageDTO);
|
||||
|
||||
const fields = result.formState.fields;
|
||||
Object.values(fields).forEach((field) => {
|
||||
expect(field).toHaveProperty('value');
|
||||
expect(field).toHaveProperty('error');
|
||||
expect(field).toHaveProperty('touched');
|
||||
expect(field).toHaveProperty('validating');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SponsorDashboardViewDataBuilder } from './SponsorDashboardViewDataBuilder';
|
||||
import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
|
||||
|
||||
describe('SponsorDashboardViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform SponsorDashboardDTO to SponsorDashboardViewData correctly', () => {
|
||||
const apiDto: SponsorDashboardDTO = {
|
||||
sponsorName: 'Test Sponsor',
|
||||
metrics: {
|
||||
impressions: 5000,
|
||||
viewers: 1000,
|
||||
exposure: 500,
|
||||
},
|
||||
investment: {
|
||||
activeSponsorships: 5,
|
||||
totalSpent: 5000,
|
||||
},
|
||||
sponsorships: [],
|
||||
};
|
||||
|
||||
const result = SponsorDashboardViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.sponsorName).toBe('Test Sponsor');
|
||||
expect(result.totalImpressions).toBe('5,000');
|
||||
expect(result.totalInvestment).toBe('$5,000.00');
|
||||
expect(result.activeSponsorships).toBe(5);
|
||||
expect(result.metrics.impressionsChange).toBe(15);
|
||||
});
|
||||
|
||||
it('should handle low impressions correctly', () => {
|
||||
const apiDto: SponsorDashboardDTO = {
|
||||
sponsorName: 'Test Sponsor',
|
||||
metrics: {
|
||||
impressions: 500,
|
||||
viewers: 100,
|
||||
exposure: 50,
|
||||
},
|
||||
investment: {
|
||||
activeSponsorships: 1,
|
||||
totalSpent: 1000,
|
||||
},
|
||||
sponsorships: [],
|
||||
};
|
||||
|
||||
const result = SponsorDashboardViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.metrics.impressionsChange).toBe(-5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const apiDto: SponsorDashboardDTO = {
|
||||
sponsorName: 'Test Sponsor',
|
||||
metrics: {
|
||||
impressions: 5000,
|
||||
viewers: 1000,
|
||||
exposure: 500,
|
||||
},
|
||||
investment: {
|
||||
activeSponsorships: 5,
|
||||
totalSpent: 5000,
|
||||
},
|
||||
sponsorships: [],
|
||||
};
|
||||
|
||||
const result = SponsorDashboardViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.sponsorName).toBe(apiDto.sponsorName);
|
||||
expect(result.activeSponsorships).toBe(apiDto.investment.activeSponsorships);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const apiDto: SponsorDashboardDTO = {
|
||||
sponsorName: 'Test Sponsor',
|
||||
metrics: {
|
||||
impressions: 5000,
|
||||
viewers: 1000,
|
||||
exposure: 500,
|
||||
},
|
||||
investment: {
|
||||
activeSponsorships: 5,
|
||||
totalSpent: 5000,
|
||||
},
|
||||
sponsorships: [],
|
||||
};
|
||||
|
||||
const originalDto = JSON.parse(JSON.stringify(apiDto));
|
||||
SponsorDashboardViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(apiDto).toEqual(originalDto);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SponsorLogoViewDataBuilder } from './SponsorLogoViewDataBuilder';
|
||||
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
|
||||
describe('SponsorLogoViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform MediaBinaryDTO to SponsorLogoViewData correctly', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle JPEG sponsor logos', () => {
|
||||
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/jpeg',
|
||||
};
|
||||
|
||||
const result = SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle SVG sponsor logos', () => {
|
||||
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><text x="10" y="20">Sponsor</text></svg>');
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/svg+xml',
|
||||
};
|
||||
|
||||
const result = SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/svg+xml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBeDefined();
|
||||
expect(result.contentType).toBe(mediaDto.contentType);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const originalDto = { ...mediaDto };
|
||||
SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(mediaDto).toEqual(originalDto);
|
||||
});
|
||||
|
||||
it('should convert buffer to base64 string', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(typeof result.buffer).toBe('string');
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty buffer', () => {
|
||||
const buffer = new Uint8Array([]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe('');
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle large sponsor logos', () => {
|
||||
const buffer = new Uint8Array(3 * 1024 * 1024); // 3MB
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/jpeg',
|
||||
};
|
||||
|
||||
const result = SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle buffer with all zeros', () => {
|
||||
const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle buffer with all ones', () => {
|
||||
const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle different content types', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const contentTypes = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
'image/bmp',
|
||||
'image/tiff',
|
||||
];
|
||||
|
||||
contentTypes.forEach((contentType) => {
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType,
|
||||
};
|
||||
|
||||
const result = SponsorLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.contentType).toBe(contentType);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TeamLogoViewDataBuilder } from './TeamLogoViewDataBuilder';
|
||||
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
|
||||
describe('TeamLogoViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform MediaBinaryDTO to TeamLogoViewData correctly', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TeamLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle JPEG team logos', () => {
|
||||
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/jpeg',
|
||||
};
|
||||
|
||||
const result = TeamLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle SVG team logos', () => {
|
||||
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="40"/></svg>');
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/svg+xml',
|
||||
};
|
||||
|
||||
const result = TeamLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/svg+xml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TeamLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBeDefined();
|
||||
expect(result.contentType).toBe(mediaDto.contentType);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const originalDto = { ...mediaDto };
|
||||
TeamLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(mediaDto).toEqual(originalDto);
|
||||
});
|
||||
|
||||
it('should convert buffer to base64 string', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TeamLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(typeof result.buffer).toBe('string');
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty buffer', () => {
|
||||
const buffer = new Uint8Array([]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TeamLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe('');
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle small logo files', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TeamLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle buffer with special characters', () => {
|
||||
const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TeamLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle different content types', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const contentTypes = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
'image/bmp',
|
||||
'image/tiff',
|
||||
];
|
||||
|
||||
contentTypes.forEach((contentType) => {
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType,
|
||||
};
|
||||
|
||||
const result = TeamLogoViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.contentType).toBe(contentType);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,430 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TeamRankingsViewDataBuilder } from './TeamRankingsViewDataBuilder';
|
||||
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
|
||||
|
||||
describe('TeamRankingsViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform GetTeamsLeaderboardOutputDTO to TeamRankingsViewData correctly', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo1.jpg',
|
||||
memberCount: 15,
|
||||
rating: 1500,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
{
|
||||
id: 'team-2',
|
||||
name: 'Speed Demons',
|
||||
tag: 'SD',
|
||||
logoUrl: 'https://example.com/logo2.jpg',
|
||||
memberCount: 8,
|
||||
rating: 1200,
|
||||
totalWins: 20,
|
||||
totalRaces: 150,
|
||||
performanceLevel: 'advanced',
|
||||
isRecruiting: true,
|
||||
createdAt: '2023-06-01',
|
||||
},
|
||||
{
|
||||
id: 'team-3',
|
||||
name: 'Rookie Racers',
|
||||
tag: 'RR',
|
||||
logoUrl: 'https://example.com/logo3.jpg',
|
||||
memberCount: 5,
|
||||
rating: 800,
|
||||
totalWins: 5,
|
||||
totalRaces: 50,
|
||||
performanceLevel: 'intermediate',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-09-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 5,
|
||||
groupsBySkillLevel: 'elite,advanced,intermediate',
|
||||
topTeams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo1.jpg',
|
||||
memberCount: 15,
|
||||
rating: 1500,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
{
|
||||
id: 'team-2',
|
||||
name: 'Speed Demons',
|
||||
tag: 'SD',
|
||||
logoUrl: 'https://example.com/logo2.jpg',
|
||||
memberCount: 8,
|
||||
rating: 1200,
|
||||
totalWins: 20,
|
||||
totalRaces: 150,
|
||||
performanceLevel: 'advanced',
|
||||
isRecruiting: true,
|
||||
createdAt: '2023-06-01',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
// Verify teams
|
||||
expect(result.teams).toHaveLength(3);
|
||||
expect(result.teams[0].id).toBe('team-1');
|
||||
expect(result.teams[0].name).toBe('Racing Team Alpha');
|
||||
expect(result.teams[0].tag).toBe('RTA');
|
||||
expect(result.teams[0].memberCount).toBe(15);
|
||||
expect(result.teams[0].totalWins).toBe(50);
|
||||
expect(result.teams[0].totalRaces).toBe(200);
|
||||
expect(result.teams[0].logoUrl).toBe('https://example.com/logo1.jpg');
|
||||
expect(result.teams[0].position).toBe(1);
|
||||
expect(result.teams[0].isRecruiting).toBe(false);
|
||||
expect(result.teams[0].performanceLevel).toBe('elite');
|
||||
expect(result.teams[0].rating).toBe(1500);
|
||||
expect(result.teams[0].category).toBeUndefined();
|
||||
|
||||
// Verify podium (top 3)
|
||||
expect(result.podium).toHaveLength(3);
|
||||
expect(result.podium[0].id).toBe('team-1');
|
||||
expect(result.podium[0].position).toBe(1);
|
||||
expect(result.podium[1].id).toBe('team-2');
|
||||
expect(result.podium[1].position).toBe(2);
|
||||
expect(result.podium[2].id).toBe('team-3');
|
||||
expect(result.podium[2].position).toBe(3);
|
||||
|
||||
// Verify recruiting count
|
||||
expect(result.recruitingCount).toBe(5);
|
||||
});
|
||||
|
||||
it('should handle empty team array', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams).toEqual([]);
|
||||
expect(result.podium).toEqual([]);
|
||||
expect(result.recruitingCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle less than 3 teams for podium', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo1.jpg',
|
||||
memberCount: 15,
|
||||
rating: 1500,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
{
|
||||
id: 'team-2',
|
||||
name: 'Speed Demons',
|
||||
tag: 'SD',
|
||||
logoUrl: 'https://example.com/logo2.jpg',
|
||||
memberCount: 8,
|
||||
rating: 1200,
|
||||
totalWins: 20,
|
||||
totalRaces: 150,
|
||||
performanceLevel: 'advanced',
|
||||
isRecruiting: true,
|
||||
createdAt: '2023-06-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 2,
|
||||
groupsBySkillLevel: 'elite,advanced',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams).toHaveLength(2);
|
||||
expect(result.podium).toHaveLength(2);
|
||||
expect(result.podium[0].position).toBe(1);
|
||||
expect(result.podium[1].position).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle missing avatar URLs with empty string fallback', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
memberCount: 15,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams[0].logoUrl).toBe('');
|
||||
});
|
||||
|
||||
it('should calculate position based on index', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{ id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' },
|
||||
{ id: 'team-2', name: 'Team 2', tag: 'T2', memberCount: 8, totalWins: 20, totalRaces: 120, performanceLevel: 'advanced', isRecruiting: true, createdAt: '2023-02-01' },
|
||||
{ id: 'team-3', name: 'Team 3', tag: 'T3', memberCount: 6, totalWins: 10, totalRaces: 80, performanceLevel: 'intermediate', isRecruiting: false, createdAt: '2023-03-01' },
|
||||
{ id: 'team-4', name: 'Team 4', tag: 'T4', memberCount: 4, totalWins: 5, totalRaces: 40, performanceLevel: 'beginner', isRecruiting: true, createdAt: '2023-04-01' },
|
||||
],
|
||||
recruitingCount: 2,
|
||||
groupsBySkillLevel: 'elite,advanced,intermediate,beginner',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams[0].position).toBe(1);
|
||||
expect(result.teams[1].position).toBe(2);
|
||||
expect(result.teams[2].position).toBe(3);
|
||||
expect(result.teams[3].position).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-123',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo.jpg',
|
||||
memberCount: 15,
|
||||
rating: 1500,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 5,
|
||||
groupsBySkillLevel: 'elite,advanced',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams[0].name).toBe(teamDTO.teams[0].name);
|
||||
expect(result.teams[0].tag).toBe(teamDTO.teams[0].tag);
|
||||
expect(result.teams[0].logoUrl).toBe(teamDTO.teams[0].logoUrl);
|
||||
expect(result.teams[0].memberCount).toBe(teamDTO.teams[0].memberCount);
|
||||
expect(result.teams[0].rating).toBe(teamDTO.teams[0].rating);
|
||||
expect(result.teams[0].totalWins).toBe(teamDTO.teams[0].totalWins);
|
||||
expect(result.teams[0].totalRaces).toBe(teamDTO.teams[0].totalRaces);
|
||||
expect(result.teams[0].performanceLevel).toBe(teamDTO.teams[0].performanceLevel);
|
||||
expect(result.teams[0].isRecruiting).toBe(teamDTO.teams[0].isRecruiting);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-123',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo.jpg',
|
||||
memberCount: 15,
|
||||
rating: 1500,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 5,
|
||||
groupsBySkillLevel: 'elite,advanced',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const originalDTO = JSON.parse(JSON.stringify(teamDTO));
|
||||
TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(teamDTO).toEqual(originalDTO);
|
||||
});
|
||||
|
||||
it('should handle large numbers correctly', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: 'https://example.com/logo.jpg',
|
||||
memberCount: 100,
|
||||
rating: 999999,
|
||||
totalWins: 5000,
|
||||
totalRaces: 10000,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams[0].rating).toBe(999999);
|
||||
expect(result.teams[0].totalWins).toBe(5000);
|
||||
expect(result.teams[0].totalRaces).toBe(10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle null/undefined logo URLs', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
logoUrl: null as any,
|
||||
memberCount: 15,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams[0].logoUrl).toBe('');
|
||||
});
|
||||
|
||||
it('should handle null/undefined rating', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
memberCount: 15,
|
||||
rating: null as any,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams[0].rating).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle null/undefined totalWins and totalRaces', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
memberCount: 15,
|
||||
totalWins: null as any,
|
||||
totalRaces: null as any,
|
||||
performanceLevel: 'elite',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams[0].totalWins).toBe(0);
|
||||
expect(result.teams[0].totalRaces).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle empty performance level', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
tag: 'RTA',
|
||||
memberCount: 15,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
performanceLevel: '',
|
||||
isRecruiting: false,
|
||||
createdAt: '2023-01-01',
|
||||
},
|
||||
],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams[0].performanceLevel).toBe('N/A');
|
||||
});
|
||||
|
||||
it('should handle position 0', () => {
|
||||
const teamDTO: GetTeamsLeaderboardOutputDTO = {
|
||||
teams: [
|
||||
{ id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' },
|
||||
],
|
||||
recruitingCount: 0,
|
||||
groupsBySkillLevel: '',
|
||||
topTeams: [],
|
||||
};
|
||||
|
||||
const result = TeamRankingsViewDataBuilder.build(teamDTO);
|
||||
|
||||
expect(result.teams[0].position).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
157
apps/website/lib/builders/view-data/TeamsViewDataBuilder.test.ts
Normal file
157
apps/website/lib/builders/view-data/TeamsViewDataBuilder.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TeamsViewDataBuilder } from './TeamsViewDataBuilder';
|
||||
|
||||
describe('TeamsViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform TeamsPageDto to TeamsViewData correctly', () => {
|
||||
const apiDto = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Racing Team Alpha',
|
||||
memberCount: 15,
|
||||
logoUrl: 'https://example.com/logo1.jpg',
|
||||
rating: 1500,
|
||||
totalWins: 50,
|
||||
totalRaces: 200,
|
||||
region: 'USA',
|
||||
isRecruiting: false,
|
||||
category: 'competitive',
|
||||
performanceLevel: 'elite',
|
||||
description: 'A top-tier racing team',
|
||||
},
|
||||
{
|
||||
id: 'team-2',
|
||||
name: 'Speed Demons',
|
||||
memberCount: 8,
|
||||
logoUrl: 'https://example.com/logo2.jpg',
|
||||
rating: 1200,
|
||||
totalWins: 20,
|
||||
totalRaces: 150,
|
||||
region: 'UK',
|
||||
isRecruiting: true,
|
||||
category: 'casual',
|
||||
performanceLevel: 'advanced',
|
||||
description: 'Fast and fun',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = TeamsViewDataBuilder.build(apiDto as any);
|
||||
|
||||
expect(result.teams).toHaveLength(2);
|
||||
expect(result.teams[0]).toEqual({
|
||||
teamId: 'team-1',
|
||||
teamName: 'Racing Team Alpha',
|
||||
memberCount: 15,
|
||||
logoUrl: 'https://example.com/logo1.jpg',
|
||||
ratingLabel: '1,500',
|
||||
ratingValue: 1500,
|
||||
winsLabel: '50',
|
||||
racesLabel: '200',
|
||||
region: 'USA',
|
||||
isRecruiting: false,
|
||||
category: 'competitive',
|
||||
performanceLevel: 'elite',
|
||||
description: 'A top-tier racing team',
|
||||
countryCode: 'USA',
|
||||
});
|
||||
expect(result.teams[1]).toEqual({
|
||||
teamId: 'team-2',
|
||||
teamName: 'Speed Demons',
|
||||
memberCount: 8,
|
||||
logoUrl: 'https://example.com/logo2.jpg',
|
||||
ratingLabel: '1,200',
|
||||
ratingValue: 1200,
|
||||
winsLabel: '20',
|
||||
racesLabel: '150',
|
||||
region: 'UK',
|
||||
isRecruiting: true,
|
||||
category: 'casual',
|
||||
performanceLevel: 'advanced',
|
||||
description: 'Fast and fun',
|
||||
countryCode: 'UK',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty teams list', () => {
|
||||
const apiDto = {
|
||||
teams: [],
|
||||
};
|
||||
|
||||
const result = TeamsViewDataBuilder.build(apiDto as any);
|
||||
|
||||
expect(result.teams).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle teams with missing optional fields', () => {
|
||||
const apiDto = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Minimal Team',
|
||||
memberCount: 5,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = TeamsViewDataBuilder.build(apiDto as any);
|
||||
|
||||
expect(result.teams[0].ratingValue).toBe(0);
|
||||
expect(result.teams[0].winsLabel).toBe('0');
|
||||
expect(result.teams[0].racesLabel).toBe('0');
|
||||
expect(result.teams[0].logoUrl).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const apiDto = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Test Team',
|
||||
memberCount: 10,
|
||||
rating: 1000,
|
||||
totalWins: 5,
|
||||
totalRaces: 20,
|
||||
region: 'EU',
|
||||
isRecruiting: true,
|
||||
category: 'test',
|
||||
performanceLevel: 'test-level',
|
||||
description: 'test-desc',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = TeamsViewDataBuilder.build(apiDto as any);
|
||||
|
||||
expect(result.teams[0].teamId).toBe(apiDto.teams[0].id);
|
||||
expect(result.teams[0].teamName).toBe(apiDto.teams[0].name);
|
||||
expect(result.teams[0].memberCount).toBe(apiDto.teams[0].memberCount);
|
||||
expect(result.teams[0].ratingValue).toBe(apiDto.teams[0].rating);
|
||||
expect(result.teams[0].region).toBe(apiDto.teams[0].region);
|
||||
expect(result.teams[0].isRecruiting).toBe(apiDto.teams[0].isRecruiting);
|
||||
expect(result.teams[0].category).toBe(apiDto.teams[0].category);
|
||||
expect(result.teams[0].performanceLevel).toBe(apiDto.teams[0].performanceLevel);
|
||||
expect(result.teams[0].description).toBe(apiDto.teams[0].description);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const apiDto = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Test Team',
|
||||
memberCount: 10,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const originalDto = JSON.parse(JSON.stringify(apiDto));
|
||||
TeamsViewDataBuilder.build(apiDto as any);
|
||||
|
||||
expect(apiDto).toEqual(originalDto);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TrackImageViewDataBuilder } from './TrackImageViewDataBuilder';
|
||||
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
|
||||
describe('TrackImageViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform MediaBinaryDTO to TrackImageViewData correctly', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle JPEG track images', () => {
|
||||
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/jpeg',
|
||||
};
|
||||
|
||||
const result = TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle WebP track images', () => {
|
||||
const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/webp',
|
||||
};
|
||||
|
||||
const result = TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/webp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBeDefined();
|
||||
expect(result.contentType).toBe(mediaDto.contentType);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const originalDto = { ...mediaDto };
|
||||
TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(mediaDto).toEqual(originalDto);
|
||||
});
|
||||
|
||||
it('should convert buffer to base64 string', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(typeof result.buffer).toBe('string');
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty buffer', () => {
|
||||
const buffer = new Uint8Array([]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe('');
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle large track images', () => {
|
||||
const buffer = new Uint8Array(5 * 1024 * 1024); // 5MB
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/jpeg',
|
||||
};
|
||||
|
||||
const result = TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle buffer with all zeros', () => {
|
||||
const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle buffer with all ones', () => {
|
||||
const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
|
||||
const result = TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
|
||||
expect(result.contentType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle different content types', () => {
|
||||
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
const contentTypes = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
'image/bmp',
|
||||
'image/tiff',
|
||||
];
|
||||
|
||||
contentTypes.forEach((contentType) => {
|
||||
const mediaDto: MediaBinaryDTO = {
|
||||
buffer: buffer.buffer,
|
||||
contentType,
|
||||
};
|
||||
|
||||
const result = TrackImageViewDataBuilder.build(mediaDto);
|
||||
|
||||
expect(result.contentType).toBe(contentType);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DashboardConsistencyDisplay } from './DashboardConsistencyDisplay';
|
||||
|
||||
describe('DashboardConsistencyDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format consistency correctly', () => {
|
||||
expect(DashboardConsistencyDisplay.format(0)).toBe('0%');
|
||||
expect(DashboardConsistencyDisplay.format(50)).toBe('50%');
|
||||
expect(DashboardConsistencyDisplay.format(100)).toBe('100%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle decimal consistency', () => {
|
||||
expect(DashboardConsistencyDisplay.format(85.5)).toBe('85.5%');
|
||||
expect(DashboardConsistencyDisplay.format(99.9)).toBe('99.9%');
|
||||
});
|
||||
|
||||
it('should handle negative consistency', () => {
|
||||
expect(DashboardConsistencyDisplay.format(-10)).toBe('-10%');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DashboardCountDisplay } from './DashboardCountDisplay';
|
||||
|
||||
describe('DashboardCountDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format positive numbers correctly', () => {
|
||||
expect(DashboardCountDisplay.format(0)).toBe('0');
|
||||
expect(DashboardCountDisplay.format(1)).toBe('1');
|
||||
expect(DashboardCountDisplay.format(100)).toBe('100');
|
||||
expect(DashboardCountDisplay.format(1000)).toBe('1000');
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
expect(DashboardCountDisplay.format(null)).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle undefined values', () => {
|
||||
expect(DashboardCountDisplay.format(undefined)).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle negative numbers', () => {
|
||||
expect(DashboardCountDisplay.format(-1)).toBe('-1');
|
||||
expect(DashboardCountDisplay.format(-100)).toBe('-100');
|
||||
});
|
||||
|
||||
it('should handle large numbers', () => {
|
||||
expect(DashboardCountDisplay.format(999999)).toBe('999999');
|
||||
expect(DashboardCountDisplay.format(1000000)).toBe('1000000');
|
||||
});
|
||||
|
||||
it('should handle decimal numbers', () => {
|
||||
expect(DashboardCountDisplay.format(1.5)).toBe('1.5');
|
||||
expect(DashboardCountDisplay.format(100.99)).toBe('100.99');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DashboardDateDisplay } from './DashboardDateDisplay';
|
||||
|
||||
describe('DashboardDateDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format future date correctly', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours from now
|
||||
|
||||
const result = DashboardDateDisplay.format(futureDate);
|
||||
|
||||
expect(result.date).toMatch(/^[A-Za-z]{3}, [A-Za-z]{3} \d{1,2}, \d{4}$/);
|
||||
expect(result.time).toMatch(/^\d{2}:\d{2}$/);
|
||||
expect(result.relative).toBe('1d');
|
||||
});
|
||||
|
||||
it('should format date less than 24 hours correctly', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 6 * 60 * 60 * 1000); // 6 hours from now
|
||||
|
||||
const result = DashboardDateDisplay.format(futureDate);
|
||||
|
||||
expect(result.relative).toBe('6h');
|
||||
});
|
||||
|
||||
it('should format date more than 24 hours correctly', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 48 * 60 * 60 * 1000); // 2 days from now
|
||||
|
||||
const result = DashboardDateDisplay.format(futureDate);
|
||||
|
||||
expect(result.relative).toBe('2d');
|
||||
});
|
||||
|
||||
it('should format past date correctly', () => {
|
||||
const now = new Date();
|
||||
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago
|
||||
|
||||
const result = DashboardDateDisplay.format(pastDate);
|
||||
|
||||
expect(result.relative).toBe('Past');
|
||||
});
|
||||
|
||||
it('should format current date correctly', () => {
|
||||
const now = new Date();
|
||||
|
||||
const result = DashboardDateDisplay.format(now);
|
||||
|
||||
expect(result.relative).toBe('Now');
|
||||
});
|
||||
|
||||
it('should format date with leading zeros in time', () => {
|
||||
const date = new Date('2024-01-15T05:03:00');
|
||||
|
||||
const result = DashboardDateDisplay.format(date);
|
||||
|
||||
expect(result.time).toBe('05:03');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle midnight correctly', () => {
|
||||
const date = new Date('2024-01-15T00:00:00');
|
||||
|
||||
const result = DashboardDateDisplay.format(date);
|
||||
|
||||
expect(result.time).toBe('00:00');
|
||||
});
|
||||
|
||||
it('should handle end of day correctly', () => {
|
||||
const date = new Date('2024-01-15T23:59:59');
|
||||
|
||||
const result = DashboardDateDisplay.format(date);
|
||||
|
||||
expect(result.time).toBe('23:59');
|
||||
});
|
||||
|
||||
it('should handle different days of week', () => {
|
||||
const date = new Date('2024-01-15'); // Monday
|
||||
|
||||
const result = DashboardDateDisplay.format(date);
|
||||
|
||||
expect(result.date).toContain('Mon');
|
||||
});
|
||||
|
||||
it('should handle different months', () => {
|
||||
const date = new Date('2024-01-15');
|
||||
|
||||
const result = DashboardDateDisplay.format(date);
|
||||
|
||||
expect(result.date).toContain('Jan');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DashboardLeaguePositionDisplay } from './DashboardLeaguePositionDisplay';
|
||||
|
||||
describe('DashboardLeaguePositionDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format position correctly', () => {
|
||||
expect(DashboardLeaguePositionDisplay.format(1)).toBe('#1');
|
||||
expect(DashboardLeaguePositionDisplay.format(5)).toBe('#5');
|
||||
expect(DashboardLeaguePositionDisplay.format(100)).toBe('#100');
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
expect(DashboardLeaguePositionDisplay.format(null)).toBe('-');
|
||||
});
|
||||
|
||||
it('should handle undefined values', () => {
|
||||
expect(DashboardLeaguePositionDisplay.format(undefined)).toBe('-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle position 0', () => {
|
||||
expect(DashboardLeaguePositionDisplay.format(0)).toBe('#0');
|
||||
});
|
||||
|
||||
it('should handle large positions', () => {
|
||||
expect(DashboardLeaguePositionDisplay.format(999)).toBe('#999');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DashboardRankDisplay } from './DashboardRankDisplay';
|
||||
|
||||
describe('DashboardRankDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format rank correctly', () => {
|
||||
expect(DashboardRankDisplay.format(1)).toBe('1');
|
||||
expect(DashboardRankDisplay.format(42)).toBe('42');
|
||||
expect(DashboardRankDisplay.format(100)).toBe('100');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle rank 0', () => {
|
||||
expect(DashboardRankDisplay.format(0)).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle large ranks', () => {
|
||||
expect(DashboardRankDisplay.format(999999)).toBe('999999');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,369 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DashboardViewDataBuilder } from '../builders/view-data/DashboardViewDataBuilder';
|
||||
import { DashboardDateDisplay } from './DashboardDateDisplay';
|
||||
import { DashboardCountDisplay } from './DashboardCountDisplay';
|
||||
import { DashboardRankDisplay } from './DashboardRankDisplay';
|
||||
import { DashboardConsistencyDisplay } from './DashboardConsistencyDisplay';
|
||||
import { DashboardLeaguePositionDisplay } from './DashboardLeaguePositionDisplay';
|
||||
import { RatingDisplay } from './RatingDisplay';
|
||||
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
|
||||
|
||||
describe('Dashboard View Data - Cross-Component Consistency', () => {
|
||||
describe('common patterns', () => {
|
||||
it('should all use consistent formatting for numeric values', () => {
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
rating: 1234.56,
|
||||
globalRank: 42,
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
consistency: 85,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 3,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [
|
||||
{
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Test League',
|
||||
position: 5,
|
||||
totalDrivers: 50,
|
||||
points: 1250,
|
||||
},
|
||||
],
|
||||
feedSummary: {
|
||||
notificationCount: 0,
|
||||
items: [],
|
||||
},
|
||||
friends: [
|
||||
{ id: 'friend-1', name: 'Alice', country: 'UK' },
|
||||
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
// All numeric values should be formatted as strings
|
||||
expect(typeof result.currentDriver.rating).toBe('string');
|
||||
expect(typeof result.currentDriver.rank).toBe('string');
|
||||
expect(typeof result.currentDriver.totalRaces).toBe('string');
|
||||
expect(typeof result.currentDriver.wins).toBe('string');
|
||||
expect(typeof result.currentDriver.podiums).toBe('string');
|
||||
expect(typeof result.currentDriver.consistency).toBe('string');
|
||||
expect(typeof result.activeLeaguesCount).toBe('string');
|
||||
expect(typeof result.friendCount).toBe('string');
|
||||
expect(typeof result.leagueStandings[0].position).toBe('string');
|
||||
expect(typeof result.leagueStandings[0].points).toBe('string');
|
||||
expect(typeof result.leagueStandings[0].totalDrivers).toBe('string');
|
||||
});
|
||||
|
||||
it('should all handle missing data gracefully', () => {
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 0,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 0,
|
||||
items: [],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
// All fields should have safe defaults
|
||||
expect(result.currentDriver.name).toBe('');
|
||||
expect(result.currentDriver.avatarUrl).toBe('');
|
||||
expect(result.currentDriver.country).toBe('');
|
||||
expect(result.currentDriver.rating).toBe('0.0');
|
||||
expect(result.currentDriver.rank).toBe('0');
|
||||
expect(result.currentDriver.totalRaces).toBe('0');
|
||||
expect(result.currentDriver.wins).toBe('0');
|
||||
expect(result.currentDriver.podiums).toBe('0');
|
||||
expect(result.currentDriver.consistency).toBe('0%');
|
||||
expect(result.nextRace).toBeNull();
|
||||
expect(result.upcomingRaces).toEqual([]);
|
||||
expect(result.leagueStandings).toEqual([]);
|
||||
expect(result.feedItems).toEqual([]);
|
||||
expect(result.friends).toEqual([]);
|
||||
expect(result.activeLeaguesCount).toBe('0');
|
||||
expect(result.friendCount).toBe('0');
|
||||
});
|
||||
|
||||
it('should all preserve ISO timestamps for serialization', () => {
|
||||
const now = new Date();
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
const feedTimestamp = new Date(now.getTime() - 30 * 60 * 1000);
|
||||
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 1,
|
||||
nextRace: {
|
||||
id: 'race-1',
|
||||
track: 'Spa',
|
||||
car: 'Porsche',
|
||||
scheduledAt: futureDate.toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: true,
|
||||
},
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 1,
|
||||
items: [
|
||||
{
|
||||
id: 'feed-1',
|
||||
type: 'notification',
|
||||
headline: 'Test',
|
||||
timestamp: feedTimestamp.toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
// All timestamps should be preserved as ISO strings
|
||||
expect(result.nextRace?.scheduledAt).toBe(futureDate.toISOString());
|
||||
expect(result.feedItems[0].timestamp).toBe(feedTimestamp.toISOString());
|
||||
});
|
||||
|
||||
it('should all handle boolean flags correctly', () => {
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Spa',
|
||||
car: 'Porsche',
|
||||
scheduledAt: new Date().toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: true,
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
track: 'Monza',
|
||||
car: 'Ferrari',
|
||||
scheduledAt: new Date().toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: false,
|
||||
},
|
||||
],
|
||||
activeLeaguesCount: 1,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 0,
|
||||
items: [],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
expect(result.upcomingRaces[0].isMyLeague).toBe(true);
|
||||
expect(result.upcomingRaces[1].isMyLeague).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data integrity', () => {
|
||||
it('should maintain data consistency across transformations', () => {
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
rating: 1234.56,
|
||||
globalRank: 42,
|
||||
totalRaces: 150,
|
||||
wins: 25,
|
||||
podiums: 60,
|
||||
consistency: 85,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 3,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 5,
|
||||
items: [],
|
||||
},
|
||||
friends: [
|
||||
{ id: 'friend-1', name: 'Alice', country: 'UK' },
|
||||
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
// Verify derived fields match their source data
|
||||
expect(result.friendCount).toBe(dashboardDTO.friends.length.toString());
|
||||
expect(result.activeLeaguesCount).toBe(dashboardDTO.activeLeaguesCount.toString());
|
||||
expect(result.hasFriends).toBe(dashboardDTO.friends.length > 0);
|
||||
expect(result.hasUpcomingRaces).toBe(dashboardDTO.upcomingRaces.length > 0);
|
||||
expect(result.hasLeagueStandings).toBe(dashboardDTO.leagueStandingsSummaries.length > 0);
|
||||
expect(result.hasFeedItems).toBe(dashboardDTO.feedSummary.items.length > 0);
|
||||
});
|
||||
|
||||
it('should handle complex real-world scenarios', () => {
|
||||
const now = new Date();
|
||||
const race1Date = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000);
|
||||
const race2Date = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
|
||||
const feedTimestamp = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
|
||||
const dashboardDTO: DashboardOverviewDTO = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'John Doe',
|
||||
country: 'USA',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
rating: 2456.78,
|
||||
globalRank: 15,
|
||||
totalRaces: 250,
|
||||
wins: 45,
|
||||
podiums: 120,
|
||||
consistency: 92.5,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [
|
||||
{
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Pro League',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
scheduledAt: race1Date.toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: true,
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
track: 'Monza',
|
||||
car: 'Ferrari 488 GT3',
|
||||
scheduledAt: race2Date.toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: false,
|
||||
},
|
||||
],
|
||||
activeLeaguesCount: 2,
|
||||
nextRace: {
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Pro League',
|
||||
track: 'Spa',
|
||||
car: 'Porsche 911 GT3',
|
||||
scheduledAt: race1Date.toISOString(),
|
||||
status: 'scheduled',
|
||||
isMyLeague: true,
|
||||
},
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [
|
||||
{
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'Pro League',
|
||||
position: 3,
|
||||
totalDrivers: 100,
|
||||
points: 2450,
|
||||
},
|
||||
{
|
||||
leagueId: 'league-2',
|
||||
leagueName: 'Rookie League',
|
||||
position: 1,
|
||||
totalDrivers: 50,
|
||||
points: 1800,
|
||||
},
|
||||
],
|
||||
feedSummary: {
|
||||
notificationCount: 3,
|
||||
items: [
|
||||
{
|
||||
id: 'feed-1',
|
||||
type: 'race_result',
|
||||
headline: 'Race completed',
|
||||
body: 'You finished 3rd in the Pro League race',
|
||||
timestamp: feedTimestamp.toISOString(),
|
||||
ctaLabel: 'View Results',
|
||||
ctaHref: '/races/123',
|
||||
},
|
||||
{
|
||||
id: 'feed-2',
|
||||
type: 'league_update',
|
||||
headline: 'League standings updated',
|
||||
body: 'You moved up 2 positions',
|
||||
timestamp: feedTimestamp.toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
friends: [
|
||||
{ id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' },
|
||||
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
|
||||
{ id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = DashboardViewDataBuilder.build(dashboardDTO);
|
||||
|
||||
// Verify all transformations
|
||||
expect(result.currentDriver.name).toBe('John Doe');
|
||||
expect(result.currentDriver.rating).toBe('2,457');
|
||||
expect(result.currentDriver.rank).toBe('15');
|
||||
expect(result.currentDriver.totalRaces).toBe('250');
|
||||
expect(result.currentDriver.wins).toBe('45');
|
||||
expect(result.currentDriver.podiums).toBe('120');
|
||||
expect(result.currentDriver.consistency).toBe('92.5%');
|
||||
|
||||
expect(result.nextRace).not.toBeNull();
|
||||
expect(result.nextRace?.id).toBe('race-1');
|
||||
expect(result.nextRace?.track).toBe('Spa');
|
||||
expect(result.nextRace?.isMyLeague).toBe(true);
|
||||
|
||||
expect(result.upcomingRaces).toHaveLength(2);
|
||||
expect(result.upcomingRaces[0].isMyLeague).toBe(true);
|
||||
expect(result.upcomingRaces[1].isMyLeague).toBe(false);
|
||||
|
||||
expect(result.leagueStandings).toHaveLength(2);
|
||||
expect(result.leagueStandings[0].position).toBe('#3');
|
||||
expect(result.leagueStandings[0].points).toBe('2450');
|
||||
expect(result.leagueStandings[1].position).toBe('#1');
|
||||
expect(result.leagueStandings[1].points).toBe('1800');
|
||||
|
||||
expect(result.feedItems).toHaveLength(2);
|
||||
expect(result.feedItems[0].type).toBe('race_result');
|
||||
expect(result.feedItems[0].ctaLabel).toBe('View Results');
|
||||
expect(result.feedItems[1].type).toBe('league_update');
|
||||
expect(result.feedItems[1].ctaLabel).toBeUndefined();
|
||||
|
||||
expect(result.friends).toHaveLength(3);
|
||||
expect(result.friends[0].avatarUrl).toBe('https://example.com/alice.jpg');
|
||||
expect(result.friends[1].avatarUrl).toBe('');
|
||||
expect(result.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg');
|
||||
|
||||
expect(result.activeLeaguesCount).toBe('2');
|
||||
expect(result.friendCount).toBe('3');
|
||||
expect(result.hasUpcomingRaces).toBe(true);
|
||||
expect(result.hasLeagueStandings).toBe(true);
|
||||
expect(result.hasFeedItems).toBe(true);
|
||||
expect(result.hasFriends).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
38
apps/website/lib/display-objects/RatingDisplay.test.ts
Normal file
38
apps/website/lib/display-objects/RatingDisplay.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RatingDisplay } from './RatingDisplay';
|
||||
|
||||
describe('RatingDisplay', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should format rating correctly', () => {
|
||||
expect(RatingDisplay.format(0)).toBe('0');
|
||||
expect(RatingDisplay.format(1234.56)).toBe('1,235');
|
||||
expect(RatingDisplay.format(9999.99)).toBe('10,000');
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
expect(RatingDisplay.format(null)).toBe('—');
|
||||
});
|
||||
|
||||
it('should handle undefined values', () => {
|
||||
expect(RatingDisplay.format(undefined)).toBe('—');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should round down correctly', () => {
|
||||
expect(RatingDisplay.format(1234.4)).toBe('1,234');
|
||||
});
|
||||
|
||||
it('should round up correctly', () => {
|
||||
expect(RatingDisplay.format(1234.6)).toBe('1,235');
|
||||
});
|
||||
|
||||
it('should handle decimal ratings', () => {
|
||||
expect(RatingDisplay.format(1234.5)).toBe('1,235');
|
||||
});
|
||||
|
||||
it('should handle large ratings', () => {
|
||||
expect(RatingDisplay.format(999999.99)).toBe('1,000,000');
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,472 +0,0 @@
|
||||
/**
|
||||
* View Data Layer Tests - Onboarding Functionality
|
||||
*
|
||||
* This test file covers the view data layer for onboarding functionality.
|
||||
*
|
||||
* The view data layer is responsible for:
|
||||
* - DTO → UI model mapping
|
||||
* - Formatting, sorting, and grouping
|
||||
* - Derived fields and defaults
|
||||
* - UI-specific semantics
|
||||
*
|
||||
* This layer isolates the UI from API churn by providing a stable interface
|
||||
* between the API layer and the presentation layer.
|
||||
*
|
||||
* Test coverage includes:
|
||||
* - Onboarding page data transformation and validation
|
||||
* - Onboarding wizard view models and field formatting
|
||||
* - Authentication and authorization checks for onboarding flow
|
||||
* - Redirect logic based on onboarding status (already onboarded, not authenticated)
|
||||
* - Onboarding-specific formatting and validation
|
||||
* - Derived fields for onboarding UI components (progress, completion status, etc.)
|
||||
* - Default values and fallbacks for onboarding views
|
||||
* - Onboarding step data mapping and state management
|
||||
* - Error handling and fallback UI states for onboarding flow
|
||||
*/
|
||||
|
||||
import { OnboardingViewDataBuilder } from '@/lib/builders/view-data/OnboardingViewDataBuilder';
|
||||
import { OnboardingPageViewDataBuilder } from '@/lib/builders/view-data/OnboardingPageViewDataBuilder';
|
||||
import { CompleteOnboardingViewDataBuilder } from '@/lib/builders/view-data/CompleteOnboardingViewDataBuilder';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { PresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
|
||||
|
||||
describe('OnboardingViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform successful onboarding check to ViewData correctly', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.ok({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle already onboarded user correctly', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.ok({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing isAlreadyOnboarded field with default false', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded?: boolean }, PresentationError> = Result.ok({});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should propagate unauthorized error', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('unauthorized');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('unauthorized');
|
||||
});
|
||||
|
||||
it('should propagate notFound error', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('notFound');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('notFound');
|
||||
});
|
||||
|
||||
it('should propagate serverError', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('serverError');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('serverError');
|
||||
});
|
||||
|
||||
it('should propagate networkError', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('networkError');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('networkError');
|
||||
});
|
||||
|
||||
it('should propagate validationError', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('validationError');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('validationError');
|
||||
});
|
||||
|
||||
it('should propagate unknown error', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.err('unknown');
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.ok({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.unwrap().isAlreadyOnboarded).toBe(false);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError> = Result.ok({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
|
||||
const originalDto = { ...apiDto.unwrap() };
|
||||
OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(apiDto.unwrap()).toEqual(originalDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle null isAlreadyOnboarded as false', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean | null }, PresentationError> = Result.ok({
|
||||
isAlreadyOnboarded: null,
|
||||
});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined isAlreadyOnboarded as false', () => {
|
||||
const apiDto: Result<{ isAlreadyOnboarded: boolean | undefined }, PresentationError> = Result.ok({
|
||||
isAlreadyOnboarded: undefined,
|
||||
});
|
||||
|
||||
const result = OnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OnboardingPageViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform driver data to ViewData correctly when driver exists', () => {
|
||||
const apiDto = { id: 'driver-123', name: 'Test Driver' };
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty object as driver data', () => {
|
||||
const apiDto = {};
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null driver data', () => {
|
||||
const apiDto = null;
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined driver data', () => {
|
||||
const apiDto = undefined;
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all driver data fields in the output', () => {
|
||||
const apiDto = {
|
||||
id: 'driver-123',
|
||||
name: 'Test Driver',
|
||||
email: 'test@example.com',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.isAlreadyOnboarded).toBe(true);
|
||||
});
|
||||
|
||||
it('should not modify the input driver data', () => {
|
||||
const apiDto = { id: 'driver-123', name: 'Test Driver' };
|
||||
const originalDto = { ...apiDto };
|
||||
|
||||
OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(apiDto).toEqual(originalDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty string as driver data', () => {
|
||||
const apiDto = '';
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle zero as driver data', () => {
|
||||
const apiDto = 0;
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle false as driver data', () => {
|
||||
const apiDto = false;
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle array as driver data', () => {
|
||||
const apiDto = ['driver-123'];
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle function as driver data', () => {
|
||||
const apiDto = () => {};
|
||||
|
||||
const result = OnboardingPageViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
isAlreadyOnboarded: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CompleteOnboardingViewDataBuilder', () => {
|
||||
describe('happy paths', () => {
|
||||
it('should transform successful onboarding completion DTO to ViewData correctly', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
errorMessage: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle onboarding completion with error message', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: 'Failed to complete onboarding',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: 'Failed to complete onboarding',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle onboarding completion with only success field', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
driverId: undefined,
|
||||
errorMessage: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data transformation', () => {
|
||||
it('should preserve all DTO fields in the output', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
errorMessage: undefined,
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.success).toBe(apiDto.success);
|
||||
expect(result.driverId).toBe(apiDto.driverId);
|
||||
expect(result.errorMessage).toBe(apiDto.errorMessage);
|
||||
});
|
||||
|
||||
it('should not modify the input DTO', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
errorMessage: undefined,
|
||||
};
|
||||
|
||||
const originalDto = { ...apiDto };
|
||||
CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(apiDto).toEqual(originalDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle false success value', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: 'Error occurred',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.driverId).toBeUndefined();
|
||||
expect(result.errorMessage).toBe('Error occurred');
|
||||
});
|
||||
|
||||
it('should handle empty string error message', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: '',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorMessage).toBe('');
|
||||
});
|
||||
|
||||
it('should handle very long driverId', () => {
|
||||
const longDriverId = 'driver-' + 'a'.repeat(1000);
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: longDriverId,
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.driverId).toBe(longDriverId);
|
||||
});
|
||||
|
||||
it('should handle special characters in error message', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: 'Error: "Failed to create driver" (code: 500)',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.errorMessage).toBe('Error: "Failed to create driver" (code: 500)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('derived fields calculation', () => {
|
||||
it('should calculate isSuccessful derived field correctly', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
// Note: The builder doesn't add derived fields, but we can verify the structure
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.driverId).toBe('driver-123');
|
||||
});
|
||||
|
||||
it('should handle success with no driverId', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: true,
|
||||
driverId: undefined,
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.driverId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle failure with driverId', () => {
|
||||
const apiDto: CompleteOnboardingOutputDTO = {
|
||||
success: false,
|
||||
driverId: 'driver-123',
|
||||
errorMessage: 'Partial failure',
|
||||
};
|
||||
|
||||
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.driverId).toBe('driver-123');
|
||||
expect(result.errorMessage).toBe('Partial failure');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* View Data Layer Tests - Profile Functionality
|
||||
*
|
||||
* This test file will cover the view data layer for profile functionality.
|
||||
*
|
||||
* The view data layer is responsible for:
|
||||
* - DTO → UI model mapping
|
||||
* - Formatting, sorting, and grouping
|
||||
* - Derived fields and defaults
|
||||
* - UI-specific semantics
|
||||
*
|
||||
* This layer isolates the UI from API churn by providing a stable interface
|
||||
* between the API layer and the presentation layer.
|
||||
*
|
||||
* Test coverage will include:
|
||||
* - Driver profile data transformation and formatting
|
||||
* - Profile statistics (rating, rank, race counts, finishes, consistency, etc.)
|
||||
* - Team membership data mapping and role labeling
|
||||
* - Extended profile data (timezone, racing style, favorite track/car, etc.)
|
||||
* - Social handles formatting and URL generation
|
||||
* - Achievement data transformation and icon mapping
|
||||
* - Friends list data mapping and display formatting
|
||||
* - Derived fields (percentile, consistency, looking for team, open to requests)
|
||||
* - Default values and fallbacks for profile views
|
||||
* - Profile-specific formatting (country flags, date labels, etc.)
|
||||
*/
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* View Data Layer Tests - Races Functionality
|
||||
*
|
||||
* This test file will cover the view data layer for races functionality.
|
||||
*
|
||||
* The view data layer is responsible for:
|
||||
* - DTO → UI model mapping
|
||||
* - Formatting, sorting, and grouping
|
||||
* - Derived fields and defaults
|
||||
* - UI-specific semantics
|
||||
*
|
||||
* This layer isolates the UI from API churn by providing a stable interface
|
||||
* between the API layer and the presentation layer.
|
||||
*
|
||||
* Test coverage will include:
|
||||
* - Race list data transformation and sorting
|
||||
* - Individual race page view models (race details, schedule, participants)
|
||||
* - Race results data formatting and ranking calculations
|
||||
* - Stewarding data transformation (protests, penalties, incidents)
|
||||
* - All races page data aggregation and filtering
|
||||
* - Derived race fields (status, eligibility, availability, etc.)
|
||||
* - Default values and fallbacks for race views
|
||||
* - Race-specific formatting (lap times, gaps, points, positions, etc.)
|
||||
* - Data grouping and categorization for race components (by series, date, type)
|
||||
* - Race search and filtering view models
|
||||
* - Real-time race updates and state management
|
||||
* - Historical race data transformation
|
||||
* - Race registration and withdrawal data handling
|
||||
*/
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* View Data Layer Tests - Sponsor Functionality
|
||||
*
|
||||
* This test file will cover the view data layer for sponsor functionality.
|
||||
*
|
||||
* The view data layer is responsible for:
|
||||
* - DTO → UI model mapping
|
||||
* - Formatting, sorting, and grouping
|
||||
* - Derived fields and defaults
|
||||
* - UI-specific semantics
|
||||
*
|
||||
* This layer isolates the UI from API churn by providing a stable interface
|
||||
* between the API layer and the presentation layer.
|
||||
*
|
||||
* Test coverage will include:
|
||||
* - Sponsor dashboard data transformation and metrics
|
||||
* - Sponsor billing and payment view models
|
||||
* - Campaign management data formatting and status tracking
|
||||
* - League sponsorship data aggregation and tier calculations
|
||||
* - Sponsor settings and configuration view models
|
||||
* - Sponsor signup and onboarding data handling
|
||||
* - Derived sponsor fields (engagement metrics, ROI calculations, etc.)
|
||||
* - Default values and fallbacks for sponsor views
|
||||
* - Sponsor-specific formatting (budgets, impressions, clicks, conversions)
|
||||
* - Data grouping and categorization for sponsor components (by campaign, league, status)
|
||||
* - Sponsor search and filtering view models
|
||||
* - Real-time sponsor metrics and state management
|
||||
* - Historical sponsor performance data transformation
|
||||
*/
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* View Data Layer Tests - Teams Functionality
|
||||
*
|
||||
* This test file will cover the view data layer for teams functionality.
|
||||
*
|
||||
* The view data layer is responsible for:
|
||||
* - DTO → UI model mapping
|
||||
* - Formatting, sorting, and grouping
|
||||
* - Derived fields and defaults
|
||||
* - UI-specific semantics
|
||||
*
|
||||
* This layer isolates the UI from API churn by providing a stable interface
|
||||
* between the API layer and the presentation layer.
|
||||
*
|
||||
* Test coverage will include:
|
||||
* - Team list data transformation and sorting
|
||||
* - Individual team profile view models
|
||||
* - Team creation form data handling
|
||||
* - Team leaderboard data transformation
|
||||
* - Team statistics and metrics formatting
|
||||
* - Derived team fields (performance ratings, rankings, etc.)
|
||||
* - Default values and fallbacks for team views
|
||||
* - Team-specific formatting (points, positions, member counts, etc.)
|
||||
* - Data grouping and categorization for team components
|
||||
* - Team search and filtering view models
|
||||
* - Team member data transformation
|
||||
* - Team comparison data transformation
|
||||
*/
|
||||
Reference in New Issue
Block a user