Files
tm_app/test/view_models/ai_recommendations_view_model_test.dart
AmirReza Jamali 6cd8d7cc7f
continuous-integration/drone/push Build is passing
Update tenders recommendations view
2026-06-29 13:27:21 +03:30

206 lines
6.4 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tm_app/core/network/network_manager.dart';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/repositories/company_ai_repository.dart';
import 'package:tm_app/data/repositories/home_repository.dart';
import 'package:tm_app/data/services/company_ai_service.dart';
import 'package:tm_app/data/services/home_service.dart';
import 'package:tm_app/data/services/liked_tenders_service.dart';
import 'package:tm_app/data/services/model/recommended_tenders_data/recommended_tenders_data.dart';
import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_tenders_response.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
import 'package:tm_app/data/services/your_tenders_service.dart';
import 'package:tm_app/view_models/ai_recommendations_view_model.dart';
void main() {
group('AiRecommendationsViewModel', () {
test(
'loads recommended tenders from GET /api/v1/tenders/recommend',
() async {
final prefs = await _prefs();
final homeRepository = _FakeHomeRepository(
prefs,
result: Result.ok(
RecommendedTendersResponse(
success: true,
message: 'ok',
error: null,
data: RecommendedTendersData(
tenders: [
_sampleTender(id: 'tender-1', title: 'Bridge repair'),
],
metadata: null,
),
),
),
);
final viewModel = AiRecommendationsViewModel(
companyAiRepository: _FakeCompanyAiRepository(prefs),
homeRepository: homeRepository,
);
await viewModel.loadRecommendations();
expect(viewModel.hasLoaded, isTrue);
expect(viewModel.errorMessage, isNull);
expect(homeRepository.recommendedTendersCalls, 1);
expect(viewModel.recommendedTenders.single.id, 'tender-1');
},
);
test('keeps failed transient loads retryable for loadIfNeeded', () async {
final prefs = await _prefs();
final homeRepository = _FakeHomeRepository(
prefs,
results: [
Result.error(Exception('network failed')),
Result.ok(
RecommendedTendersResponse(
success: true,
message: 'ok',
error: null,
data: RecommendedTendersData(
tenders: [
_sampleTender(id: 'tender-1', title: 'Bridge repair'),
],
metadata: null,
),
),
),
],
);
final viewModel = AiRecommendationsViewModel(
companyAiRepository: _FakeCompanyAiRepository(prefs),
homeRepository: homeRepository,
);
await viewModel.loadRecommendations();
await viewModel.loadIfNeeded();
expect(homeRepository.recommendedTendersCalls, 2);
expect(viewModel.hasLoaded, isTrue);
expect(viewModel.errorMessage, isNull);
expect(viewModel.recommendedTenders.single.id, 'tender-1');
});
test(
'marks empty results as still learning after recent onboarding',
() async {
final prefs = await _prefs();
final viewModel = AiRecommendationsViewModel(
companyAiRepository: _FakeCompanyAiRepository(
prefs,
lastOnboardingStartedAt: DateTime.now().millisecondsSinceEpoch,
),
homeRepository: _FakeHomeRepository(prefs),
);
await viewModel.loadRecommendations();
expect(viewModel.hasLoaded, isTrue);
expect(viewModel.stillLearning, isTrue);
expect(viewModel.isEmpty, isTrue);
},
);
});
}
Future<SharedPreferences> _prefs() async {
SharedPreferences.setMockInitialValues({});
return SharedPreferences.getInstance();
}
class _FakeCompanyAiRepository extends CompanyAiRepository {
_FakeCompanyAiRepository(
SharedPreferences prefs, {
int? lastOnboardingStartedAt,
}) : _lastOnboardingStartedAt = lastOnboardingStartedAt,
super(companyAiService: _UnusedCompanyAiService(prefs), prefs: prefs);
final int? _lastOnboardingStartedAt;
@override
int? lastOnboardingStartedAt() => _lastOnboardingStartedAt;
}
class _FakeHomeRepository extends HomeRepository {
_FakeHomeRepository(
SharedPreferences prefs, {
List<Result<RecommendedTendersResponse>>? results,
Result<RecommendedTendersResponse>? result,
}) : _results =
results ??
[
result ??
const Result.ok(
RecommendedTendersResponse(
success: true,
message: 'ok',
error: null,
data: RecommendedTendersData(tenders: [], metadata: null),
),
),
],
super(
homeService: HomeService(networkManager: NetworkManager(prefs)),
yourTendersService: YourTendersService(
networkManager: NetworkManager(prefs),
),
likedTendersService: LikedTendersService(
networkManager: NetworkManager(prefs),
),
);
final List<Result<RecommendedTendersResponse>> _results;
int recommendedTendersCalls = 0;
@override
Future<Result<RecommendedTendersResponse>> getHomeRecommendedTenders({
required int limit,
required int offset,
}) async {
final index = recommendedTendersCalls;
recommendedTendersCalls += 1;
return _results[index];
}
}
class _UnusedCompanyAiService extends CompanyAiService {
_UnusedCompanyAiService(SharedPreferences prefs)
: super(networkManager: NetworkManager(prefs));
}
TenderData _sampleTender({required String id, required String title}) {
return TenderData(
success: null,
message: null,
id: id,
tenderId: id,
noticePublicationId: null,
noticeTypeCode: null,
formType: null,
noticeLanguageCode: null,
issueDate: null,
issueTime: null,
publicationDate: null,
title: title,
description: null,
procurementTypeCode: null,
procedureCode: null,
estimatedValue: null,
currency: null,
duration: null,
tenderDeadline: null,
submissionDeadline: null,
applicationDeadline: null,
submissionUrl: null,
countryCode: null,
durationUnit: null,
buyerOrganization: null,
lots: null,
officialLanguages: null,
status: null,
);
}