This commit is contained in:
2025-12-21 19:53:22 +01:00
parent f2d8a23583
commit 3c64f328e2
105 changed files with 3191 additions and 1706 deletions

View File

@@ -5,6 +5,8 @@ import { AnalyticsService } from './AnalyticsService';
import type { Response } from 'express';
import { EntityType, VisitorType } from '@core/analytics/domain/types/PageView';
import { EngagementAction, EngagementEntityType } from '@core/analytics/domain/types/EngagementEvent';
import type { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO';
import type { RecordPageViewOutputDTO } from './dtos/RecordPageViewOutputDTO';
describe('AnalyticsController', () => {
let controller: AnalyticsController;
@@ -42,8 +44,8 @@ describe('AnalyticsController', () => {
userAgent: 'Mozilla/5.0',
country: 'US',
};
const presenterMock = { viewModel: { pageViewId: 'pv-123' } };
service.recordPageView.mockResolvedValue(presenterMock as any);
const dto: RecordPageViewOutputDTO = { pageViewId: 'pv-123' };
service.recordPageView.mockResolvedValue(dto);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
@@ -54,7 +56,7 @@ describe('AnalyticsController', () => {
expect(service.recordPageView).toHaveBeenCalledWith(input);
expect(mockRes.status).toHaveBeenCalledWith(201);
expect(mockRes.json).toHaveBeenCalledWith(presenterMock.viewModel);
expect(mockRes.json).toHaveBeenCalledWith(dto);
});
});
@@ -69,8 +71,8 @@ describe('AnalyticsController', () => {
actorId: 'actor-789',
metadata: { key: 'value' },
};
const presenterMock = { eventId: 'event-123', engagementWeight: 10 };
service.recordEngagement.mockResolvedValue(presenterMock as any);
const dto: RecordEngagementOutputDTO = { eventId: 'event-123', engagementWeight: 10 };
service.recordEngagement.mockResolvedValue(dto);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
@@ -81,45 +83,41 @@ describe('AnalyticsController', () => {
expect(service.recordEngagement).toHaveBeenCalledWith(input);
expect(mockRes.status).toHaveBeenCalledWith(201);
expect(mockRes.json).toHaveBeenCalledWith((presenterMock as any).viewModel);
expect(mockRes.json).toHaveBeenCalledWith(dto);
});
});
describe('getDashboardData', () => {
it('should return dashboard data', async () => {
const presenterMock = {
viewModel: {
totalUsers: 100,
activeUsers: 50,
totalRaces: 20,
totalLeagues: 5,
},
const dto = {
totalUsers: 100,
activeUsers: 50,
totalRaces: 20,
totalLeagues: 5,
};
service.getDashboardData.mockResolvedValue(presenterMock as any);
service.getDashboardData.mockResolvedValue(dto);
const result = await controller.getDashboardData();
expect(service.getDashboardData).toHaveBeenCalled();
expect(result).toEqual(presenterMock.viewModel);
expect(result).toEqual(dto);
});
});
describe('getAnalyticsMetrics', () => {
it('should return analytics metrics', async () => {
const presenterMock = {
viewModel: {
pageViews: 1000,
uniqueVisitors: 500,
averageSessionDuration: 300,
bounceRate: 0.4,
},
const dto = {
pageViews: 1000,
uniqueVisitors: 500,
averageSessionDuration: 300,
bounceRate: 0.4,
};
service.getAnalyticsMetrics.mockResolvedValue(presenterMock as any);
service.getAnalyticsMetrics.mockResolvedValue(dto);
const result = await controller.getAnalyticsMetrics();
expect(service.getAnalyticsMetrics).toHaveBeenCalled();
expect(result).toEqual(presenterMock.viewModel);
expect(result).toEqual(dto);
});
});
});

View File

@@ -27,8 +27,8 @@ export class AnalyticsController {
@Body() input: RecordPageViewInput,
@Res() res: Response,
): Promise<void> {
const presenter = await this.analyticsService.recordPageView(input);
res.status(HttpStatus.CREATED).json(presenter.viewModel);
const dto = await this.analyticsService.recordPageView(input);
res.status(HttpStatus.CREATED).json(dto);
}
@Post('engagement')
@@ -39,23 +39,21 @@ export class AnalyticsController {
@Body() input: RecordEngagementInput,
@Res() res: Response,
): Promise<void> {
const presenter = await this.analyticsService.recordEngagement(input);
res.status(HttpStatus.CREATED).json(presenter.viewModel);
const dto = await this.analyticsService.recordEngagement(input);
res.status(HttpStatus.CREATED).json(dto);
}
@Get('dashboard')
@ApiOperation({ summary: 'Get analytics dashboard data' })
@ApiResponse({ status: 200, description: 'Dashboard data', type: GetDashboardDataOutputDTO })
async getDashboardData(): Promise<GetDashboardDataOutputDTO> {
const presenter = await this.analyticsService.getDashboardData();
return presenter.viewModel;
return this.analyticsService.getDashboardData();
}
@Get('metrics')
@ApiOperation({ summary: 'Get analytics metrics' })
@ApiResponse({ status: 200, description: 'Analytics metrics', type: GetAnalyticsMetricsOutputDTO })
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutputDTO> {
const presenter = await this.analyticsService.getAnalyticsMetrics();
return presenter.viewModel;
return this.analyticsService.getAnalyticsMetrics();
}
}

