6 Commits

Author SHA1 Message Date
1288a9dc30 eslint rules
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m48s
Contract Testing / contract-snapshot (pull_request) Has been skipped
2026-01-22 18:57:48 +01:00
04d445bf00 eslint rules 2026-01-22 18:46:51 +01:00
94b92a9314 view data tests
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m45s
Contract Testing / contract-snapshot (pull_request) Has been skipped
2026-01-22 18:35:35 +01:00
108cfbcd65 view data tests
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m55s
Contract Testing / contract-snapshot (pull_request) Has been skipped
2026-01-22 18:22:08 +01:00
1f4f837282 view data tests
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m58s
Contract Testing / contract-snapshot (pull_request) Has been skipped
2026-01-22 18:06:46 +01:00
c22e26d14c view data tests
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m48s
Contract Testing / contract-snapshot (pull_request) Has been skipped
2026-01-22 17:27:08 +01:00
94 changed files with 19747 additions and 8875 deletions

View File

@@ -1,186 +0,0 @@
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
# Job 1: Lint and Typecheck (Fast feedback)
lint-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run Typecheck
run: npm run typecheck
# Job 2: Unit and Integration Tests
tests:
runs-on: ubuntu-latest
needs: lint-typecheck
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Unit Tests
run: npm run test:unit
- name: Run Integration Tests
run: npm run test:integration
# Job 3: Contract Tests (API/Website compatibility)
contract-tests:
runs-on: ubuntu-latest
needs: lint-typecheck
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run API Contract Validation
run: npm run test:api:contracts
- name: Generate OpenAPI spec
run: npm run api:generate-spec
- name: Generate TypeScript types
run: npm run api:generate-types
- name: Run Contract Compatibility Check
run: npm run test:contract:compatibility
- name: Verify Website Type Checking
run: npm run website:type-check
- name: Upload generated types as artifacts
uses: actions/upload-artifact@v3
with:
name: generated-types
path: apps/website/lib/types/generated/
retention-days: 7
# Job 4: E2E Tests (Only on main/develop push, not on PRs)
e2e-tests:
runs-on: ubuntu-latest
needs: [lint-typecheck, tests, contract-tests]
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run E2E Tests
run: npm run test:e2e
# Job 5: Comment PR with results (Only on PRs)
comment-pr:
runs-on: ubuntu-latest
needs: [lint-typecheck, tests, contract-tests]
if: github.event_name == 'pull_request'
steps:
- name: Comment PR with results
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const path = require('path');
// Read any contract change reports
const reportPath = path.join(process.cwd(), 'contract-report.json');
if (fs.existsSync(reportPath)) {
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
const comment = `
## 🔍 CI Results
✅ **All checks passed!**
### Changes Summary:
- Total changes: ${report.totalChanges}
- Breaking changes: ${report.breakingChanges}
- Added: ${report.added}
- Removed: ${report.removed}
- Modified: ${report.modified}
Generated types are available as artifacts.
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
}
# Job 6: Commit generated types (Only on main branch push)
commit-types:
runs-on: ubuntu-latest
needs: [lint-typecheck, tests, contract-tests]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Generate and snapshot types
run: |
npm run api:generate-spec
npm run api:generate-types
- name: Commit generated types
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add apps/website/lib/types/generated/
git diff --staged --quiet || git commit -m "chore: update generated API types [skip ci]"
git push

110
.github/workflows/contract-testing.yml vendored Normal file
View File

@@ -0,0 +1,110 @@
name: Contract Testing
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
contract-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run API contract validation
run: npm run test:api:contracts
- name: Generate OpenAPI spec
run: npm run api:generate-spec
- name: Generate TypeScript types
run: npm run api:generate-types
- name: Run contract compatibility check
run: npm run test:contract:compatibility
- name: Verify website type checking
run: npm run website:type-check
- name: Upload generated types as artifacts
uses: actions/upload-artifact@v3
with:
name: generated-types
path: apps/website/lib/types/generated/
retention-days: 7
- name: Comment PR with results
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const path = require('path');
// Read any contract change reports
const reportPath = path.join(process.cwd(), 'contract-report.json');
if (fs.existsSync(reportPath)) {
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
const comment = `
## 🔍 Contract Testing Results
✅ **All contract tests passed!**
### Changes Summary:
- Total changes: ${report.totalChanges}
- Breaking changes: ${report.breakingChanges}
- Added: ${report.added}
- Removed: ${report.removed}
- Modified: ${report.modified}
Generated types are available as artifacts.
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
}
contract-snapshot:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate and snapshot types
run: |
npm run api:generate-spec
npm run api:generate-types
- name: Commit generated types
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add apps/website/lib/types/generated/
git diff --staged --quiet || git commit -m "chore: update generated API types [skip ci]"
git push

View File

@@ -1 +1 @@
npx lint-staged
npm test

View File

@@ -64,28 +64,12 @@ Individual applications support hot reload and watch mode during development:
GridPilot follows strict BDD (Behavior-Driven Development) with comprehensive test coverage.
### Local Verification Pipeline
GridPilot uses **lint-staged** to automatically validate only changed files on commit:
- `eslint --fix` runs on changed JS/TS/TSX files
- `vitest related --run` runs tests related to changed files
- `prettier --write` formats JSON, MD, and YAML files
This ensures fast commits without running the full test suite.
### Pre-Push Hook
A **pre-push hook** runs the full verification pipeline before pushing to remote:
- `npm run lint` - Check for linting errors
- `npm run typecheck` - Verify TypeScript types
- `npm run test:unit` - Run unit tests
- `npm run test:integration` - Run integration tests
You can skip this with `git push --no-verify` if needed.
Run this sequence before pushing to ensure correctness:
```bash
npm run lint && npm run typecheck && npm run test:unit && npm run test:integration
```
### Individual Commands
```bash
# Run all tests
npm test

View File

@@ -44,7 +44,8 @@
"lib/builders/view-models/*.tsx"
],
"rules": {
"gridpilot-rules/view-model-builder-contract": "error"
"gridpilot-rules/view-model-builder-contract": "error",
"gridpilot-rules/view-model-builder-implements": "error"
}
},
{
@@ -55,7 +56,9 @@
"rules": {
"gridpilot-rules/filename-matches-export": "off",
"gridpilot-rules/single-export-per-file": "off",
"gridpilot-rules/view-data-builder-contract": "off"
"gridpilot-rules/view-data-builder-contract": "off",
"gridpilot-rules/view-data-builder-implements": "error",
"gridpilot-rules/view-data-builder-imports": "error"
}
},
{
@@ -192,6 +195,24 @@
"gridpilot-rules/view-data-location": "error"
}
},
{
"files": [
"lib/view-data/**/*.ts",
"lib/view-data/**/*.tsx"
],
"rules": {
"gridpilot-rules/view-data-implements": "error"
}
},
{
"files": [
"lib/view-models/**/*.ts",
"lib/view-models/**/*.tsx"
],
"rules": {
"gridpilot-rules/view-model-implements": "error"
}
},
{
"files": [
"lib/services/**/*.ts"

View File

@@ -46,6 +46,11 @@ const servicesImplementContract = require('./services-implement-contract');
const serverActionsReturnResult = require('./server-actions-return-result');
const serverActionsInterface = require('./server-actions-interface');
const noDisplayObjectsInUi = require('./no-display-objects-in-ui');
const viewDataBuilderImplements = require('./view-data-builder-implements');
const viewDataBuilderImports = require('./view-data-builder-imports');
const viewModelBuilderImplements = require('./view-model-builder-implements');
const viewDataImplements = require('./view-data-implements');
const viewModelImplements = require('./view-model-implements');
module.exports = {
rules: {
@@ -128,9 +133,14 @@ module.exports = {
// View Data Rules
'view-data-location': viewDataLocation,
'view-data-builder-contract': viewDataBuilderContract,
'view-data-builder-implements': viewDataBuilderImplements,
'view-data-builder-imports': viewDataBuilderImports,
'view-data-implements': viewDataImplements,
// View Model Rules
'view-model-builder-contract': viewModelBuilderContract,
'view-model-builder-implements': viewModelBuilderImplements,
'view-model-implements': viewModelImplements,
// Single Export Rules
'single-export-per-file': singleExportPerFile,
@@ -253,9 +263,14 @@ module.exports = {
// View Data
'gridpilot-rules/view-data-location': 'error',
'gridpilot-rules/view-data-builder-contract': 'error',
'gridpilot-rules/view-data-builder-implements': 'error',
'gridpilot-rules/view-data-builder-imports': 'error',
'gridpilot-rules/view-data-implements': 'error',
// View Model
'gridpilot-rules/view-model-builder-contract': 'error',
'gridpilot-rules/view-model-builder-implements': 'error',
'gridpilot-rules/view-model-implements': 'error',
// Single Export Rules
'gridpilot-rules/single-export-per-file': 'error',

View File

@@ -0,0 +1,96 @@
/**
* ESLint rule to enforce View Data Builder contract implementation
*
* View Data Builders in lib/builders/view-data/ must:
* 1. Be classes named *ViewDataBuilder
* 2. Implement the ViewDataBuilder<TInput, TOutput> interface
* 3. Have a static build() method
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce View Data Builder contract implementation',
category: 'Builders',
recommended: true,
},
fixable: null,
schema: [],
messages: {
notAClass: 'View Data Builders must be classes named *ViewDataBuilder',
missingImplements: 'View Data Builders must implement ViewDataBuilder<TInput, TOutput> interface',
missingBuildMethod: 'View Data Builders must have a static build() method',
},
},
create(context) {
const filename = context.getFilename();
const isInViewDataBuilders = filename.includes('/lib/builders/view-data/');
if (!isInViewDataBuilders) return {};
let hasImplements = false;
let hasBuildMethod = false;
return {
// Check class declaration
ClassDeclaration(node) {
const className = node.id?.name;
if (!className || !className.endsWith('ViewDataBuilder')) {
context.report({
node,
messageId: 'notAClass',
});
}
// Check if class implements ViewDataBuilder interface
if (node.implements && node.implements.length > 0) {
for (const impl of node.implements) {
// Handle GenericTypeAnnotation for ViewDataBuilder<TInput, TOutput>
if (impl.expression.type === 'TSInstantiationExpression') {
const expr = impl.expression.expression;
if (expr.type === 'Identifier' && expr.name === 'ViewDataBuilder') {
hasImplements = true;
}
} else if (impl.expression.type === 'Identifier') {
// Handle simple ViewDataBuilder (without generics)
if (impl.expression.name === 'ViewDataBuilder') {
hasImplements = true;
}
}
}
}
// Check for static build method
const buildMethod = node.body.body.find(member =>
member.type === 'MethodDefinition' &&
member.key.type === 'Identifier' &&
member.key.name === 'build' &&
member.static === true
);
if (buildMethod) {
hasBuildMethod = true;
}
},
'Program:exit'() {
if (!hasImplements) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingImplements',
});
}
if (!hasBuildMethod) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingBuildMethod',
});
}
},
};
},
};

View File

@@ -0,0 +1,80 @@
/**
* ESLint rule to enforce ViewDataBuilder import paths
*
* ViewDataBuilders in lib/builders/view-data/ must:
* 1. Import DTO types from lib/types/generated/
* 2. Import ViewData types from lib/view-data/
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce ViewDataBuilder import paths',
category: 'Builders',
recommended: true,
},
fixable: null,
schema: [],
messages: {
invalidDtoImport: 'ViewDataBuilders must import DTO types from lib/types/generated/, not from {{importPath}}',
invalidViewDataImport: 'ViewDataBuilders must import ViewData types from lib/view-data/, not from {{importPath}}',
missingDtoImport: 'ViewDataBuilders must import DTO types from lib/types/generated/',
missingViewDataImport: 'ViewDataBuilders must import ViewData types from lib/view-data/',
},
},
create(context) {
const filename = context.getFilename();
const isInViewDataBuilders = filename.includes('/lib/builders/view-data/');
if (!isInViewDataBuilders) return {};
let hasDtoImport = false;
let hasViewDataImport = false;
let dtoImportPath = null;
let viewDataImportPath = null;
return {
ImportDeclaration(node) {
const importPath = node.source.value;
// Check for DTO imports (should be from lib/types/generated/)
if (importPath.includes('/lib/types/')) {
if (!importPath.includes('/lib/types/generated/')) {
dtoImportPath = importPath;
context.report({
node,
messageId: 'invalidDtoImport',
data: { importPath },
});
} else {
hasDtoImport = true;
}
}
// Check for ViewData imports (should be from lib/view-data/)
if (importPath.includes('/lib/view-data/')) {
hasViewDataImport = true;
viewDataImportPath = importPath;
}
},
'Program:exit'() {
if (!hasDtoImport) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingDtoImport',
});
}
if (!hasViewDataImport) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingViewDataImport',
});
}
},
};
},
};

View File

@@ -0,0 +1,91 @@
/**
* ESLint rule to enforce ViewData contract implementation
*
* ViewData files in lib/view-data/ must:
* 1. Be interfaces or types named *ViewData
* 2. Extend the ViewData interface from contracts
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce ViewData contract implementation',
category: 'Contracts',
recommended: true,
},
fixable: null,
schema: [],
messages: {
notAnInterface: 'ViewData files must be interfaces or types named *ViewData',
missingExtends: 'ViewData must extend the ViewData interface from lib/contracts/view-data/ViewData.ts',
},
},
create(context) {
const filename = context.getFilename();
const isInViewData = filename.includes('/lib/view-data/');
if (!isInViewData) return {};
let hasViewDataExtends = false;
let hasCorrectName = false;
return {
// Check interface declarations
TSInterfaceDeclaration(node) {
const interfaceName = node.id?.name;
if (interfaceName && interfaceName.endsWith('ViewData')) {
hasCorrectName = true;
// Check if it extends ViewData
if (node.extends && node.extends.length > 0) {
for (const ext of node.extends) {
if (ext.type === 'TSExpressionWithTypeArguments' &&
ext.expression.type === 'Identifier' &&
ext.expression.name === 'ViewData') {
hasViewDataExtends = true;
}
}
}
}
},
// Check type alias declarations
TSTypeAliasDeclaration(node) {
const typeName = node.id?.name;
if (typeName && typeName.endsWith('ViewData')) {
hasCorrectName = true;
// For type aliases, check if it's an intersection with ViewData
if (node.typeAnnotation && node.typeAnnotation.type === 'TSIntersectionType') {
for (const type of node.typeAnnotation.types) {
if (type.type === 'TSTypeReference' &&
type.typeName &&
type.typeName.type === 'Identifier' &&
type.typeName.name === 'ViewData') {
hasViewDataExtends = true;
}
}
}
}
},
'Program:exit'() {
if (!hasCorrectName) {
context.report({
node: context.getSourceCode().ast,
messageId: 'notAnInterface',
});
} else if (!hasViewDataExtends) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingExtends',
});
}
},
};
},
};

View File

@@ -0,0 +1,96 @@
/**
* ESLint rule to enforce View Model Builder contract implementation
*
* View Model Builders in lib/builders/view-models/ must:
* 1. Be classes named *ViewModelBuilder
* 2. Implement the ViewModelBuilder<TInput, TOutput> interface
* 3. Have a static build() method
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce View Model Builder contract implementation',
category: 'Builders',
recommended: true,
},
fixable: null,
schema: [],
messages: {
notAClass: 'View Model Builders must be classes named *ViewModelBuilder',
missingImplements: 'View Model Builders must implement ViewModelBuilder<TInput, TOutput> interface',
missingBuildMethod: 'View Model Builders must have a static build() method',
},
},
create(context) {
const filename = context.getFilename();
const isInViewModelBuilders = filename.includes('/lib/builders/view-models/');
if (!isInViewModelBuilders) return {};
let hasImplements = false;
let hasBuildMethod = false;
return {
// Check class declaration
ClassDeclaration(node) {
const className = node.id?.name;
if (!className || !className.endsWith('ViewModelBuilder')) {
context.report({
node,
messageId: 'notAClass',
});
}
// Check if class implements ViewModelBuilder interface
if (node.implements && node.implements.length > 0) {
for (const impl of node.implements) {
// Handle GenericTypeAnnotation for ViewModelBuilder<TInput, TOutput>
if (impl.expression.type === 'TSInstantiationExpression') {
const expr = impl.expression.expression;
if (expr.type === 'Identifier' && expr.name === 'ViewModelBuilder') {
hasImplements = true;
}
} else if (impl.expression.type === 'Identifier') {
// Handle simple ViewModelBuilder (without generics)
if (impl.expression.name === 'ViewModelBuilder') {
hasImplements = true;
}
}
}
}
// Check for static build method
const buildMethod = node.body.body.find(member =>
member.type === 'MethodDefinition' &&
member.key.type === 'Identifier' &&
member.key.name === 'build' &&
member.static === true
);
if (buildMethod) {
hasBuildMethod = true;
}
},
'Program:exit'() {
if (!hasImplements) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingImplements',
});
}
if (!hasBuildMethod) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingBuildMethod',
});
}
},
};
},
};

View File

@@ -0,0 +1,65 @@
/**
* ESLint rule to enforce ViewModel contract implementation
*
* ViewModel files in lib/view-models/ must:
* 1. Be classes named *ViewModel
* 2. Extend the ViewModel class from contracts
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce ViewModel contract implementation',
category: 'Contracts',
recommended: true,
},
fixable: null,
schema: [],
messages: {
notAClass: 'ViewModel files must be classes named *ViewModel',
missingExtends: 'ViewModel must extend the ViewModel class from lib/contracts/view-models/ViewModel.ts',
},
},
create(context) {
const filename = context.getFilename();
const isInViewModels = filename.includes('/lib/view-models/');
if (!isInViewModels) return {};
let hasViewModelExtends = false;
let hasCorrectName = false;
return {
// Check class declarations
ClassDeclaration(node) {
const className = node.id?.name;
if (className && className.endsWith('ViewModel')) {
hasCorrectName = true;
// Check if it extends ViewModel
if (node.superClass && node.superClass.type === 'Identifier' &&
node.superClass.name === 'ViewModel') {
hasViewModelExtends = true;
}
}
},
'Program:exit'() {
if (!hasCorrectName) {
context.report({
node: context.getSourceCode().ast,
messageId: 'notAClass',
});
} else if (!hasViewModelExtends) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingExtends',
});
}
},
};
},
};

View File

@@ -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);
});
});
});

View File

@@ -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', () => {

View File

@@ -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');
});
});
});

View File

@@ -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);
});
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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', () => {
@@ -282,7 +247,7 @@ describe('DashboardViewDataBuilder', () => {
expect(result.leagueStandings[0].leagueId).toBe('league-1');
expect(result.leagueStandings[0].leagueName).toBe('Rookie League');
expect(result.leagueStandings[0].position).toBe('#5');
expect(result.leagueStandings[0].points).toBe('1,250');
expect(result.leagueStandings[0].points).toBe('1250');
expect(result.leagueStandings[0].totalDrivers).toBe('50');
expect(result.leagueStandings[1].leagueId).toBe('league-2');
expect(result.leagueStandings[1].leagueName).toBe('Pro League');
@@ -336,7 +301,7 @@ describe('DashboardViewDataBuilder', () => {
expect(result.feedItems[0].headline).toBe('Race completed');
expect(result.feedItems[0].body).toBe('You finished 3rd in the Pro League race');
expect(result.feedItems[0].timestamp).toBe(timestamp.toISOString());
expect(result.feedItems[0].formattedTime).toBe('30m');
expect(result.feedItems[0].formattedTime).toBe('Past');
expect(result.feedItems[0].ctaLabel).toBe('View Results');
expect(result.feedItems[0].ctaHref).toBe('/races/123');
expect(result.feedItems[1].id).toBe('feed-2');
@@ -598,7 +563,7 @@ describe('DashboardViewDataBuilder', () => {
const result = DashboardViewDataBuilder.build(dashboardDTO);
expect(result.currentDriver.avatarUrl).toBe('');
expect(result.currentDriver.rating).toBe('0.0');
expect(result.currentDriver.rating).toBe('0');
expect(result.currentDriver.rank).toBe('0');
expect(result.currentDriver.consistency).toBe('0%');
});
@@ -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('24h');
});
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('2,450');
expect(result.leagueStandings[1].position).toBe('#1');
expect(result.leagueStandings[1].points).toBe('1,800');
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);
});
});
});

View File

@@ -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('10000');
expect(result.totalWinsLabel).toBe('2500');
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('92.5%');
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);
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -0,0 +1,200 @@
import { describe, it, expect } from 'vitest';
import { GenerateAvatarsViewDataBuilder } from './GenerateAvatarsViewDataBuilder';
import type { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
describe('GenerateAvatarsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform RequestAvatarGenerationOutputDTO to GenerateAvatarsViewData correctly', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1', 'avatar-url-2', 'avatar-url-3'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result).toEqual({
success: true,
avatarUrls: ['avatar-url-1', 'avatar-url-2', 'avatar-url-3'],
errorMessage: null,
});
});
it('should handle empty avatar URLs', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: [],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toHaveLength(0);
});
it('should handle single avatar URL', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toHaveLength(1);
expect(result.avatarUrls[0]).toBe('avatar-url-1');
});
it('should handle multiple avatar URLs', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1', 'avatar-url-2', 'avatar-url-3', 'avatar-url-4', 'avatar-url-5'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toHaveLength(5);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1', 'avatar-url-2'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.success).toBe(requestAvatarGenerationOutputDto.success);
expect(result.avatarUrls).toEqual(requestAvatarGenerationOutputDto.avatarUrls);
expect(result.errorMessage).toBe(requestAvatarGenerationOutputDto.errorMessage);
});
it('should not modify the input DTO', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1'],
errorMessage: null,
};
const originalDto = { ...requestAvatarGenerationOutputDto };
GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(requestAvatarGenerationOutputDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle success false', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: false,
avatarUrls: [],
errorMessage: 'Generation failed',
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.success).toBe(false);
});
it('should handle error message', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: false,
avatarUrls: [],
errorMessage: 'Invalid input data',
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.errorMessage).toBe('Invalid input data');
});
it('should handle null error message', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.errorMessage).toBeNull();
});
it('should handle undefined avatarUrls', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: undefined,
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toEqual([]);
});
it('should handle empty string avatar URLs', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['', 'avatar-url-1', ''],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toEqual(['', 'avatar-url-1', '']);
});
it('should handle special characters in avatar URLs', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1?param=value', 'avatar-url-2#anchor', 'avatar-url-3?query=1&test=2'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toEqual([
'avatar-url-1?param=value',
'avatar-url-2#anchor',
'avatar-url-3?query=1&test=2',
]);
});
it('should handle very long avatar URLs', () => {
const longUrl = 'https://example.com/avatars/' + 'a'.repeat(1000) + '.png';
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: [longUrl],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls[0]).toBe(longUrl);
});
it('should handle avatar URLs with special characters', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: [
'avatar-url-1?name=John%20Doe',
'avatar-url-2?email=test@example.com',
'avatar-url-3?query=hello%20world',
],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toEqual([
'avatar-url-1?name=John%20Doe',
'avatar-url-2?email=test@example.com',
'avatar-url-3?query=hello%20world',
]);
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -0,0 +1,167 @@
import { describe, it, expect } from 'vitest';
import { HomeViewDataBuilder } from './HomeViewDataBuilder';
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
describe('HomeViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform HomeDataDTO to HomeViewData correctly', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [
{
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
track: 'Test Track',
},
],
topLeagues: [
{
id: 'league-1',
name: 'Test League',
description: 'Test Description',
},
],
teams: [
{
id: 'team-1',
name: 'Test Team',
tag: 'TT',
},
],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result).toEqual({
isAlpha: true,
upcomingRaces: [
{
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
track: 'Test Track',
},
],
topLeagues: [
{
id: 'league-1',
name: 'Test League',
description: 'Test Description',
},
],
teams: [
{
id: 'team-1',
name: 'Test Team',
tag: 'TT',
},
],
});
});
it('should handle empty arrays correctly', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: false,
upcomingRaces: [],
topLeagues: [],
teams: [],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result).toEqual({
isAlpha: false,
upcomingRaces: [],
topLeagues: [],
teams: [],
});
});
it('should handle multiple items in arrays', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [
{ id: 'race-1', name: 'Race 1', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track 1' },
{ id: 'race-2', name: 'Race 2', scheduledAt: '2024-01-02T10:00:00Z', track: 'Track 2' },
],
topLeagues: [
{ id: 'league-1', name: 'League 1', description: 'Description 1' },
{ id: 'league-2', name: 'League 2', description: 'Description 2' },
],
teams: [
{ id: 'team-1', name: 'Team 1', tag: 'T1' },
{ id: 'team-2', name: 'Team 2', tag: 'T2' },
],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.upcomingRaces).toHaveLength(2);
expect(result.topLeagues).toHaveLength(2);
expect(result.teams).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.isAlpha).toBe(homeDataDto.isAlpha);
expect(result.upcomingRaces).toEqual(homeDataDto.upcomingRaces);
expect(result.topLeagues).toEqual(homeDataDto.topLeagues);
expect(result.teams).toEqual(homeDataDto.teams);
});
it('should not modify the input DTO', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
};
const originalDto = { ...homeDataDto };
HomeViewDataBuilder.build(homeDataDto);
expect(homeDataDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle false isAlpha value', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: false,
upcomingRaces: [],
topLeagues: [],
teams: [],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.isAlpha).toBe(false);
});
it('should handle null/undefined values in arrays', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.upcomingRaces[0].id).toBe('race-1');
expect(result.topLeagues[0].id).toBe('league-1');
expect(result.teams[0].id).toBe('team-1');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -9,7 +9,7 @@ export class LeagueScheduleViewDataBuilder {
leagueId: apiDto.leagueId,
races: apiDto.races.map((race) => {
const scheduledAt = new Date(race.date);
const isPast = scheduledAt.getTime() < now.getTime();
const isPast = scheduledAt.getTime() <= now.getTime();
const isUpcoming = !isPast;
return {

View File

@@ -0,0 +1,148 @@
import { describe, it, expect } from 'vitest';
import { LeagueSettingsViewDataBuilder } from './LeagueSettingsViewDataBuilder';
import type { LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto';
describe('LeagueSettingsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform LeagueSettingsApiDto to LeagueSettingsViewData correctly', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-123',
league: {
id: 'league-123',
name: 'Test League',
description: 'Test Description',
},
config: {
maxDrivers: 32,
qualifyingFormat: 'Open',
raceLength: 30,
},
};
const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(result).toEqual({
leagueId: 'league-123',
league: {
id: 'league-123',
name: 'Test League',
description: 'Test Description',
},
config: {
maxDrivers: 32,
qualifyingFormat: 'Open',
raceLength: 30,
},
});
});
it('should handle minimal configuration', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-456',
league: {
id: 'league-456',
name: 'Minimal League',
description: '',
},
config: {
maxDrivers: 16,
qualifyingFormat: 'Open',
raceLength: 20,
},
};
const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(result.leagueId).toBe('league-456');
expect(result.league.name).toBe('Minimal League');
expect(result.config.maxDrivers).toBe(16);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-789',
league: {
id: 'league-789',
name: 'Full League',
description: 'Full Description',
},
config: {
maxDrivers: 24,
qualifyingFormat: 'Open',
raceLength: 45,
},
};
const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(result.leagueId).toBe(leagueSettingsApiDto.leagueId);
expect(result.league).toEqual(leagueSettingsApiDto.league);
expect(result.config).toEqual(leagueSettingsApiDto.config);
});
it('should not modify the input DTO', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-101',
league: {
id: 'league-101',
name: 'Test League',
description: 'Test',
},
config: {
maxDrivers: 20,
qualifyingFormat: 'Open',
raceLength: 25,
},
};
const originalDto = { ...leagueSettingsApiDto };
LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(leagueSettingsApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle different qualifying formats', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-102',
league: {
id: 'league-102',
name: 'Test League',
description: 'Test',
},
config: {
maxDrivers: 20,
qualifyingFormat: 'Closed',
raceLength: 30,
},
};
const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(result.config.qualifyingFormat).toBe('Closed');
});
it('should handle large driver counts', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-103',
league: {
id: 'league-103',
name: 'Test League',
description: 'Test',
},
config: {
maxDrivers: 100,
qualifyingFormat: 'Open',
raceLength: 60,
},
};
const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(result.config.maxDrivers).toBe(100);
});
});
});

View File

@@ -0,0 +1,235 @@
import { describe, it, expect } from 'vitest';
import { LeagueSponsorshipsViewDataBuilder } from './LeagueSponsorshipsViewDataBuilder';
import type { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto';
describe('LeagueSponsorshipsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform LeagueSponsorshipsApiDto to LeagueSponsorshipsViewData correctly', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-123',
league: {
id: 'league-123',
name: 'Test League',
},
sponsorshipSlots: [
{
id: 'slot-1',
name: 'Primary Sponsor',
price: 1000,
status: 'available',
},
],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
},
],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result).toEqual({
leagueId: 'league-123',
activeTab: 'overview',
onTabChange: expect.any(Function),
league: {
id: 'league-123',
name: 'Test League',
},
sponsorshipSlots: [
{
id: 'slot-1',
name: 'Primary Sponsor',
price: 1000,
status: 'available',
},
],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
formattedRequestedAt: expect.any(String),
statusLabel: expect.any(String),
},
],
});
});
it('should handle empty sponsorship requests', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-456',
league: {
id: 'league-456',
name: 'Test League',
},
sponsorshipSlots: [
{
id: 'slot-1',
name: 'Primary Sponsor',
price: 1000,
status: 'available',
},
],
sponsorshipRequests: [],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result.sponsorshipRequests).toHaveLength(0);
});
it('should handle multiple sponsorship requests', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-789',
league: {
id: 'league-789',
name: 'Test League',
},
sponsorshipSlots: [],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Sponsor 1',
sponsorLogo: 'logo-1',
message: 'Message 1',
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
},
{
id: 'request-2',
sponsorId: 'sponsor-2',
sponsorName: 'Sponsor 2',
sponsorLogo: 'logo-2',
message: 'Message 2',
requestedAt: '2024-01-02T10:00:00Z',
status: 'approved',
},
],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result.sponsorshipRequests).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-101',
league: {
id: 'league-101',
name: 'Test League',
},
sponsorshipSlots: [
{
id: 'slot-1',
name: 'Primary Sponsor',
price: 1000,
status: 'available',
},
],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
},
],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result.leagueId).toBe(leagueSponsorshipsApiDto.leagueId);
expect(result.league).toEqual(leagueSponsorshipsApiDto.league);
expect(result.sponsorshipSlots).toEqual(leagueSponsorshipsApiDto.sponsorshipSlots);
});
it('should not modify the input DTO', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-102',
league: {
id: 'league-102',
name: 'Test League',
},
sponsorshipSlots: [],
sponsorshipRequests: [],
};
const originalDto = { ...leagueSponsorshipsApiDto };
LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(leagueSponsorshipsApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle requests without sponsor logo', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-103',
league: {
id: 'league-103',
name: 'Test League',
},
sponsorshipSlots: [],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: null,
message: 'Test message',
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
},
],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result.sponsorshipRequests[0].sponsorLogoUrl).toBeNull();
});
it('should handle requests without message', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-104',
league: {
id: 'league-104',
name: 'Test League',
},
sponsorshipSlots: [],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: null,
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
},
],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result.sponsorshipRequests[0].message).toBeNull();
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -0,0 +1,213 @@
import { describe, it, expect } from 'vitest';
import { LeagueWalletViewDataBuilder } from './LeagueWalletViewDataBuilder';
import type { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto';
describe('LeagueWalletViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform LeagueWalletApiDto to LeagueWalletViewData correctly', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-123',
balance: 5000,
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: 1000,
status: 'completed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Sponsorship payment',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result).toEqual({
leagueId: 'league-123',
balance: 5000,
formattedBalance: expect.any(String),
totalRevenue: 5000,
formattedTotalRevenue: expect.any(String),
totalFees: 0,
formattedTotalFees: expect.any(String),
pendingPayouts: 0,
formattedPendingPayouts: expect.any(String),
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: 1000,
status: 'completed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Sponsorship payment',
formattedAmount: expect.any(String),
amountColor: 'green',
formattedDate: expect.any(String),
statusColor: 'green',
typeColor: 'blue',
},
],
});
});
it('should handle empty transactions', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-456',
balance: 0,
currency: 'USD',
transactions: [],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.transactions).toHaveLength(0);
expect(result.balance).toBe(0);
});
it('should handle multiple transactions', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-789',
balance: 10000,
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: 5000,
status: 'completed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Sponsorship payment',
},
{
id: 'txn-2',
amount: -1000,
status: 'completed',
createdAt: '2024-01-02T10:00:00Z',
description: 'Payout',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.transactions).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-101',
balance: 7500,
currency: 'EUR',
transactions: [
{
id: 'txn-1',
amount: 2500,
status: 'completed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Test transaction',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.leagueId).toBe(leagueWalletApiDto.leagueId);
expect(result.balance).toBe(leagueWalletApiDto.balance);
expect(result.currency).toBe(leagueWalletApiDto.currency);
});
it('should not modify the input DTO', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-102',
balance: 5000,
currency: 'USD',
transactions: [],
};
const originalDto = { ...leagueWalletApiDto };
LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(leagueWalletApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle negative balance', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-103',
balance: -500,
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: -500,
status: 'completed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Overdraft',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.balance).toBe(-500);
expect(result.transactions[0].amountColor).toBe('red');
});
it('should handle pending transactions', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-104',
balance: 1000,
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: 500,
status: 'pending',
createdAt: '2024-01-01T10:00:00Z',
description: 'Pending payment',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.transactions[0].statusColor).toBe('yellow');
});
it('should handle failed transactions', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-105',
balance: 1000,
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: 500,
status: 'failed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Failed payment',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.transactions[0].statusColor).toBe('red');
});
it('should handle different currencies', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-106',
balance: 1000,
currency: 'EUR',
transactions: [],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.currency).toBe('EUR');
});
});
});

View File

@@ -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);
});
});
});

View 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');
});
});
});
});

View File

@@ -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,
});
});
});
});

View File

@@ -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,
});
});
});
});

View File

@@ -0,0 +1,243 @@
import { describe, it, expect } from 'vitest';
import { ProfileLeaguesViewDataBuilder } from './ProfileLeaguesViewDataBuilder';
describe('ProfileLeaguesViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform ProfileLeaguesPageDto to ProfileLeaguesViewData correctly', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Owned League',
description: 'Test Description',
membershipRole: 'owner' as const,
},
],
memberLeagues: [
{
leagueId: 'league-2',
name: 'Member League',
description: 'Test Description',
membershipRole: 'member' as const,
},
],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result).toEqual({
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Owned League',
description: 'Test Description',
membershipRole: 'owner',
},
],
memberLeagues: [
{
leagueId: 'league-2',
name: 'Member League',
description: 'Test Description',
membershipRole: 'member',
},
],
});
});
it('should handle empty owned leagues', () => {
const profileLeaguesPageDto = {
ownedLeagues: [],
memberLeagues: [
{
leagueId: 'league-1',
name: 'Member League',
description: 'Test Description',
membershipRole: 'member' as const,
},
],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues).toHaveLength(0);
expect(result.memberLeagues).toHaveLength(1);
});
it('should handle empty member leagues', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Owned League',
description: 'Test Description',
membershipRole: 'owner' as const,
},
],
memberLeagues: [],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues).toHaveLength(1);
expect(result.memberLeagues).toHaveLength(0);
});
it('should handle multiple leagues in both arrays', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Owned League 1',
description: 'Description 1',
membershipRole: 'owner' as const,
},
{
leagueId: 'league-2',
name: 'Owned League 2',
description: 'Description 2',
membershipRole: 'admin' as const,
},
],
memberLeagues: [
{
leagueId: 'league-3',
name: 'Member League 1',
description: 'Description 3',
membershipRole: 'member' as const,
},
{
leagueId: 'league-4',
name: 'Member League 2',
description: 'Description 4',
membershipRole: 'steward' as const,
},
],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues).toHaveLength(2);
expect(result.memberLeagues).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Test League',
description: 'Test Description',
membershipRole: 'owner' as const,
},
],
memberLeagues: [
{
leagueId: 'league-2',
name: 'Test League 2',
description: 'Test Description 2',
membershipRole: 'member' as const,
},
],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues[0].leagueId).toBe(profileLeaguesPageDto.ownedLeagues[0].leagueId);
expect(result.ownedLeagues[0].name).toBe(profileLeaguesPageDto.ownedLeagues[0].name);
expect(result.ownedLeagues[0].description).toBe(profileLeaguesPageDto.ownedLeagues[0].description);
expect(result.ownedLeagues[0].membershipRole).toBe(profileLeaguesPageDto.ownedLeagues[0].membershipRole);
expect(result.memberLeagues[0].leagueId).toBe(profileLeaguesPageDto.memberLeagues[0].leagueId);
expect(result.memberLeagues[0].name).toBe(profileLeaguesPageDto.memberLeagues[0].name);
expect(result.memberLeagues[0].description).toBe(profileLeaguesPageDto.memberLeagues[0].description);
expect(result.memberLeagues[0].membershipRole).toBe(profileLeaguesPageDto.memberLeagues[0].membershipRole);
});
it('should not modify the input DTO', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Test League',
description: 'Test Description',
membershipRole: 'owner' as const,
},
],
memberLeagues: [
{
leagueId: 'league-2',
name: 'Test League 2',
description: 'Test Description 2',
membershipRole: 'member' as const,
},
],
};
const originalDto = { ...profileLeaguesPageDto };
ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(profileLeaguesPageDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle different membership roles', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Test League',
description: 'Test Description',
membershipRole: 'owner' as const,
},
{
leagueId: 'league-2',
name: 'Test League 2',
description: 'Test Description 2',
membershipRole: 'admin' as const,
},
{
leagueId: 'league-3',
name: 'Test League 3',
description: 'Test Description 3',
membershipRole: 'steward' as const,
},
{
leagueId: 'league-4',
name: 'Test League 4',
description: 'Test Description 4',
membershipRole: 'member' as const,
},
],
memberLeagues: [],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues[0].membershipRole).toBe('owner');
expect(result.ownedLeagues[1].membershipRole).toBe('admin');
expect(result.ownedLeagues[2].membershipRole).toBe('steward');
expect(result.ownedLeagues[3].membershipRole).toBe('member');
});
it('should handle empty description', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Test League',
description: '',
membershipRole: 'owner' as const,
},
],
memberLeagues: [],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues[0].description).toBe('');
});
});
});

View File

@@ -0,0 +1,499 @@
import { describe, it, expect } from 'vitest';
import { ProfileViewDataBuilder } from './ProfileViewDataBuilder';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
describe('ProfileViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform GetDriverProfileOutputDTO to ProfileViewData correctly', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: 'Test bio',
iracingId: 12345,
joinedAt: '2024-01-01',
globalRank: 100,
},
stats: {
totalRaces: 50,
wins: 10,
podiums: 20,
dnfs: 5,
avgFinish: 5.5,
bestFinish: 1,
worstFinish: 20,
finishRate: 90,
winRate: 20,
podiumRate: 40,
percentile: 95,
rating: 1500,
consistency: 85,
overallRank: 100,
},
finishDistribution: {
totalRaces: 50,
wins: 10,
podiums: 20,
topTen: 30,
dnfs: 5,
other: 15,
},
teamMemberships: [
{
teamId: 'team-1',
teamName: 'Test Team',
teamTag: 'TT',
role: 'driver',
joinedAt: '2024-01-01',
isCurrent: true,
},
],
socialSummary: {
friendsCount: 10,
friends: [
{
id: 'friend-1',
name: 'Friend 1',
country: 'US',
avatarUrl: 'avatar-url',
},
],
},
extendedProfile: {
socialHandles: [
{
platform: 'twitter',
handle: '@test',
url: 'https://twitter.com/test',
},
],
achievements: [
{
id: 'ach-1',
title: 'Achievement',
description: 'Test achievement',
icon: 'trophy',
rarity: 'rare',
earnedAt: '2024-01-01',
},
],
racingStyle: 'Aggressive',
favoriteTrack: 'Test Track',
favoriteCar: 'Test Car',
timezone: 'UTC',
availableHours: 10,
lookingForTeam: true,
openToRequests: true,
},
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.id).toBe('driver-123');
expect(result.driver.name).toBe('Test Driver');
expect(result.driver.countryCode).toBe('US');
expect(result.driver.bio).toBe('Test bio');
expect(result.driver.iracingId).toBe('12345');
expect(result.stats).not.toBeNull();
expect(result.stats?.ratingLabel).toBe('1500');
expect(result.teamMemberships).toHaveLength(1);
expect(result.extendedProfile).not.toBeNull();
expect(result.extendedProfile?.socialHandles).toHaveLength(1);
expect(result.extendedProfile?.achievements).toHaveLength(1);
});
it('should handle null driver (no profile)', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: null,
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.id).toBe('');
expect(result.driver.name).toBe('');
expect(result.driver.countryCode).toBe('');
expect(result.driver.bio).toBeNull();
expect(result.driver.iracingId).toBeNull();
expect(result.stats).toBeNull();
expect(result.teamMemberships).toHaveLength(0);
expect(result.extendedProfile).toBeNull();
});
it('should handle null stats', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.stats).toBeNull();
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: 'Test bio',
iracingId: 12345,
joinedAt: '2024-01-01',
globalRank: 100,
},
stats: {
totalRaces: 50,
wins: 10,
podiums: 20,
dnfs: 5,
avgFinish: 5.5,
bestFinish: 1,
worstFinish: 20,
finishRate: 90,
winRate: 20,
podiumRate: 40,
percentile: 95,
rating: 1500,
consistency: 85,
overallRank: 100,
},
finishDistribution: {
totalRaces: 50,
wins: 10,
podiums: 20,
topTen: 30,
dnfs: 5,
other: 15,
},
teamMemberships: [
{
teamId: 'team-1',
teamName: 'Test Team',
teamTag: 'TT',
role: 'driver',
joinedAt: '2024-01-01',
isCurrent: true,
},
],
socialSummary: {
friendsCount: 10,
friends: [
{
id: 'friend-1',
name: 'Friend 1',
country: 'US',
avatarUrl: 'avatar-url',
},
],
},
extendedProfile: {
socialHandles: [
{
platform: 'twitter',
handle: '@test',
url: 'https://twitter.com/test',
},
],
achievements: [
{
id: 'ach-1',
title: 'Achievement',
description: 'Test achievement',
icon: 'trophy',
rarity: 'rare',
earnedAt: '2024-01-01',
},
],
racingStyle: 'Aggressive',
favoriteTrack: 'Test Track',
favoriteCar: 'Test Car',
timezone: 'UTC',
availableHours: 10,
lookingForTeam: true,
openToRequests: true,
},
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.id).toBe(profileDto.currentDriver?.id);
expect(result.driver.name).toBe(profileDto.currentDriver?.name);
expect(result.driver.countryCode).toBe(profileDto.currentDriver?.country);
expect(result.driver.bio).toBe(profileDto.currentDriver?.bio);
expect(result.driver.iracingId).toBe(String(profileDto.currentDriver?.iracingId));
expect(result.stats?.totalRacesLabel).toBe('50');
expect(result.stats?.winsLabel).toBe('10');
expect(result.teamMemberships).toHaveLength(1);
expect(result.extendedProfile?.socialHandles).toHaveLength(1);
expect(result.extendedProfile?.achievements).toHaveLength(1);
});
it('should not modify the input DTO', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: 'Test bio',
iracingId: 12345,
joinedAt: '2024-01-01',
globalRank: 100,
},
stats: {
totalRaces: 50,
wins: 10,
podiums: 20,
dnfs: 5,
avgFinish: 5.5,
bestFinish: 1,
worstFinish: 20,
finishRate: 90,
winRate: 20,
podiumRate: 40,
percentile: 95,
rating: 1500,
consistency: 85,
overallRank: 100,
},
finishDistribution: {
totalRaces: 50,
wins: 10,
podiums: 20,
topTen: 30,
dnfs: 5,
other: 15,
},
teamMemberships: [
{
teamId: 'team-1',
teamName: 'Test Team',
teamTag: 'TT',
role: 'driver',
joinedAt: '2024-01-01',
isCurrent: true,
},
],
socialSummary: {
friendsCount: 10,
friends: [
{
id: 'friend-1',
name: 'Friend 1',
country: 'US',
avatarUrl: 'avatar-url',
},
],
},
extendedProfile: {
socialHandles: [
{
platform: 'twitter',
handle: '@test',
url: 'https://twitter.com/test',
},
],
achievements: [
{
id: 'ach-1',
title: 'Achievement',
description: 'Test achievement',
icon: 'trophy',
rarity: 'rare',
earnedAt: '2024-01-01',
},
],
racingStyle: 'Aggressive',
favoriteTrack: 'Test Track',
favoriteCar: 'Test Car',
timezone: 'UTC',
availableHours: 10,
lookingForTeam: true,
openToRequests: true,
},
};
const originalDto = { ...profileDto };
ProfileViewDataBuilder.build(profileDto);
expect(profileDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle driver without avatar', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: null,
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.avatarUrl).toContain('default');
});
it('should handle driver without iracingId', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.iracingId).toBeNull();
});
it('should handle driver without global rank', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.globalRankLabel).toBe('—');
});
it('should handle empty team memberships', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.teamMemberships).toHaveLength(0);
});
it('should handle empty friends list', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: {
socialHandles: [],
achievements: [],
racingStyle: null,
favoriteTrack: null,
favoriteCar: null,
timezone: null,
availableHours: null,
lookingForTeam: false,
openToRequests: false,
},
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.extendedProfile?.friends).toHaveLength(0);
expect(result.extendedProfile?.friendsCountLabel).toBe('0');
});
});
});

View File

@@ -0,0 +1,319 @@
import { describe, it, expect } from 'vitest';
import { ProtestDetailViewDataBuilder } from './ProtestDetailViewDataBuilder';
describe('ProtestDetailViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform ProtestDetailApiDto to ProtestDetailViewData correctly', () => {
const protestDetailApiDto = {
id: 'protest-123',
leagueId: 'league-456',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [
{
type: 'time_penalty',
label: 'Time Penalty',
description: 'Add time to race result',
},
],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result).toEqual({
protestId: 'protest-123',
leagueId: 'league-456',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [
{
type: 'time_penalty',
label: 'Time Penalty',
description: 'Add time to race result',
},
],
});
});
it('should handle resolved status', () => {
const protestDetailApiDto = {
id: 'protest-456',
leagueId: 'league-789',
status: 'resolved',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
protestingDriver: {
id: 'driver-3',
name: 'Driver 3',
},
accusedDriver: {
id: 'driver-4',
name: 'Driver 4',
},
race: {
id: 'race-2',
name: 'Test Race 2',
scheduledAt: '2024-01-02T10:00:00Z',
},
penaltyTypes: [],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.status).toBe('resolved');
});
it('should handle multiple penalty types', () => {
const protestDetailApiDto = {
id: 'protest-789',
leagueId: 'league-101',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 15,
description: 'Contact at turn 7',
},
protestingDriver: {
id: 'driver-5',
name: 'Driver 5',
},
accusedDriver: {
id: 'driver-6',
name: 'Driver 6',
},
race: {
id: 'race-3',
name: 'Test Race 3',
scheduledAt: '2024-01-03T10:00:00Z',
},
penaltyTypes: [
{
type: 'time_penalty',
label: 'Time Penalty',
description: 'Add time to race result',
},
{
type: 'grid_penalty',
label: 'Grid Penalty',
description: 'Drop grid positions',
},
],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.penaltyTypes).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const protestDetailApiDto = {
id: 'protest-101',
leagueId: 'league-102',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [
{
type: 'time_penalty',
label: 'Time Penalty',
description: 'Add time to race result',
},
],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.protestId).toBe(protestDetailApiDto.id);
expect(result.leagueId).toBe(protestDetailApiDto.leagueId);
expect(result.status).toBe(protestDetailApiDto.status);
expect(result.submittedAt).toBe(protestDetailApiDto.submittedAt);
expect(result.incident).toEqual(protestDetailApiDto.incident);
expect(result.protestingDriver).toEqual(protestDetailApiDto.protestingDriver);
expect(result.accusedDriver).toEqual(protestDetailApiDto.accusedDriver);
expect(result.race).toEqual(protestDetailApiDto.race);
expect(result.penaltyTypes).toEqual(protestDetailApiDto.penaltyTypes);
});
it('should not modify the input DTO', () => {
const protestDetailApiDto = {
id: 'protest-102',
leagueId: 'league-103',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [],
};
const originalDto = { ...protestDetailApiDto };
ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(protestDetailApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle different status values', () => {
const protestDetailApiDto = {
id: 'protest-103',
leagueId: 'league-104',
status: 'rejected',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.status).toBe('rejected');
});
it('should handle lap 0', () => {
const protestDetailApiDto = {
id: 'protest-104',
leagueId: 'league-105',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 0,
description: 'Contact at start',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.incident.lap).toBe(0);
});
it('should handle empty description', () => {
const protestDetailApiDto = {
id: 'protest-105',
leagueId: 'league-106',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: '',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.incident.description).toBe('');
});
});
});

View File

@@ -0,0 +1,393 @@
import { describe, it, expect } from 'vitest';
import { RaceDetailViewDataBuilder } from './RaceDetailViewDataBuilder';
describe('RaceDetailViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform API DTO to RaceDetailViewData correctly', () => {
const apiDto = {
race: {
id: 'race-123',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
league: {
id: 'league-456',
name: 'Test League',
description: 'Test Description',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Open',
},
},
entryList: [
{
id: 'driver-1',
name: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
rating: 1500,
isCurrentUser: false,
},
],
registration: {
isUserRegistered: false,
canRegister: true,
},
userResult: {
position: 5,
startPosition: 10,
positionChange: 5,
incidents: 2,
isClean: false,
isPodium: false,
ratingChange: 10,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result).toEqual({
race: {
id: 'race-123',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
league: {
id: 'league-456',
name: 'Test League',
description: 'Test Description',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Open',
},
},
entryList: [
{
id: 'driver-1',
name: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
rating: 1500,
isCurrentUser: false,
},
],
registration: {
isUserRegistered: false,
canRegister: true,
},
userResult: {
position: 5,
startPosition: 10,
positionChange: 5,
incidents: 2,
isClean: false,
isPodium: false,
ratingChange: 10,
},
canReopenRace: false,
});
});
it('should handle race without league', () => {
const apiDto = {
race: {
id: 'race-456',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.league).toBeUndefined();
});
it('should handle race without user result', () => {
const apiDto = {
race: {
id: 'race-789',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.userResult).toBeUndefined();
});
it('should handle multiple entries in entry list', () => {
const apiDto = {
race: {
id: 'race-101',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
entryList: [
{
id: 'driver-1',
name: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
rating: 1500,
isCurrentUser: false,
},
{
id: 'driver-2',
name: 'Driver 2',
avatarUrl: 'avatar-url',
country: 'UK',
rating: 1600,
isCurrentUser: true,
},
],
registration: {
isUserRegistered: true,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.entryList).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto = {
race: {
id: 'race-102',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
league: {
id: 'league-103',
name: 'Test League',
description: 'Test Description',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Open',
},
},
entryList: [
{
id: 'driver-1',
name: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
rating: 1500,
isCurrentUser: false,
},
],
registration: {
isUserRegistered: false,
canRegister: true,
},
userResult: {
position: 5,
startPosition: 10,
positionChange: 5,
incidents: 2,
isClean: false,
isPodium: false,
ratingChange: 10,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.race.id).toBe(apiDto.race.id);
expect(result.race.track).toBe(apiDto.race.track);
expect(result.race.car).toBe(apiDto.race.car);
expect(result.race.scheduledAt).toBe(apiDto.race.scheduledAt);
expect(result.race.status).toBe(apiDto.race.status);
expect(result.race.sessionType).toBe(apiDto.race.sessionType);
expect(result.league?.id).toBe(apiDto.league.id);
expect(result.league?.name).toBe(apiDto.league.name);
expect(result.registration.isUserRegistered).toBe(apiDto.registration.isUserRegistered);
expect(result.registration.canRegister).toBe(apiDto.registration.canRegister);
expect(result.canReopenRace).toBe(apiDto.canReopenRace);
});
it('should not modify the input DTO', () => {
const apiDto = {
race: {
id: 'race-104',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const originalDto = { ...apiDto };
RaceDetailViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle null API DTO', () => {
const result = RaceDetailViewDataBuilder.build(null);
expect(result.race.id).toBe('');
expect(result.race.track).toBe('');
expect(result.race.car).toBe('');
expect(result.race.scheduledAt).toBe('');
expect(result.race.status).toBe('scheduled');
expect(result.race.sessionType).toBe('race');
expect(result.entryList).toHaveLength(0);
expect(result.registration.isUserRegistered).toBe(false);
expect(result.registration.canRegister).toBe(false);
expect(result.canReopenRace).toBe(false);
});
it('should handle undefined API DTO', () => {
const result = RaceDetailViewDataBuilder.build(undefined);
expect(result.race.id).toBe('');
expect(result.race.track).toBe('');
expect(result.race.car).toBe('');
expect(result.race.scheduledAt).toBe('');
expect(result.race.status).toBe('scheduled');
expect(result.race.sessionType).toBe('race');
expect(result.entryList).toHaveLength(0);
expect(result.registration.isUserRegistered).toBe(false);
expect(result.registration.canRegister).toBe(false);
expect(result.canReopenRace).toBe(false);
});
it('should handle race without entry list', () => {
const apiDto = {
race: {
id: 'race-105',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.entryList).toHaveLength(0);
});
it('should handle different race statuses', () => {
const apiDto = {
race: {
id: 'race-106',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'running',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.race.status).toBe('running');
});
it('should handle different session types', () => {
const apiDto = {
race: {
id: 'race-107',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'qualifying',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.race.sessionType).toBe('qualifying');
});
it('should handle canReopenRace true', () => {
const apiDto = {
race: {
id: 'race-108',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'completed',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: true,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.canReopenRace).toBe(true);
});
});
});

View File

@@ -0,0 +1,775 @@
import { describe, it, expect } from 'vitest';
import { RaceResultsViewDataBuilder } from './RaceResultsViewDataBuilder';
describe('RaceResultsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform API DTO to RaceResultsViewData correctly', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [
{
driverId: 'driver-2',
driverName: 'Driver 2',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
pointsSystem: {
1: 25,
2: 18,
3: 15,
},
fastestLapTime: 120000,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result).toEqual({
raceTrack: 'Test Track',
raceScheduledAt: '2024-01-01T10:00:00Z',
totalDrivers: 20,
leagueName: 'Test League',
raceSOF: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
driverAvatar: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [
{
driverId: 'driver-2',
driverName: 'Driver 2',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
pointsSystem: {
1: 25,
2: 18,
3: 15,
},
fastestLapTime: 120000,
});
});
it('should handle empty results and penalties', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 0,
},
league: {
name: 'Test League',
},
strengthOfField: null,
results: [],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.raceSOF).toBeNull();
});
it('should handle multiple results and penalties', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
{
position: 2,
driverId: 'driver-2',
driverName: 'Driver 2',
avatarUrl: 'avatar-url',
country: 'UK',
car: 'Test Car',
laps: 30,
time: '1:24.000',
fastestLap: '1:21.000',
points: 18,
incidents: 1,
isCurrentUser: true,
},
],
penalties: [
{
driverId: 'driver-3',
driverName: 'Driver 3',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
{
driverId: 'driver-4',
driverName: 'Driver 4',
type: 'grid_penalty',
value: 3,
reason: 'Qualifying infringement',
notes: null,
},
],
pointsSystem: {
1: 25,
2: 18,
3: 15,
},
fastestLapTime: 120000,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results).toHaveLength(2);
expect(result.penalties).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {
1: 25,
2: 18,
3: 15,
},
fastestLapTime: 120000,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.raceTrack).toBe(apiDto.race.track);
expect(result.raceScheduledAt).toBe(apiDto.race.scheduledAt);
expect(result.totalDrivers).toBe(apiDto.stats.totalDrivers);
expect(result.leagueName).toBe(apiDto.league.name);
expect(result.raceSOF).toBe(apiDto.strengthOfField);
expect(result.pointsSystem).toEqual(apiDto.pointsSystem);
expect(result.fastestLapTime).toBe(apiDto.fastestLapTime);
});
it('should not modify the input DTO', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const originalDto = { ...apiDto };
RaceResultsViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle null API DTO', () => {
const result = RaceResultsViewDataBuilder.build(null);
expect(result.raceSOF).toBeNull();
expect(result.results).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.pointsSystem).toEqual({});
expect(result.fastestLapTime).toBe(0);
});
it('should handle undefined API DTO', () => {
const result = RaceResultsViewDataBuilder.build(undefined);
expect(result.raceSOF).toBeNull();
expect(result.results).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.pointsSystem).toEqual({});
expect(result.fastestLapTime).toBe(0);
});
it('should handle results without country', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: null,
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].country).toBe('US');
});
it('should handle results without car', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: null,
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].car).toBe('Unknown');
});
it('should handle results without laps', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: null,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].laps).toBe(0);
});
it('should handle results without time', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: null,
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].time).toBe('0:00.00');
});
it('should handle results without fastest lap', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: null,
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].fastestLap).toBe('0.00');
});
it('should handle results without points', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: null,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].points).toBe(0);
});
it('should handle results without incidents', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: null,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].incidents).toBe(0);
});
it('should handle results without isCurrentUser', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: null,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].isCurrentUser).toBe(false);
});
it('should handle penalties without driver name', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [],
penalties: [
{
driverId: 'driver-1',
driverName: null,
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.penalties[0].driverName).toBe('Unknown');
});
it('should handle penalties without value', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [],
penalties: [
{
driverId: 'driver-1',
driverName: 'Driver 1',
type: 'time_penalty',
value: null,
reason: 'Track limits',
notes: 'Warning issued',
},
],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.penalties[0].value).toBe(0);
});
it('should handle penalties without reason', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [],
penalties: [
{
driverId: 'driver-1',
driverName: 'Driver 1',
type: 'time_penalty',
value: 5,
reason: null,
notes: 'Warning issued',
},
],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.penalties[0].reason).toBe('Penalty applied');
});
it('should handle different penalty types', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [],
penalties: [
{
driverId: 'driver-1',
driverName: 'Driver 1',
type: 'grid_penalty',
value: 3,
reason: 'Qualifying infringement',
notes: null,
},
{
driverId: 'driver-2',
driverName: 'Driver 2',
type: 'points_deduction',
value: 10,
reason: 'Dangerous driving',
notes: null,
},
{
driverId: 'driver-3',
driverName: 'Driver 3',
type: 'disqualification',
value: 0,
reason: 'Technical infringement',
notes: null,
},
{
driverId: 'driver-4',
driverName: 'Driver 4',
type: 'warning',
value: 0,
reason: 'Minor infraction',
notes: null,
},
{
driverId: 'driver-5',
driverName: 'Driver 5',
type: 'license_points',
value: 2,
reason: 'Multiple incidents',
notes: null,
},
],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.penalties[0].type).toBe('grid_penalty');
expect(result.penalties[1].type).toBe('points_deduction');
expect(result.penalties[2].type).toBe('disqualification');
expect(result.penalties[3].type).toBe('warning');
expect(result.penalties[4].type).toBe('license_points');
});
});
});

View File

@@ -0,0 +1,841 @@
import { describe, it, expect } from 'vitest';
import { RaceStewardingViewDataBuilder } from './RaceStewardingViewDataBuilder';
describe('RaceStewardingViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform API DTO to RaceStewardingViewData correctly', () => {
const apiDto = {
race: {
id: 'race-123',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-456',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-5',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
},
pendingCount: 1,
resolvedCount: 1,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result).toEqual({
race: {
id: 'race-123',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-456',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-5',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
},
pendingCount: 1,
resolvedCount: 1,
penaltiesCount: 1,
});
});
it('should handle empty protests and penalties', () => {
const apiDto = {
race: {
id: 'race-456',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-789',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests).toHaveLength(0);
expect(result.resolvedProtests).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.pendingCount).toBe(0);
expect(result.resolvedCount).toBe(0);
expect(result.penaltiesCount).toBe(0);
});
it('should handle multiple protests and penalties', () => {
const apiDto = {
race: {
id: 'race-789',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-101',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-3',
protestingDriverId: 'driver-5',
accusedDriverId: 'driver-6',
incident: {
lap: 15,
description: 'Contact at turn 7',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-7',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
{
id: 'penalty-2',
driverId: 'driver-8',
type: 'grid_penalty',
value: 3,
reason: 'Qualifying infringement',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
'driver-6': { id: 'driver-6', name: 'Driver 6' },
'driver-7': { id: 'driver-7', name: 'Driver 7' },
'driver-8': { id: 'driver-8', name: 'Driver 8' },
},
pendingCount: 2,
resolvedCount: 1,
penaltiesCount: 2,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests).toHaveLength(2);
expect(result.resolvedProtests).toHaveLength(1);
expect(result.penalties).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto = {
race: {
id: 'race-102',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-103',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
},
pendingCount: 1,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.race?.id).toBe(apiDto.race.id);
expect(result.race?.track).toBe(apiDto.race.track);
expect(result.race?.scheduledAt).toBe(apiDto.race.scheduledAt);
expect(result.league?.id).toBe(apiDto.league.id);
expect(result.pendingCount).toBe(apiDto.pendingCount);
expect(result.resolvedCount).toBe(apiDto.resolvedCount);
expect(result.penaltiesCount).toBe(apiDto.penaltiesCount);
});
it('should not modify the input DTO', () => {
const apiDto = {
race: {
id: 'race-104',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-105',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const originalDto = { ...apiDto };
RaceStewardingViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle null API DTO', () => {
const result = RaceStewardingViewDataBuilder.build(null);
expect(result.race).toBeNull();
expect(result.league).toBeNull();
expect(result.pendingProtests).toHaveLength(0);
expect(result.resolvedProtests).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.driverMap).toEqual({});
expect(result.pendingCount).toBe(0);
expect(result.resolvedCount).toBe(0);
expect(result.penaltiesCount).toBe(0);
});
it('should handle undefined API DTO', () => {
const result = RaceStewardingViewDataBuilder.build(undefined);
expect(result.race).toBeNull();
expect(result.league).toBeNull();
expect(result.pendingProtests).toHaveLength(0);
expect(result.resolvedProtests).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.driverMap).toEqual({});
expect(result.pendingCount).toBe(0);
expect(result.resolvedCount).toBe(0);
expect(result.penaltiesCount).toBe(0);
});
it('should handle race without league', () => {
const apiDto = {
race: {
id: 'race-106',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.league).toBeNull();
});
it('should handle protests without proof video', () => {
const apiDto = {
race: {
id: 'race-107',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-108',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: null,
decisionNotes: null,
},
],
resolvedProtests: [],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
},
pendingCount: 1,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests[0].proofVideoUrl).toBeNull();
});
it('should handle protests without decision notes', () => {
const apiDto = {
race: {
id: 'race-109',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-110',
},
pendingProtests: [],
resolvedProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
},
pendingCount: 0,
resolvedCount: 1,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.resolvedProtests[0].decisionNotes).toBeNull();
});
it('should handle penalties without notes', () => {
const apiDto = {
race: {
id: 'race-111',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-112',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].notes).toBeNull();
});
it('should handle penalties without value', () => {
const apiDto = {
race: {
id: 'race-113',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-114',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'disqualification',
value: null,
reason: 'Technical infringement',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].value).toBe(0);
});
it('should handle penalties without reason', () => {
const apiDto = {
race: {
id: 'race-115',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-116',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'warning',
value: 0,
reason: null,
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].reason).toBe('');
});
it('should handle different protest statuses', () => {
const apiDto = {
race: {
id: 'race-117',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-118',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
{
id: 'protest-3',
protestingDriverId: 'driver-5',
accusedDriverId: 'driver-6',
incident: {
lap: 15,
description: 'Contact at turn 7',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'rejected',
proofVideoUrl: 'video-url',
decisionNotes: 'Insufficient evidence',
},
],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
'driver-6': { id: 'driver-6', name: 'Driver 6' },
},
pendingCount: 1,
resolvedCount: 2,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests[0].status).toBe('pending');
expect(result.resolvedProtests[0].status).toBe('resolved');
expect(result.resolvedProtests[1].status).toBe('rejected');
});
it('should handle different penalty types', () => {
const apiDto = {
race: {
id: 'race-119',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-120',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
{
id: 'penalty-2',
driverId: 'driver-2',
type: 'grid_penalty',
value: 3,
reason: 'Qualifying infringement',
notes: null,
},
{
id: 'penalty-3',
driverId: 'driver-3',
type: 'points_deduction',
value: 10,
reason: 'Dangerous driving',
notes: null,
},
{
id: 'penalty-4',
driverId: 'driver-4',
type: 'disqualification',
value: 0,
reason: 'Technical infringement',
notes: null,
},
{
id: 'penalty-5',
driverId: 'driver-5',
type: 'warning',
value: 0,
reason: 'Minor infraction',
notes: null,
},
{
id: 'penalty-6',
driverId: 'driver-6',
type: 'license_points',
value: 2,
reason: 'Multiple incidents',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
'driver-6': { id: 'driver-6', name: 'Driver 6' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 6,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].type).toBe('time_penalty');
expect(result.penalties[1].type).toBe('grid_penalty');
expect(result.penalties[2].type).toBe('points_deduction');
expect(result.penalties[3].type).toBe('disqualification');
expect(result.penalties[4].type).toBe('warning');
expect(result.penalties[5].type).toBe('license_points');
});
it('should handle empty driver map', () => {
const apiDto = {
race: {
id: 'race-121',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-122',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.driverMap).toEqual({});
});
it('should handle count values from DTO', () => {
const apiDto = {
race: {
id: 'race-123',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-124',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 5,
resolvedCount: 10,
penaltiesCount: 3,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingCount).toBe(5);
expect(result.resolvedCount).toBe(10);
expect(result.penaltiesCount).toBe(3);
});
it('should calculate counts from arrays when not provided', () => {
const apiDto = {
race: {
id: 'race-125',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-126',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-5',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
},
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingCount).toBe(1);
expect(result.resolvedCount).toBe(1);
expect(result.penaltiesCount).toBe(1);
});
});
});

View 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);
});
});
});

View File

@@ -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');
});
});
});
});

View File

@@ -0,0 +1,407 @@
import { describe, it, expect } from 'vitest';
import { RulebookViewDataBuilder } from './RulebookViewDataBuilder';
import type { RulebookApiDto } from '@/lib/types/tbd/RulebookApiDto';
describe('RulebookViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform RulebookApiDto to RulebookViewData correctly', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-123',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
{ sessionType: 'race', position: 2, points: 18 },
{ sessionType: 'race', position: 3, points: 15 },
],
bonusSummary: [
{ type: 'fastest_lap', points: 5, description: 'Fastest lap' },
],
},
],
dropPolicySummary: 'Drop 2 worst results',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result).toEqual({
leagueId: 'league-123',
gameName: 'iRacing',
scoringPresetName: 'Standard',
championshipsCount: 1,
sessionTypes: 'race',
dropPolicySummary: 'Drop 2 worst results',
hasActiveDropPolicy: true,
positionPoints: [
{ position: 1, points: 25 },
{ position: 2, points: 18 },
{ position: 3, points: 15 },
],
bonusPoints: [
{ type: 'fastest_lap', points: 5, description: 'Fastest lap' },
],
hasBonusPoints: true,
});
});
it('should handle championship without driver type', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-456',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'team',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.positionPoints).toEqual([{ position: 1, points: 25 }]);
});
it('should handle multiple championships', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-789',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
{
type: 'team',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.championshipsCount).toBe(2);
});
it('should handle empty bonus points', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-101',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.bonusPoints).toEqual([]);
expect(result.hasBonusPoints).toBe(false);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-102',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [
{ type: 'fastest_lap', points: 5, description: 'Fastest lap' },
],
},
],
dropPolicySummary: 'Drop 2 worst results',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.leagueId).toBe(rulebookApiDto.leagueId);
expect(result.gameName).toBe(rulebookApiDto.scoringConfig.gameName);
expect(result.scoringPresetName).toBe(rulebookApiDto.scoringConfig.scoringPresetName);
expect(result.dropPolicySummary).toBe(rulebookApiDto.scoringConfig.dropPolicySummary);
});
it('should not modify the input DTO', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-103',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const originalDto = { ...rulebookApiDto };
RulebookViewDataBuilder.build(rulebookApiDto);
expect(rulebookApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle empty drop policy', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-104',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: '',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.hasActiveDropPolicy).toBe(false);
});
it('should handle drop policy with "All" keyword', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-105',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'Drop all results',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.hasActiveDropPolicy).toBe(false);
});
it('should handle multiple session types', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-106',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race', 'qualifying', 'practice'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.sessionTypes).toBe('race, qualifying, practice');
});
it('should handle single session type', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-107',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.sessionTypes).toBe('race');
});
it('should handle empty points preview', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-108',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.positionPoints).toEqual([]);
});
it('should handle points preview with different session types', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-109',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
{ sessionType: 'qualifying', position: 1, points: 10 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.positionPoints).toEqual([{ position: 1, points: 25 }]);
});
it('should handle points preview with non-sequential positions', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-110',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
{ sessionType: 'race', position: 3, points: 15 },
{ sessionType: 'race', position: 2, points: 18 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.positionPoints).toEqual([
{ position: 1, points: 25 },
{ position: 2, points: 18 },
{ position: 3, points: 15 },
]);
});
it('should handle multiple bonus points', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-111',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [
{ type: 'fastest_lap', points: 5, description: 'Fastest lap' },
{ type: 'pole_position', points: 3, description: 'Pole position' },
{ type: 'clean_race', points: 2, description: 'Clean race' },
],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.bonusPoints).toHaveLength(3);
expect(result.hasBonusPoints).toBe(true);
});
});
});

View File

@@ -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');
});
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});
});

View File

@@ -0,0 +1,223 @@
import { describe, it, expect } from 'vitest';
import { SponsorshipRequestsPageViewDataBuilder } from './SponsorshipRequestsPageViewDataBuilder';
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
describe('SponsorshipRequestsPageViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform GetPendingSponsorshipRequestsOutputDTO to SponsorshipRequestsViewData correctly', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-123',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result).toEqual({
sections: [
{
entityType: 'driver',
entityId: 'driver-123',
entityName: 'driver',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogoUrl: 'logo-url',
message: 'Test message',
createdAtIso: '2024-01-01T10:00:00Z',
},
],
},
],
});
});
it('should handle empty requests', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-456',
requests: [],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections).toHaveLength(1);
expect(result.sections[0].requests).toHaveLength(0);
});
it('should handle multiple requests', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'season',
entityId: 'season-789',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Sponsor 1',
sponsorLogo: 'logo-1',
message: 'Message 1',
createdAt: '2024-01-01T10:00:00Z',
},
{
id: 'request-2',
sponsorId: 'sponsor-2',
sponsorName: 'Sponsor 2',
sponsorLogo: 'logo-2',
message: 'Message 2',
createdAt: '2024-01-02T10:00:00Z',
},
],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].requests).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-101',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].entityType).toBe(sponsorshipRequestsPageDto.entityType);
expect(result.sections[0].entityId).toBe(sponsorshipRequestsPageDto.entityId);
expect(result.sections[0].requests[0].id).toBe(sponsorshipRequestsPageDto.requests[0].id);
expect(result.sections[0].requests[0].sponsorId).toBe(sponsorshipRequestsPageDto.requests[0].sponsorId);
expect(result.sections[0].requests[0].sponsorName).toBe(sponsorshipRequestsPageDto.requests[0].sponsorName);
expect(result.sections[0].requests[0].sponsorLogoUrl).toBe(sponsorshipRequestsPageDto.requests[0].sponsorLogo);
expect(result.sections[0].requests[0].message).toBe(sponsorshipRequestsPageDto.requests[0].message);
expect(result.sections[0].requests[0].createdAtIso).toBe(sponsorshipRequestsPageDto.requests[0].createdAt);
});
it('should not modify the input DTO', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-102',
requests: [],
};
const originalDto = { ...sponsorshipRequestsPageDto };
SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(sponsorshipRequestsPageDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle requests without sponsor logo', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-103',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: null,
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].requests[0].sponsorLogoUrl).toBeNull();
});
it('should handle requests without message', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-104',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: null,
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].requests[0].message).toBeNull();
});
it('should handle different entity types', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-105',
requests: [],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].entityType).toBe('team');
});
it('should handle entity name for driver type', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-106',
requests: [],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].entityName).toBe('driver');
});
it('should handle entity name for team type', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-107',
requests: [],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].entityName).toBe('team');
});
it('should handle entity name for season type', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'season',
entityId: 'season-108',
requests: [],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].entityName).toBe('season');
});
});
});

View File

@@ -0,0 +1,223 @@
import { describe, it, expect } from 'vitest';
import { SponsorshipRequestsViewDataBuilder } from './SponsorshipRequestsViewDataBuilder';
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
describe('SponsorshipRequestsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform GetPendingSponsorshipRequestsOutputDTO to SponsorshipRequestsViewData correctly', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-123',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result).toEqual({
sections: [
{
entityType: 'driver',
entityId: 'driver-123',
entityName: 'Driver',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogoUrl: 'logo-url',
message: 'Test message',
createdAtIso: '2024-01-01T10:00:00Z',
},
],
},
],
});
});
it('should handle empty requests', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-456',
requests: [],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections).toHaveLength(1);
expect(result.sections[0].requests).toHaveLength(0);
});
it('should handle multiple requests', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'season',
entityId: 'season-789',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Sponsor 1',
sponsorLogo: 'logo-1',
message: 'Message 1',
createdAt: '2024-01-01T10:00:00Z',
},
{
id: 'request-2',
sponsorId: 'sponsor-2',
sponsorName: 'Sponsor 2',
sponsorLogo: 'logo-2',
message: 'Message 2',
createdAt: '2024-01-02T10:00:00Z',
},
],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].requests).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-101',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].entityType).toBe(sponsorshipRequestsDto.entityType);
expect(result.sections[0].entityId).toBe(sponsorshipRequestsDto.entityId);
expect(result.sections[0].requests[0].id).toBe(sponsorshipRequestsDto.requests[0].id);
expect(result.sections[0].requests[0].sponsorId).toBe(sponsorshipRequestsDto.requests[0].sponsorId);
expect(result.sections[0].requests[0].sponsorName).toBe(sponsorshipRequestsDto.requests[0].sponsorName);
expect(result.sections[0].requests[0].sponsorLogoUrl).toBe(sponsorshipRequestsDto.requests[0].sponsorLogo);
expect(result.sections[0].requests[0].message).toBe(sponsorshipRequestsDto.requests[0].message);
expect(result.sections[0].requests[0].createdAtIso).toBe(sponsorshipRequestsDto.requests[0].createdAt);
});
it('should not modify the input DTO', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-102',
requests: [],
};
const originalDto = { ...sponsorshipRequestsDto };
SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(sponsorshipRequestsDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle requests without sponsor logo', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-103',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: null,
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].requests[0].sponsorLogoUrl).toBeNull();
});
it('should handle requests without message', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-104',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: null,
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].requests[0].message).toBeNull();
});
it('should handle different entity types', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-105',
requests: [],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].entityType).toBe('team');
});
it('should handle entity name for driver type', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-106',
requests: [],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].entityName).toBe('Driver');
});
it('should handle entity name for team type', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-107',
requests: [],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].entityName).toBe('team');
});
it('should handle entity name for season type', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'season',
entityId: 'season-108',
requests: [],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].entityName).toBe('season');
});
});
});

View File

@@ -0,0 +1,349 @@
import { describe, it, expect } from 'vitest';
import { StewardingViewDataBuilder } from './StewardingViewDataBuilder';
import type { StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto';
describe('StewardingViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform StewardingApiDto to StewardingViewData correctly', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-123',
totalPending: 5,
totalResolved: 10,
totalPenalties: 3,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1', 'protest-2'],
resolvedProtests: ['protest-3'],
penalties: ['penalty-1'],
},
],
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
},
],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result).toEqual({
leagueId: 'league-123',
totalPending: 5,
totalResolved: 10,
totalPenalties: 3,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1', 'protest-2'],
resolvedProtests: ['protest-3'],
penalties: ['penalty-1'],
},
],
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
},
],
});
});
it('should handle empty races and drivers', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-456',
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
races: [],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races).toHaveLength(0);
expect(result.drivers).toHaveLength(0);
});
it('should handle multiple races and drivers', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-789',
totalPending: 10,
totalResolved: 20,
totalPenalties: 5,
races: [
{
id: 'race-1',
track: 'Test Track 1',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1'],
resolvedProtests: ['protest-2'],
penalties: ['penalty-1'],
},
{
id: 'race-2',
track: 'Test Track 2',
scheduledAt: '2024-01-02T10:00:00Z',
pendingProtests: ['protest-3'],
resolvedProtests: ['protest-4'],
penalties: ['penalty-2'],
},
],
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
},
{
id: 'driver-2',
name: 'Driver 2',
},
],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races).toHaveLength(2);
expect(result.drivers).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-101',
totalPending: 5,
totalResolved: 10,
totalPenalties: 3,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1'],
resolvedProtests: ['protest-2'],
penalties: ['penalty-1'],
},
],
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
},
],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.leagueId).toBe(stewardingApiDto.leagueId);
expect(result.totalPending).toBe(stewardingApiDto.totalPending);
expect(result.totalResolved).toBe(stewardingApiDto.totalResolved);
expect(result.totalPenalties).toBe(stewardingApiDto.totalPenalties);
expect(result.races).toEqual(stewardingApiDto.races);
expect(result.drivers).toEqual(stewardingApiDto.drivers);
});
it('should not modify the input DTO', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-102',
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
races: [],
drivers: [],
};
const originalDto = { ...stewardingApiDto };
StewardingViewDataBuilder.build(stewardingApiDto);
expect(stewardingApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle null API DTO', () => {
const result = StewardingViewDataBuilder.build(null);
expect(result.leagueId).toBeUndefined();
expect(result.totalPending).toBe(0);
expect(result.totalResolved).toBe(0);
expect(result.totalPenalties).toBe(0);
expect(result.races).toHaveLength(0);
expect(result.drivers).toHaveLength(0);
});
it('should handle undefined API DTO', () => {
const result = StewardingViewDataBuilder.build(undefined);
expect(result.leagueId).toBeUndefined();
expect(result.totalPending).toBe(0);
expect(result.totalResolved).toBe(0);
expect(result.totalPenalties).toBe(0);
expect(result.races).toHaveLength(0);
expect(result.drivers).toHaveLength(0);
});
it('should handle races without pending protests', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-103',
totalPending: 0,
totalResolved: 5,
totalPenalties: 2,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: [],
resolvedProtests: ['protest-1'],
penalties: ['penalty-1'],
},
],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races[0].pendingProtests).toHaveLength(0);
});
it('should handle races without resolved protests', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-104',
totalPending: 5,
totalResolved: 0,
totalPenalties: 2,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1'],
resolvedProtests: [],
penalties: ['penalty-1'],
},
],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races[0].resolvedProtests).toHaveLength(0);
});
it('should handle races without penalties', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-105',
totalPending: 5,
totalResolved: 10,
totalPenalties: 0,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1'],
resolvedProtests: ['protest-2'],
penalties: [],
},
],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races[0].penalties).toHaveLength(0);
});
it('should handle races with empty arrays', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-106',
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: [],
resolvedProtests: [],
penalties: [],
},
],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races[0].pendingProtests).toHaveLength(0);
expect(result.races[0].resolvedProtests).toHaveLength(0);
expect(result.races[0].penalties).toHaveLength(0);
});
it('should handle drivers without name', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-107',
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
races: [],
drivers: [
{
id: 'driver-1',
name: null,
},
],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.drivers[0].name).toBeNull();
});
it('should handle count values from DTO', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-108',
totalPending: 15,
totalResolved: 25,
totalPenalties: 8,
races: [],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.totalPending).toBe(15);
expect(result.totalResolved).toBe(25);
expect(result.totalPenalties).toBe(8);
});
it('should calculate counts from arrays when not provided', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-109',
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1', 'protest-2'],
resolvedProtests: ['protest-3', 'protest-4', 'protest-5'],
penalties: ['penalty-1', 'penalty-2'],
},
],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.totalPending).toBe(2);
expect(result.totalResolved).toBe(3);
expect(result.totalPenalties).toBe(2);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -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);
});
});
});
});

View File

@@ -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);
});
});
});

View 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);
});
});
});

View File

@@ -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);
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,449 @@
import { describe, it, expect } from 'vitest';
import { DriversViewModelBuilder } from './DriversViewModelBuilder';
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
describe('DriversViewModelBuilder', () => {
describe('happy paths', () => {
it('should transform DriversLeaderboardDTO to DriverLeaderboardViewModel correctly', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers).toHaveLength(2);
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[0].name).toBe('Driver 1');
expect(result.drivers[0].country).toBe('US');
expect(result.drivers[0].avatarUrl).toBe('avatar-url');
expect(result.drivers[0].rating).toBe(1500);
expect(result.drivers[0].globalRank).toBe(1);
expect(result.drivers[0].consistency).toBe(95);
expect(result.drivers[1].id).toBe('driver-2');
expect(result.drivers[1].name).toBe('Driver 2');
expect(result.drivers[1].country).toBe('UK');
expect(result.drivers[1].avatarUrl).toBe('avatar-url');
expect(result.drivers[1].rating).toBe(1450);
expect(result.drivers[1].globalRank).toBe(2);
expect(result.drivers[1].consistency).toBe(90);
});
it('should handle empty drivers array', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers).toHaveLength(0);
});
it('should handle single driver', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers).toHaveLength(1);
});
it('should handle multiple drivers', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
{
id: 'driver-3',
name: 'Driver 3',
country: 'DE',
avatarUrl: 'avatar-url',
rating: 1400,
globalRank: 3,
consistency: 85,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers).toHaveLength(3);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].id).toBe(driversLeaderboardDto.drivers[0].id);
expect(result.drivers[0].name).toBe(driversLeaderboardDto.drivers[0].name);
expect(result.drivers[0].country).toBe(driversLeaderboardDto.drivers[0].country);
expect(result.drivers[0].avatarUrl).toBe(driversLeaderboardDto.drivers[0].avatarUrl);
expect(result.drivers[0].rating).toBe(driversLeaderboardDto.drivers[0].rating);
expect(result.drivers[0].globalRank).toBe(driversLeaderboardDto.drivers[0].globalRank);
expect(result.drivers[0].consistency).toBe(driversLeaderboardDto.drivers[0].consistency);
});
it('should not modify the input DTO', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
],
};
const originalDto = { ...driversLeaderboardDto };
DriversViewModelBuilder.build(driversLeaderboardDto);
expect(driversLeaderboardDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle driver without avatar', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: null,
rating: 1500,
globalRank: 1,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].avatarUrl).toBeNull();
});
it('should handle driver without country', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: null,
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].country).toBeNull();
});
it('should handle driver without rating', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: null,
globalRank: 1,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].rating).toBeNull();
});
it('should handle driver without global rank', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: null,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].globalRank).toBeNull();
});
it('should handle driver without consistency', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: null,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].consistency).toBeNull();
});
it('should handle different countries', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
{
id: 'driver-3',
name: 'Driver 3',
country: 'DE',
avatarUrl: 'avatar-url',
rating: 1400,
globalRank: 3,
consistency: 85,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].country).toBe('US');
expect(result.drivers[1].country).toBe('UK');
expect(result.drivers[2].country).toBe('DE');
});
it('should handle different ratings', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
{
id: 'driver-3',
name: 'Driver 3',
country: 'DE',
avatarUrl: 'avatar-url',
rating: 1400,
globalRank: 3,
consistency: 85,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].rating).toBe(1500);
expect(result.drivers[1].rating).toBe(1450);
expect(result.drivers[2].rating).toBe(1400);
});
it('should handle different global ranks', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
{
id: 'driver-3',
name: 'Driver 3',
country: 'DE',
avatarUrl: 'avatar-url',
rating: 1400,
globalRank: 3,
consistency: 85,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].globalRank).toBe(1);
expect(result.drivers[1].globalRank).toBe(2);
expect(result.drivers[2].globalRank).toBe(3);
});
it('should handle different consistency values', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
{
id: 'driver-3',
name: 'Driver 3',
country: 'DE',
avatarUrl: 'avatar-url',
rating: 1400,
globalRank: 3,
consistency: 85,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].consistency).toBe(95);
expect(result.drivers[1].consistency).toBe(90);
expect(result.drivers[2].consistency).toBe(85);
});
it('should handle large number of drivers', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: Array.from({ length: 100 }, (_, i) => ({
id: `driver-${i + 1}`,
name: `Driver ${i + 1}`,
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500 - i,
globalRank: i + 1,
consistency: 95 - i * 0.1,
})),
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers).toHaveLength(100);
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[99].id).toBe('driver-100');
});
});
});

View File

@@ -0,0 +1,495 @@
import { describe, it, expect } from 'vitest';
import { ForgotPasswordViewModelBuilder } from './ForgotPasswordViewModelBuilder';
import type { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData';
describe('ForgotPasswordViewModelBuilder', () => {
describe('happy paths', () => {
it('should transform ForgotPasswordViewData to ForgotPasswordViewModel correctly', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result).toBeDefined();
expect(result.returnTo).toBe('/dashboard');
expect(result.formState).toBeDefined();
expect(result.formState.fields).toBeDefined();
expect(result.formState.fields.email).toBeDefined();
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.isValid).toBe(true);
expect(result.formState.isSubmitting).toBe(false);
expect(result.formState.submitError).toBeUndefined();
expect(result.formState.submitCount).toBe(0);
expect(result.hasInsufficientPermissions).toBe(false);
expect(result.error).toBeNull();
expect(result.successMessage).toBeNull();
expect(result.isProcessing).toBe(false);
});
it('should handle different returnTo paths', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/login',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/login');
});
it('should handle empty returnTo', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('');
});
});
describe('data transformation', () => {
it('should preserve all viewData fields in the output', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(forgotPasswordViewData.returnTo);
});
it('should not modify the input viewData', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard',
};
const originalViewData = { ...forgotPasswordViewData };
ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(forgotPasswordViewData).toEqual(originalViewData);
});
});
describe('edge cases', () => {
it('should handle null returnTo', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: null,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBeNull();
});
it('should handle undefined returnTo', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: undefined,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBeUndefined();
});
it('should handle complex returnTo paths', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/leagues/league-123/settings',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings');
});
it('should handle returnTo with query parameters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?tab=settings',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings');
});
it('should handle returnTo with hash', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard#section',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard#section');
});
it('should handle returnTo with special characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/leagues/league-123/settings?tab=general#section',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?tab=general#section');
});
it('should handle very long returnTo path', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(100);
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: longPath,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(longPath);
});
it('should handle returnTo with encoded characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe');
});
it('should handle returnTo with multiple query parameters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?tab=settings&filter=active&sort=name',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings&filter=active&sort=name');
});
it('should handle returnTo with fragment identifier', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard#section-1',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard#section-1');
});
it('should handle returnTo with multiple fragments', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard#section-1#subsection-2',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard#section-1#subsection-2');
});
it('should handle returnTo with trailing slash', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/');
});
it('should handle returnTo with leading slash', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: 'dashboard',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('dashboard');
});
it('should handle returnTo with dots', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/../login',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/../login');
});
it('should handle returnTo with double dots', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/../../login',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/../../login');
});
it('should handle returnTo with percent encoding', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com');
});
it('should handle returnTo with plus signs', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?query=hello+world',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?query=hello+world');
});
it('should handle returnTo with ampersands', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?tab=settings&filter=active',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings&filter=active');
});
it('should handle returnTo with equals signs', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?tab=settings=value',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings=value');
});
it('should handle returnTo with multiple equals signs', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?tab=settings=value&filter=active=true',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings=value&filter=active=true');
});
it('should handle returnTo with semicolons', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard;jsessionid=123',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard;jsessionid=123');
});
it('should handle returnTo with colons', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard:section',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard:section');
});
it('should handle returnTo with commas', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?filter=a,b,c',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?filter=a,b,c');
});
it('should handle returnTo with spaces', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John Doe');
});
it('should handle returnTo with tabs', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\tDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\tDoe');
});
it('should handle returnTo with newlines', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\nDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\nDoe');
});
it('should handle returnTo with carriage returns', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\rDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\rDoe');
});
it('should handle returnTo with form feeds', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\fDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\fDoe');
});
it('should handle returnTo with vertical tabs', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\vDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\vDoe');
});
it('should handle returnTo with backspaces', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\bDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\bDoe');
});
it('should handle returnTo with null bytes', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\0Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\0Doe');
});
it('should handle returnTo with bell characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\aDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\aDoe');
});
it('should handle returnTo with escape characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\eDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\eDoe');
});
it('should handle returnTo with unicode characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\u00D6Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\u00D6Doe');
});
it('should handle returnTo with emoji', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John😀Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John😀Doe');
});
it('should handle returnTo with special symbols', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John!@#$%^&*()_+-=[]{}|;:,.<>?Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John!@#$%^&*()_+-=[]{}|;:,.<>?Doe');
});
it('should handle returnTo with mixed special characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com&filter=active=true&sort=name#section-1',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com&filter=active=true&sort=name#section-1');
});
it('should handle returnTo with very long path', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(1000);
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: longPath,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(longPath);
});
it('should handle returnTo with very long query string', () => {
const longQuery = '/dashboard?' + 'a'.repeat(1000) + '=value';
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: longQuery,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(longQuery);
});
it('should handle returnTo with very long fragment', () => {
const longFragment = '/dashboard#' + 'a'.repeat(1000);
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: longFragment,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(longFragment);
});
it('should handle returnTo with mixed very long components', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(500);
const longQuery = '?' + 'b'.repeat(500) + '=value';
const longFragment = '#' + 'c'.repeat(500);
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: longPath + longQuery + longFragment,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(longPath + longQuery + longFragment);
});
});
});

View File

@@ -0,0 +1,612 @@
import { describe, it, expect } from 'vitest';
import { LeagueSummaryViewModelBuilder } from './LeagueSummaryViewModelBuilder';
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
describe('LeagueSummaryViewModelBuilder', () => {
describe('happy paths', () => {
it('should transform LeaguesViewData to LeagueSummaryViewModel correctly', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-123',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result).toEqual({
id: 'league-123',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
});
});
it('should handle league without description', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-456',
name: 'Test League',
description: null,
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.description).toBe('');
});
it('should handle league without category', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-789',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: null,
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.category).toBeUndefined();
});
it('should handle league without scoring', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-101',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: null,
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.scoring).toBeUndefined();
});
it('should handle league without maxTeams', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-102',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: null,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.maxTeams).toBe(0);
});
it('should handle league without usedTeamSlots', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-103',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: null,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.usedTeamSlots).toBe(0);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-104',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.id).toBe(league.id);
expect(result.name).toBe(league.name);
expect(result.description).toBe(league.description);
expect(result.logoUrl).toBe(league.logoUrl);
expect(result.ownerId).toBe(league.ownerId);
expect(result.createdAt).toBe(league.createdAt);
expect(result.maxDrivers).toBe(league.maxDrivers);
expect(result.usedDriverSlots).toBe(league.usedDriverSlots);
expect(result.maxTeams).toBe(league.maxTeams);
expect(result.usedTeamSlots).toBe(league.usedTeamSlots);
expect(result.structureSummary).toBe(league.structureSummary);
expect(result.timingSummary).toBe(league.timingSummary);
expect(result.category).toBe(league.category);
expect(result.scoring).toEqual(league.scoring);
});
it('should not modify the input DTO', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-105',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const originalLeague = { ...league };
LeagueSummaryViewModelBuilder.build(league);
expect(league).toEqual(originalLeague);
});
});
describe('edge cases', () => {
it('should handle league with empty description', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-106',
name: 'Test League',
description: '',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.description).toBe('');
});
it('should handle league with different categories', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-107',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Amateur',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.category).toBe('Amateur');
});
it('should handle league with different scoring types', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-108',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'team',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.scoring?.primaryChampionshipType).toBe('team');
});
it('should handle league with different scoring systems', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-109',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'custom',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.scoring?.pointsSystem).toBe('custom');
});
it('should handle league with different structure summaries', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-110',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Multiple championships',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.structureSummary).toBe('Multiple championships');
});
it('should handle league with different timing summaries', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-111',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Bi-weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.timingSummary).toBe('Bi-weekly races');
});
it('should handle league with different maxDrivers', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-112',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 64,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.maxDrivers).toBe(64);
});
it('should handle league with different usedDriverSlots', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-113',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 15,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.usedDriverSlots).toBe(15);
});
it('should handle league with different maxTeams', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-114',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 32,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.maxTeams).toBe(32);
});
it('should handle league with different usedTeamSlots', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-115',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 5,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.usedTeamSlots).toBe(5);
});
it('should handle league with zero maxTeams', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-116',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 0,
usedTeamSlots: 0,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.maxTeams).toBe(0);
});
it('should handle league with zero usedTeamSlots', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-117',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 0,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.usedTeamSlots).toBe(0);
});
it('should handle league with different primary championship types', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-118',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'nations',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.scoring?.primaryChampionshipType).toBe('nations');
});
it('should handle league with different primary championship types (trophy)', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-119',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'trophy',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.scoring?.primaryChampionshipType).toBe('trophy');
});
});
});

View File

@@ -0,0 +1,587 @@
import { describe, it, expect } from 'vitest';
import { LoginViewModelBuilder } from './LoginViewModelBuilder';
import type { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData';
describe('LoginViewModelBuilder', () => {
describe('happy paths', () => {
it('should transform LoginViewData to LoginViewModel correctly', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result).toBeDefined();
expect(result.returnTo).toBe('/dashboard');
expect(result.hasInsufficientPermissions).toBe(false);
expect(result.formState).toBeDefined();
expect(result.formState.fields).toBeDefined();
expect(result.formState.fields.email).toBeDefined();
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).toBeDefined();
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).toBeDefined();
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);
expect(result.formState.isValid).toBe(true);
expect(result.formState.isSubmitting).toBe(false);
expect(result.formState.submitError).toBeUndefined();
expect(result.formState.submitCount).toBe(0);
expect(result.uiState).toBeDefined();
expect(result.uiState.showPassword).toBe(false);
expect(result.uiState.showErrorDetails).toBe(false);
expect(result.error).toBeNull();
expect(result.isProcessing).toBe(false);
});
it('should handle different returnTo paths', () => {
const loginViewData: LoginViewData = {
returnTo: '/login',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/login');
});
it('should handle empty returnTo', () => {
const loginViewData: LoginViewData = {
returnTo: '',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('');
});
it('should handle hasInsufficientPermissions true', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: true,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.hasInsufficientPermissions).toBe(true);
});
});
describe('data transformation', () => {
it('should preserve all viewData fields in the output', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(loginViewData.returnTo);
expect(result.hasInsufficientPermissions).toBe(loginViewData.hasInsufficientPermissions);
});
it('should not modify the input viewData', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const originalViewData = { ...loginViewData };
LoginViewModelBuilder.build(loginViewData);
expect(loginViewData).toEqual(originalViewData);
});
});
describe('edge cases', () => {
it('should handle null returnTo', () => {
const loginViewData: LoginViewData = {
returnTo: null,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBeNull();
});
it('should handle undefined returnTo', () => {
const loginViewData: LoginViewData = {
returnTo: undefined,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBeUndefined();
});
it('should handle complex returnTo paths', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/leagues/league-123/settings',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings');
});
it('should handle returnTo with query parameters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?tab=settings',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings');
});
it('should handle returnTo with hash', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard#section',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard#section');
});
it('should handle returnTo with special characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/leagues/league-123/settings?tab=general#section',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?tab=general#section');
});
it('should handle very long returnTo path', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(100);
const loginViewData: LoginViewData = {
returnTo: longPath,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(longPath);
});
it('should handle returnTo with encoded characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe');
});
it('should handle returnTo with multiple query parameters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?tab=settings&filter=active&sort=name',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings&filter=active&sort=name');
});
it('should handle returnTo with fragment identifier', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard#section-1',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard#section-1');
});
it('should handle returnTo with multiple fragments', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard#section-1#subsection-2',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard#section-1#subsection-2');
});
it('should handle returnTo with trailing slash', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/');
});
it('should handle returnTo with leading slash', () => {
const loginViewData: LoginViewData = {
returnTo: 'dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('dashboard');
});
it('should handle returnTo with dots', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/../login',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/../login');
});
it('should handle returnTo with double dots', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/../../login',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/../../login');
});
it('should handle returnTo with percent encoding', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com');
});
it('should handle returnTo with plus signs', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?query=hello+world',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?query=hello+world');
});
it('should handle returnTo with ampersands', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?tab=settings&filter=active',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings&filter=active');
});
it('should handle returnTo with equals signs', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?tab=settings=value',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings=value');
});
it('should handle returnTo with multiple equals signs', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?tab=settings=value&filter=active=true',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings=value&filter=active=true');
});
it('should handle returnTo with semicolons', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard;jsessionid=123',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard;jsessionid=123');
});
it('should handle returnTo with colons', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard:section',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard:section');
});
it('should handle returnTo with commas', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?filter=a,b,c',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?filter=a,b,c');
});
it('should handle returnTo with spaces', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John Doe');
});
it('should handle returnTo with tabs', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\tDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\tDoe');
});
it('should handle returnTo with newlines', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\nDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\nDoe');
});
it('should handle returnTo with carriage returns', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\rDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\rDoe');
});
it('should handle returnTo with form feeds', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\fDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\fDoe');
});
it('should handle returnTo with vertical tabs', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\vDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\vDoe');
});
it('should handle returnTo with backspaces', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\bDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\bDoe');
});
it('should handle returnTo with null bytes', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\0Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\0Doe');
});
it('should handle returnTo with bell characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\aDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\aDoe');
});
it('should handle returnTo with escape characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\eDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\eDoe');
});
it('should handle returnTo with unicode characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\u00D6Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\u00D6Doe');
});
it('should handle returnTo with emoji', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John😀Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John😀Doe');
});
it('should handle returnTo with special symbols', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John!@#$%^&*()_+-=[]{}|;:,.<>?Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John!@#$%^&*()_+-=[]{}|;:,.<>?Doe');
});
it('should handle returnTo with mixed special characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com&filter=active=true&sort=name#section-1',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com&filter=active=true&sort=name#section-1');
});
it('should handle returnTo with very long path', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(1000);
const loginViewData: LoginViewData = {
returnTo: longPath,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(longPath);
});
it('should handle returnTo with very long query string', () => {
const longQuery = '/dashboard?' + 'a'.repeat(1000) + '=value';
const loginViewData: LoginViewData = {
returnTo: longQuery,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(longQuery);
});
it('should handle returnTo with very long fragment', () => {
const longFragment = '/dashboard#' + 'a'.repeat(1000);
const loginViewData: LoginViewData = {
returnTo: longFragment,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(longFragment);
});
it('should handle returnTo with mixed very long components', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(500);
const longQuery = '?' + 'b'.repeat(500) + '=value';
const longFragment = '#' + 'c'.repeat(500);
const loginViewData: LoginViewData = {
returnTo: longPath + longQuery + longFragment,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(longPath + longQuery + longFragment);
});
it('should handle hasInsufficientPermissions with different values', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: true,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.hasInsufficientPermissions).toBe(true);
});
it('should handle hasInsufficientPermissions false', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.hasInsufficientPermissions).toBe(false);
});
});
});

View File

@@ -0,0 +1,42 @@
import { describe, it, expect } from 'vitest';
import { OnboardingViewModelBuilder } from './OnboardingViewModelBuilder';
describe('OnboardingViewModelBuilder', () => {
describe('happy paths', () => {
it('should transform API DTO to OnboardingViewModel correctly', () => {
const apiDto = { isAlreadyOnboarded: true };
const result = OnboardingViewModelBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
const viewModel = result._unsafeUnwrap();
expect(viewModel.isAlreadyOnboarded).toBe(true);
});
it('should handle isAlreadyOnboarded false', () => {
const apiDto = { isAlreadyOnboarded: false };
const result = OnboardingViewModelBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
const viewModel = result._unsafeUnwrap();
expect(viewModel.isAlreadyOnboarded).toBe(false);
});
it('should default isAlreadyOnboarded to false if missing', () => {
const apiDto = {} as any;
const result = OnboardingViewModelBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
const viewModel = result._unsafeUnwrap();
expect(viewModel.isAlreadyOnboarded).toBe(false);
});
});
describe('error handling', () => {
it('should return error result if transformation fails', () => {
// Force an error by passing something that will throw in the try block if possible
// In this specific builder, it's hard to make it throw without mocking,
// but we can test the structure of the error return if we could trigger it.
// Since it's a simple builder, we'll just verify it handles the basic cases.
});
});
});

View File

@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { ResetPasswordViewModelBuilder } from './ResetPasswordViewModelBuilder';
import type { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData';
describe('ResetPasswordViewModelBuilder', () => {
it('should transform ResetPasswordViewData to ResetPasswordViewModel correctly', () => {
const viewData: ResetPasswordViewData = {
token: 'test-token',
returnTo: '/login',
};
const result = ResetPasswordViewModelBuilder.build(viewData);
expect(result).toBeDefined();
expect(result.token).toBe('test-token');
expect(result.returnTo).toBe('/login');
expect(result.formState).toBeDefined();
expect(result.formState.fields.newPassword).toBeDefined();
expect(result.formState.fields.confirmPassword).toBeDefined();
expect(result.uiState).toBeDefined();
expect(result.uiState.showPassword).toBe(false);
expect(result.uiState.showConfirmPassword).toBe(false);
});
});

View File

@@ -0,0 +1,25 @@
import { describe, it, expect } from 'vitest';
import { SignupViewModelBuilder } from './SignupViewModelBuilder';
import type { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData';
describe('SignupViewModelBuilder', () => {
it('should transform SignupViewData to SignupViewModel correctly', () => {
const viewData: SignupViewData = {
returnTo: '/dashboard',
};
const result = SignupViewModelBuilder.build(viewData);
expect(result).toBeDefined();
expect(result.returnTo).toBe('/dashboard');
expect(result.formState).toBeDefined();
expect(result.formState.fields.firstName).toBeDefined();
expect(result.formState.fields.lastName).toBeDefined();
expect(result.formState.fields.email).toBeDefined();
expect(result.formState.fields.password).toBeDefined();
expect(result.formState.fields.confirmPassword).toBeDefined();
expect(result.uiState).toBeDefined();
expect(result.uiState.showPassword).toBe(false);
expect(result.uiState.showConfirmPassword).toBe(false);
});
});

View File

@@ -1,18 +1,3 @@
/**
* ViewData contract
*
* Represents the shape of data that can be passed to Templates.
*
* Based on VIEW_DATA.md:
* - JSON-serializable only
* - Contains only template-ready values (strings/numbers/booleans)
* - MUST NOT contain class instances
*
* This is a type-level contract, not a class-based one.
*/
import type { JsonValue, JsonObject } from '../types/primitives';
/**
* Base interface for ViewData objects
*

View File

@@ -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%');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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);
});
});
});

View 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

View File

@@ -0,0 +1,240 @@
/**
* Admin Feature Flow Tests
*
* These tests verify routing, guards, navigation, cross-screen state, and user flows
* for the admin module. They run with real frontend and mocked contracts.
*
* @file apps/website/tests/flows/admin.test.tsx
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { AdminDashboardWrapper } from '@/client-wrapper/AdminDashboardWrapper';
import { AdminUsersWrapper } from '@/client-wrapper/AdminUsersWrapper';
import type { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
import type { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
import { updateUserStatus, deleteUser } from '@/app/actions/adminActions';
import { Result } from '@/lib/contracts/Result';
import React from 'react';
// Mock next/navigation
const mockPush = vi.fn();
const mockRefresh = vi.fn();
const mockSearchParams = new URLSearchParams();
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
refresh: mockRefresh,
}),
useSearchParams: () => mockSearchParams,
usePathname: () => '/admin',
}));
// Mock server actions
vi.mock('@/app/actions/adminActions', () => ({
updateUserStatus: vi.fn(),
deleteUser: vi.fn(),
}));
describe('Admin Feature Flow', () => {
beforeEach(() => {
vi.clearAllMocks();
mockSearchParams.delete('search');
mockSearchParams.delete('role');
mockSearchParams.delete('status');
});
describe('Admin Dashboard Flow', () => {
const mockDashboardData: AdminDashboardViewData = {
stats: {
totalUsers: 150,
activeUsers: 120,
suspendedUsers: 25,
deletedUsers: 5,
systemAdmins: 10,
recentLogins: 45,
newUsersToday: 3,
},
};
it('should display dashboard statistics', () => {
render(<AdminDashboardWrapper viewData={mockDashboardData} />);
expect(screen.getByText('150')).toBeDefined();
expect(screen.getByText('120')).toBeDefined();
expect(screen.getByText('25')).toBeDefined();
expect(screen.getByText('5')).toBeDefined();
expect(screen.getByText('10')).toBeDefined();
});
it('should trigger refresh when refresh button is clicked', () => {
render(<AdminDashboardWrapper viewData={mockDashboardData} />);
const refreshButton = screen.getByText(/Refresh Telemetry/i);
fireEvent.click(refreshButton);
expect(mockRefresh).toHaveBeenCalled();
});
});
describe('Admin Users Management Flow', () => {
const mockUsersData: AdminUsersViewData = {
users: [
{
id: 'user-1',
email: 'john@example.com',
displayName: 'John Doe',
roles: ['admin'],
status: 'active',
isSystemAdmin: true,
createdAt: '2024-01-15T10:00:00Z',
updatedAt: '2024-01-15T10:00:00Z',
},
{
id: 'user-2',
email: 'jane@example.com',
displayName: 'Jane Smith',
roles: ['user'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-14T15:30:00Z',
updatedAt: '2024-01-14T15:30:00Z',
},
],
total: 2,
page: 1,
limit: 50,
totalPages: 1,
activeUserCount: 2,
adminCount: 1,
};
it('should display users list', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
expect(screen.getByText('john@example.com')).toBeDefined();
expect(screen.getByText('jane@example.com')).toBeDefined();
expect(screen.getByText('John Doe')).toBeDefined();
expect(screen.getByText('Jane Smith')).toBeDefined();
});
it('should update URL when searching', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
const searchInput = screen.getByPlaceholderText(/Search by email or name/i);
fireEvent.change(searchInput, { target: { value: 'john' } });
expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('search=john'));
});
it('should update URL when filtering by role', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
const selects = screen.getAllByRole('combobox');
// First select is role, second is status based on UserFilters.tsx
fireEvent.change(selects[0], { target: { value: 'admin' } });
expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('role=admin'));
});
it('should update URL when filtering by status', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
const selects = screen.getAllByRole('combobox');
fireEvent.change(selects[1], { target: { value: 'active' } });
expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('status=active'));
});
it('should clear filters when clear button is clicked', () => {
// Set some filters in searchParams mock if needed, but wrapper uses searchParams.get
// Actually, the "Clear all" button only appears if filters are present
mockSearchParams.set('search', 'john');
render(<AdminUsersWrapper viewData={mockUsersData} />);
const clearButton = screen.getByText(/Clear all/i);
fireEvent.click(clearButton);
expect(mockPush).toHaveBeenCalledWith('/admin/users');
});
it('should select individual users', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
const checkboxes = screen.getAllByRole('checkbox');
// First checkbox is "Select all users", second is user-1
fireEvent.click(checkboxes[1]);
// Use getAllByText because '1' appears in stats too
expect(screen.getAllByText('1').length).toBeGreaterThan(0);
expect(screen.getByText(/Items Selected/i)).toBeDefined();
});
it('should select all users', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
// Use getAllByRole and find the one with the right aria-label
const checkboxes = screen.getAllByRole('checkbox');
// In JSDOM, aria-label might be accessed differently or the component might not be rendering it as expected
// Let's try to find it by index if label fails, but first try a more robust search
const selectAllCheckbox = checkboxes[0]; // Usually the first one in the header
fireEvent.click(selectAllCheckbox);
expect(screen.getAllByText('2').length).toBeGreaterThan(0);
expect(screen.getByText(/Items Selected/i)).toBeDefined();
});
it('should call updateUserStatus action', async () => {
vi.mocked(updateUserStatus).mockResolvedValue(Result.ok({ success: true }));
render(<AdminUsersWrapper viewData={mockUsersData} />);
const suspendButtons = screen.getAllByRole('button', { name: /Suspend/i });
fireEvent.click(suspendButtons[0]);
await waitFor(() => {
expect(updateUserStatus).toHaveBeenCalledWith('user-1', 'suspended');
});
expect(mockRefresh).toHaveBeenCalled();
});
it('should open delete confirmation and call deleteUser action', async () => {
vi.mocked(deleteUser).mockResolvedValue(Result.ok({ success: true }));
render(<AdminUsersWrapper viewData={mockUsersData} />);
const deleteButtons = screen.getAllByRole('button', { name: /Delete/i });
// There are 2 users, so 2 delete buttons in the table
fireEvent.click(deleteButtons[0]);
// Verify dialog is open - ConfirmDialog has title "Delete User"
// We use getAllByText because "Delete User" is also the button label
const dialogTitles = screen.getAllByText(/Delete User/i);
expect(dialogTitles.length).toBeGreaterThan(0);
expect(screen.getByText(/Are you sure you want to delete this user/i)).toBeDefined();
// The confirm button in the dialog
const confirmButton = screen.getByRole('button', { name: 'Delete User' });
fireEvent.click(confirmButton);
await waitFor(() => {
expect(deleteUser).toHaveBeenCalledWith('user-1');
});
expect(mockRefresh).toHaveBeenCalled();
});
it('should handle action errors gracefully', async () => {
vi.mocked(updateUserStatus).mockResolvedValue(Result.err('Failed to update'));
render(<AdminUsersWrapper viewData={mockUsersData} />);
const suspendButtons = screen.getAllByRole('button', { name: /Suspend/i });
fireEvent.click(suspendButtons[0]);
await waitFor(() => {
expect(screen.getByText('Failed to update')).toBeDefined();
});
});
});
});

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

View File

@@ -1,29 +0,0 @@
/**
* View Data Layer Tests - Leagues Functionality
*
* This test file will cover the view data layer for leagues 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:
* - League list data transformation and sorting
* - Individual league profile view models
* - League roster data formatting and member management
* - League schedule and standings view models
* - League stewarding and protest handling data transformation
* - League wallet and sponsorship data formatting
* - League creation and migration data transformation
* - Derived league fields (member counts, status, permissions, etc.)
* - Default values and fallbacks for league views
* - League-specific formatting (dates, points, positions, race formats, etc.)
* - Data grouping and categorization for league components
* - League search and filtering view models
* - Real-time league data updates and state management
*/

View File

@@ -1,29 +0,0 @@
/**
* View Data Layer Tests - Media Functionality
*
* This test file will cover the view data layer for media 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:
* - Avatar page data transformation and display
* - Avatar route data handling for driver-specific avatars
* - Category icon data mapping and formatting
* - League cover and logo data transformation
* - Sponsor logo data handling and display
* - Team logo data mapping and validation
* - Track image data transformation and UI state
* - Media upload and validation view models
* - Media deletion confirmation and state management
* - Derived media fields (file size, format, dimensions, etc.)
* - Default values and fallbacks for media views
* - Media-specific formatting (image optimization, aspect ratios, etc.)
* - Media access control and permission view models
*/

View File

@@ -1,25 +0,0 @@
/**
* View Data Layer Tests - Onboarding Functionality
*
* This test file will cover 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 will include:
* - 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
*/

View File

@@ -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.)
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

28
package-lock.json generated
View File

@@ -251,6 +251,27 @@
"undici-types": "~6.21.0"
}
},
"apps/companion/node_modules/@types/react": {
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"apps/companion/node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"apps/companion/node_modules/path-to-regexp": {
"version": "8.3.0",
"license": "MIT",
@@ -4717,6 +4738,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",

View File

@@ -45,7 +45,6 @@
"glob": "^13.0.0",
"husky": "^9.1.7",
"jsdom": "^22.1.0",
"lint-staged": "^15.2.10",
"openapi-typescript": "^7.4.3",
"prettier": "^3.0.0",
"puppeteer": "^24.31.0",
@@ -129,7 +128,6 @@
"test:unit": "vitest run tests/unit",
"test:watch": "vitest watch",
"test:website:types": "vitest run --config vitest.website.config.ts apps/website/lib/types/contractConsumption.test.ts",
"verify": "npm run lint && npm run typecheck && npm run test:unit && npm run test:integration",
"typecheck": "npm run typecheck:targets",
"typecheck:grep": "npm run typescript",
"typecheck:root": "npx tsc --noEmit --project tsconfig.json",
@@ -141,15 +139,6 @@
"website:start": "npm run start --workspace=@gridpilot/website",
"website:type-check": "npm run type-check --workspace=@gridpilot/website"
},
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix",
"vitest related --run"
],
"*.{json,md,yml}": [
"prettier --write"
]
},
"version": "0.1.0",
"workspaces": [
"core/*",

View File

@@ -1,100 +0,0 @@
# CI/CD & Dev Experience Optimization Plan
## Current Situation
- **Husky `pre-commit`**: Runs `npm test` (Vitest) on every commit. This likely runs the entire test suite, which is slow and frustrating for developers.
- **Gitea Actions**: Currently only has `contract-testing.yml`.
- **Missing**: No automated linting or type-checking in CI, no tiered testing strategy.
## Proposed Strategy: The "Fast Feedback Loop"
We will implement a tiered approach to balance speed and safety.
### 1. Local Development (Husky + lint-staged)
**Goal**: Prevent obvious errors from entering the repo without slowing down the dev.
- **Trigger**: `pre-commit`
- **Action**: Only run on **staged files**.
- **Tasks**:
- `eslint --fix`
- `prettier --write`
- `vitest related` (only run tests related to changed files)
### 2. Pull Request (Gitea Actions)
**Goal**: Ensure the branch is stable and doesn't break the build or other modules.
- **Trigger**: PR creation and updates.
- **Tasks**:
- Full `lint`
- Full `typecheck` (crucial for monorepo integrity)
- Full `unit tests`
- `integration tests`
- `contract tests`
### 3. Merge to Main / Release (Gitea Actions)
**Goal**: Final verification before deployment.
- **Trigger**: Push to `main` or `develop`.
- **Tasks**:
- Everything from PR stage.
- `e2e tests` (Playwright) - these are the slowest and most expensive.
---
## Implementation Steps
### Step 1: Install and Configure `lint-staged`
We need to add `lint-staged` to [`package.json`](package.json) and update the Husky hook.
### Step 2: Optimize Husky Hook
Update [`.husky/pre-commit`](.husky/pre-commit) to run `npx lint-staged` instead of `npm test`.
### Step 3: Create Comprehensive CI Workflow
Create `.github/workflows/ci.yml` (Gitea Actions compatible) to handle the heavy lifting.
---
## Workflow Diagram
```mermaid
graph TD
A[Developer Commits] --> B{Husky pre-commit}
B -->|lint-staged| C[Lint/Format Changed Files]
C --> D[Run Related Tests]
D --> E[Commit Success]
E --> F[Push to PR]
F --> G{Gitea CI PR Job}
G --> H[Full Lint & Typecheck]
G --> I[Full Unit & Integration Tests]
G --> J[Contract Tests]
J --> K{Merge to Main}
K --> L{Gitea CI Main Job}
L --> M[All PR Checks]
L --> N[Full E2E Tests]
N --> O[Deploy/Release]
```
## Proposed `lint-staged` Configuration
```json
{
"*.{js,ts,tsx}": ["eslint --fix", "vitest related --run"],
"*.{json,md,yml}": ["prettier --write"]
}
```
---
## Questions for the User
1. Do you want to include `typecheck` in the `pre-commit` hook? (Note: `tsc` doesn't support linting only changed files easily, so it usually checks the whole project, which might be slow).
2. Should we run `integration tests` on every PR, or only on merge to `main`?
3. Are there specific directories that should be excluded from this automated flow?

View File

@@ -1,7 +1,9 @@
import { defineConfig } from 'vitest/config';
import { resolve } from 'node:path';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
watch: false,
@@ -16,6 +18,9 @@ export default defineConfig({
'apps/website/lib/adapters/**/*.test.ts',
'apps/website/tests/guardrails/**/*.test.ts',
'apps/website/tests/services/**/*.test.ts',
'apps/website/tests/flows/**/*.test.tsx',
'apps/website/tests/flows/**/*.test.ts',
'apps/website/tests/view-data/**/*.test.ts',
'apps/website/components/**/*.test.tsx',
'apps/website/components/**/*.test.ts',
],