View File

@@ -1,27 +1,40 @@
import { Provider } from '@nestjs/common';
import { AnalyticsService } from './AnalyticsService';
import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import { GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import type { IPageViewRepository } from '@core/analytics/domain/repositories/IPageViewRepository';
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
import type { Logger } from '@core/shared/application';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { RecordPageViewOutput } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import type { RecordEngagementOutput } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import type { GetDashboardDataOutput } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import type { GetAnalyticsMetricsOutput } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
const Logger_TOKEN = 'Logger_TOKEN';
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
const RECORD_PAGE_VIEW_USE_CASE_TOKEN = 'RecordPageViewUseCase_TOKEN';
const RECORD_ENGAGEMENT_USE_CASE_TOKEN = 'RecordEngagementUseCase_TOKEN';
const GET_DASHBOARD_DATA_USE_CASE_TOKEN = 'GetDashboardDataUseCase_TOKEN';
const GET_ANALYTICS_METRICS_USE_CASE_TOKEN = 'GetAnalyticsMetricsUseCase_TOKEN';
const RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN = 'RecordPageViewOutputPort_TOKEN';
const RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN = 'RecordEngagementOutputPort_TOKEN';
const GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN = 'GetDashboardDataOutputPort_TOKEN';
const GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN = 'GetAnalyticsMetricsOutputPort_TOKEN';
import { InMemoryEngagementRepository } from '@adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
import { InMemoryPageViewRepository } from '@adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import { GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import { RecordPageViewPresenter } from './presenters/RecordPageViewPresenter';
import { RecordEngagementPresenter } from './presenters/RecordEngagementPresenter';
import { GetDashboardDataPresenter } from './presenters/GetDashboardDataPresenter';
import { GetAnalyticsMetricsPresenter } from './presenters/GetAnalyticsMetricsPresenter';
export const AnalyticsProviders: Provider[] = [
AnalyticsService,
RecordPageViewPresenter,
RecordEngagementPresenter,
GetDashboardDataPresenter,
GetAnalyticsMetricsPresenter,
{
provide: Logger_TOKEN,
useClass: ConsoleLogger,
@@ -35,23 +48,43 @@ export const AnalyticsProviders: Provider[] = [
useClass: InMemoryEngagementRepository,
},
{
provide: RECORD_PAGE_VIEW_USE_CASE_TOKEN,
useFactory: (repo: IPageViewRepository, logger: Logger) => new RecordPageViewUseCase(repo, logger),
inject: [IPAGE_VIEW_REPO_TOKEN, Logger_TOKEN],
provide: RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN,
useExisting: RecordPageViewPresenter,
},
{
provide: RECORD_ENGAGEMENT_USE_CASE_TOKEN,
useFactory: (repo: IEngagementRepository, logger: Logger) => new RecordEngagementUseCase(repo, logger),
inject: [IENGAGEMENT_REPO_TOKEN, Logger_TOKEN],
provide: RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN,
useExisting: RecordEngagementPresenter,
},
{
provide: GET_DASHBOARD_DATA_USE_CASE_TOKEN,
useFactory: (logger: Logger) => new GetDashboardDataUseCase(logger),
inject: [Logger_TOKEN],
provide: GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN,
useExisting: GetDashboardDataPresenter,
},
{
provide: GET_ANALYTICS_METRICS_USE_CASE_TOKEN,
useFactory: (repo: IPageViewRepository, logger: Logger) => new GetAnalyticsMetricsUseCase(repo, logger),
inject: [IPAGE_VIEW_REPO_TOKEN, Logger_TOKEN],
provide: GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN,
useExisting: GetAnalyticsMetricsPresenter,
},
{
provide: RecordPageViewUseCase,
useFactory: (repo: IPageViewRepository, logger: Logger, output: UseCaseOutputPort<RecordPageViewOutput>) =>
new RecordPageViewUseCase(repo, logger, output),
inject: [IPAGE_VIEW_REPO_TOKEN, Logger_TOKEN, RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN],
},
{
provide: RecordEngagementUseCase,
useFactory: (repo: IEngagementRepository, logger: Logger, output: UseCaseOutputPort<RecordEngagementOutput>) =>
new RecordEngagementUseCase(repo, logger, output),
inject: [IENGAGEMENT_REPO_TOKEN, Logger_TOKEN, RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN],
},
{
provide: GetDashboardDataUseCase,
useFactory: (logger: Logger, output: UseCaseOutputPort<GetDashboardDataOutput>) =>
new GetDashboardDataUseCase(logger, output),
inject: [Logger_TOKEN, GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN],
},
{
provide: GetAnalyticsMetricsUseCase,
useFactory: (repo: IPageViewRepository, logger: Logger, output: UseCaseOutputPort<GetAnalyticsMetricsOutput>) =>
new GetAnalyticsMetricsUseCase(repo, logger, output),
inject: [IPAGE_VIEW_REPO_TOKEN, Logger_TOKEN, GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN],
},
];

View File

@@ -17,41 +17,52 @@ import { GetAnalyticsMetricsPresenter } from './presenters/GetAnalyticsMetricsPr
type RecordPageViewInput = RecordPageViewInputDTO;
type RecordEngagementInput = RecordEngagementInputDTO;
const RECORD_PAGE_VIEW_USE_CASE_TOKEN = 'RecordPageViewUseCase_TOKEN';
const RECORD_ENGAGEMENT_USE_CASE_TOKEN = 'RecordEngagementUseCase_TOKEN';
const GET_DASHBOARD_DATA_USE_CASE_TOKEN = 'GetDashboardDataUseCase_TOKEN';
const GET_ANALYTICS_METRICS_USE_CASE_TOKEN = 'GetAnalyticsMetricsUseCase_TOKEN';
@Injectable()
export class AnalyticsService {
constructor(
@Inject(RECORD_PAGE_VIEW_USE_CASE_TOKEN) private readonly recordPageViewUseCase: RecordPageViewUseCase,
@Inject(RECORD_ENGAGEMENT_USE_CASE_TOKEN) private readonly recordEngagementUseCase: RecordEngagementUseCase,
@Inject(GET_DASHBOARD_DATA_USE_CASE_TOKEN) private readonly getDashboardDataUseCase: GetDashboardDataUseCase,
@Inject(GET_ANALYTICS_METRICS_USE_CASE_TOKEN) private readonly getAnalyticsMetricsUseCase: GetAnalyticsMetricsUseCase,
@Inject(RecordPageViewUseCase) private readonly recordPageViewUseCase: RecordPageViewUseCase,
@Inject(RecordEngagementUseCase) private readonly recordEngagementUseCase: RecordEngagementUseCase,
@Inject(GetDashboardDataUseCase) private readonly getDashboardDataUseCase: GetDashboardDataUseCase,
@Inject(GetAnalyticsMetricsUseCase) private readonly getAnalyticsMetricsUseCase: GetAnalyticsMetricsUseCase,
private readonly recordPageViewPresenter: RecordPageViewPresenter,
private readonly recordEngagementPresenter: RecordEngagementPresenter,
private readonly getDashboardDataPresenter: GetDashboardDataPresenter,
private readonly getAnalyticsMetricsPresenter: GetAnalyticsMetricsPresenter,
) {}
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewPresenter> {
const presenter = new RecordPageViewPresenter();
await this.recordPageViewUseCase.execute(input, presenter);
return presenter;
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutputDTO> {
const result = await this.recordPageViewUseCase.execute(input);
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to record page view');
}
return this.recordPageViewPresenter.getResponseModel();
}
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementPresenter> {
const presenter = new RecordEngagementPresenter();
await this.recordEngagementUseCase.execute(input, presenter);
return presenter;
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutputDTO> {
const result = await this.recordEngagementUseCase.execute(input);
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to record engagement');
}
return this.recordEngagementPresenter.getResponseModel();
}
async getDashboardData(): Promise<GetDashboardDataPresenter> {
const presenter = new GetDashboardDataPresenter();
await this.getDashboardDataUseCase.execute(undefined, presenter);
return presenter;
async getDashboardData(): Promise<GetDashboardDataOutputDTO> {
const result = await this.getDashboardDataUseCase.execute({});
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get dashboard data');
}
return this.getDashboardDataPresenter.getResponseModel();
}
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsPresenter> {
const presenter = new GetAnalyticsMetricsPresenter();
await this.getAnalyticsMetricsUseCase.execute(undefined, presenter);
return presenter;
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutputDTO> {
const result = await this.getAnalyticsMetricsUseCase.execute({});
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get analytics metrics');
}
return this.getAnalyticsMetricsPresenter.getResponseModel();
}
}

View File

@@ -9,7 +9,7 @@ describe('GetAnalyticsMetricsPresenter', () => {
presenter = new GetAnalyticsMetricsPresenter();
});
it('maps use case output to DTO correctly', () => {
it('maps output to DTO correctly', () => {
const output: GetAnalyticsMetricsOutput = {
pageViews: 1000,
uniqueVisitors: 500,
@@ -19,7 +19,9 @@ describe('GetAnalyticsMetricsPresenter', () => {
presenter.present(output);
expect(presenter.viewModel).toEqual({
const dto = presenter.getResponseModel();
expect(dto).toEqual({
pageViews: 1000,
uniqueVisitors: 500,
averageSessionDuration: 300,
@@ -27,19 +29,7 @@ describe('GetAnalyticsMetricsPresenter', () => {
});
});
it('reset clears state and causes viewModel to throw', () => {
const output: GetAnalyticsMetricsOutput = {
pageViews: 1000,
uniqueVisitors: 500,
averageSessionDuration: 300,
bounceRate: 0.4,
};
presenter.present(output);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
it('getResponseModel throws if not presented', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
});

View File

@@ -1,24 +1,21 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetAnalyticsMetricsOutput } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import type { GetAnalyticsMetricsOutputDTO } from '../dtos/GetAnalyticsMetricsOutputDTO';
export class GetAnalyticsMetricsPresenter {
private result: GetAnalyticsMetricsOutputDTO | null = null;
export class GetAnalyticsMetricsPresenter implements UseCaseOutputPort<GetAnalyticsMetricsOutput> {
private responseModel: GetAnalyticsMetricsOutputDTO | null = null;
reset() {
this.result = null;
}
present(output: GetAnalyticsMetricsOutput): void {
this.result = {
pageViews: output.pageViews,
uniqueVisitors: output.uniqueVisitors,
averageSessionDuration: output.averageSessionDuration,
bounceRate: output.bounceRate,
present(result: GetAnalyticsMetricsOutput): void {
this.responseModel = {
pageViews: result.pageViews,
uniqueVisitors: result.uniqueVisitors,
averageSessionDuration: result.averageSessionDuration,
bounceRate: result.bounceRate,
};
}
get viewModel(): GetAnalyticsMetricsOutputDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
getResponseModel(): GetAnalyticsMetricsOutputDTO {
if (!this.responseModel) throw new Error('Presenter not presented');
return this.responseModel;
}
}

View File

@@ -9,7 +9,7 @@ describe('GetDashboardDataPresenter', () => {
presenter = new GetDashboardDataPresenter();
});
it('maps use case output to DTO correctly', () => {
it('maps output to DTO correctly', () => {
const output: GetDashboardDataOutput = {
totalUsers: 100,
activeUsers: 50,
@@ -19,7 +19,7 @@ describe('GetDashboardDataPresenter', () => {
presenter.present(output);
expect(presenter.viewModel).toEqual({
expect(presenter.getResponseModel()).toEqual({
totalUsers: 100,
activeUsers: 50,
totalRaces: 20,
@@ -27,19 +27,7 @@ describe('GetDashboardDataPresenter', () => {
});
});
it('reset clears state and causes viewModel to throw', () => {
const output: GetDashboardDataOutput = {
totalUsers: 100,
activeUsers: 50,
totalRaces: 20,
totalLeagues: 5,
};
presenter.present(output);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
it('getResponseModel throws if not presented', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
});

View File

@@ -1,24 +1,21 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetDashboardDataOutput } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import type { GetDashboardDataOutputDTO } from '../dtos/GetDashboardDataOutputDTO';
export class GetDashboardDataPresenter {
private result: GetDashboardDataOutputDTO | null = null;
export class GetDashboardDataPresenter implements UseCaseOutputPort<GetDashboardDataOutput> {
private responseModel: GetDashboardDataOutputDTO | null = null;
reset() {
this.result = null;
}
present(output: GetDashboardDataOutput): void {
this.result = {
totalUsers: output.totalUsers,
activeUsers: output.activeUsers,
totalRaces: output.totalRaces,
totalLeagues: output.totalLeagues,
present(result: GetDashboardDataOutput): void {
this.responseModel = {
totalUsers: result.totalUsers,
activeUsers: result.activeUsers,
totalRaces: result.totalRaces,
totalLeagues: result.totalLeagues,
};
}
get viewModel(): GetDashboardDataOutputDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
getResponseModel(): GetDashboardDataOutputDTO {
if (!this.responseModel) throw new Error('Presenter not presented');
return this.responseModel;
}
}

View File

@@ -9,31 +9,21 @@ describe('RecordEngagementPresenter', () => {
presenter = new RecordEngagementPresenter();
});
it('maps use case output to DTO correctly', () => {
it('maps output to DTO correctly', () => {
const output: RecordEngagementOutput = {
eventId: 'event-123',
engagementWeight: 10,
} as RecordEngagementOutput;
};
presenter.present(output);
expect(presenter.viewModel).toEqual({
expect(presenter.getResponseModel()).toEqual({
eventId: 'event-123',
engagementWeight: 10,
});
});
it('reset clears state and causes viewModel to throw', () => {
const output: RecordEngagementOutput = {
eventId: 'event-123',
engagementWeight: 10,
} as RecordEngagementOutput;
presenter.present(output);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
it('getResponseModel throws if not presented', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
});

View File

@@ -1,22 +1,19 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { RecordEngagementOutput } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import type { RecordEngagementOutputDTO } from '../dtos/RecordEngagementOutputDTO';
export class RecordEngagementPresenter {
private result: RecordEngagementOutputDTO | null = null;
export class RecordEngagementPresenter implements UseCaseOutputPort<RecordEngagementOutput> {
private responseModel: RecordEngagementOutputDTO | null = null;
reset() {
this.result = null;
}
present(output: RecordEngagementOutput): void {
this.result = {
eventId: output.eventId,
engagementWeight: output.engagementWeight,
present(result: RecordEngagementOutput): void {
this.responseModel = {
eventId: result.eventId,
engagementWeight: result.engagementWeight,
};
}
get viewModel(): RecordEngagementOutputDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
getResponseModel(): RecordEngagementOutputDTO {
if (!this.responseModel) throw new Error('Presenter not presented');
return this.responseModel;
}
}

View File

@@ -9,28 +9,19 @@ describe('RecordPageViewPresenter', () => {
presenter = new RecordPageViewPresenter();
});
it('maps use case output to DTO correctly', () => {
it('maps output to DTO correctly', () => {
const output: RecordPageViewOutput = {
pageViewId: 'pv-123',
} as RecordPageViewOutput;
};
presenter.present(output);
expect(presenter.viewModel).toEqual({
expect(presenter.getResponseModel()).toEqual({
pageViewId: 'pv-123',
});
});
it('reset clears state and causes viewModel to throw', () => {
const output: RecordPageViewOutput = {
pageViewId: 'pv-123',
} as RecordPageViewOutput;
presenter.present(output);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
it('getResponseModel throws if not presented', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
});

View File

@@ -1,21 +1,18 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { RecordPageViewOutput } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import type { RecordPageViewOutputDTO } from '../dtos/RecordPageViewOutputDTO';
export class RecordPageViewPresenter {
private result: RecordPageViewOutputDTO | null = null;
export class RecordPageViewPresenter implements UseCaseOutputPort<RecordPageViewOutput> {
private responseModel: RecordPageViewOutputDTO | null = null;
reset() {
this.result = null;
}
present(output: RecordPageViewOutput): void {
this.result = {
pageViewId: output.pageViewId,
present(result: RecordPageViewOutput): void {
this.responseModel = {
pageViewId: result.pageViewId,
};
}
get viewModel(): RecordPageViewOutputDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
getResponseModel(): RecordPageViewOutputDTO {
if (!this.responseModel) throw new Error('Presenter not presented');
return this.responseModel;
}
}