Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ coverage:
status:
project:
default:
target: 59%
target: 50%
threshold: 1%
94 changes: 94 additions & 0 deletions integration_test/cores_integration_live_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc_app_template/main.dart' as app;
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

setUp(() {});

testWidgets('cores test', (
WidgetTester tester,
) async {
app.main([]);
await tester.pumpAndSettle();

expect(find.byKey(const Key('cores_screen')), findsOneWidget);

final coresTab = find.byKey(const Key('cores_screen'));
await tester.ensureVisible(coresTab);
await tester.tap(coresTab);
await tester.pumpAndSettle();

final firstItem = find.byKey(const Key('Merlin1A0'));

expect(firstItem, findsOneWidget);
await tester.ensureVisible(firstItem);

final allFilterChip = find.byKey(const Key('core_status_filter_all'));
await tester.ensureVisible(allFilterChip);

await tester.tap(allFilterChip);
await tester.pumpAndSettle();

expect(find.text('Merlin1A'), findsOneWidget);

final activeFilterChip = find.byKey(const Key('core_status_filter_active'));
await tester.ensureVisible(activeFilterChip);

await tester.tap(activeFilterChip);
await tester.pumpAndSettle();

expect(find.text('active'), findsAtLeast(1));

final lostFilterChip = find.byKey(const Key('core_status_filter_lost'));
await tester.ensureVisible(lostFilterChip);

await tester.tap(lostFilterChip);
await tester.pumpAndSettle();

expect(find.text('lost'), findsAtLeast(1));

final inactiveFilterChip = find.byKey(
const Key('core_status_filter_inactive'),
);
await tester.ensureVisible(inactiveFilterChip);

await tester.tap(inactiveFilterChip);
await tester.pumpAndSettle();

expect(find.text('inactive'), findsAtLeast(1));

final unknownFilterChip = find.byKey(
const Key('core_status_filter_unknown'),
);
await tester.ensureVisible(unknownFilterChip);

await tester.tap(unknownFilterChip);
await tester.pumpAndSettle();

expect(find.text('unknown'), findsAtLeast(1));

final searchField = find.byType(TextField);
expect(searchField, findsOneWidget);

await tester.enterText(searchField, 'flutter');
await tester.pumpAndSettle();

expect(find.text('No cores found for "flutter"'), findsOneWidget);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use localized string instead of hard-coded English

Avoid locale-coupled flakiness; assert using l10n.

Apply:

+import 'package:flutter_bloc_app_template/generated/l10n.dart';
@@
-    expect(find.text('No cores found for "flutter"'), findsOneWidget);
+    final ctx = tester.element(find.byType(Scaffold).first);
+    final l10n = S.of(ctx);
+    expect(find.text(l10n.noCoresFound('flutter')), findsOneWidget);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(find.text('No cores found for "flutter"'), findsOneWidget);
// At the top of integration_test/cores_integration_live_test.dart
import 'package:flutter_bloc_app_template/generated/l10n.dart';
[...]
// In your test, replace the hard-coded expect at line 79:
final ctx = tester.element(find.byType(Scaffold).first);
final l10n = S.of(ctx);
expect(find.text(l10n.noCoresFound('flutter')), findsOneWidget);
🤖 Prompt for AI Agents
In integration_test/cores_integration_live_test.dart around line 79, the test
asserts a hard-coded English message; replace that with the app's localized
string to avoid locale-dependent flakiness by retrieving the localization from
the test widget tree (e.g. obtain AppLocalizations via
AppLocalizations.of(tester.element(find.byType(<root app widget>))) or similar)
and use the localized formatter/getter for the "No cores found for {query}"
message with the query 'flutter' when calling expect.


await tester.tap(allFilterChip);
await tester.pumpAndSettle();

await tester.enterText(searchField, 'Merlin');
await tester.pumpAndSettle();

expect(find.text('Merlin'), findsAtLeast(1));

// await tester.tap(find.byType(CoreItemWidget).first);
// await tester.pumpAndSettle();
// todo replace with details screen text
// expect(find.text('Merlin1A'), findsOneWidget);
});
}
120 changes: 120 additions & 0 deletions integration_test/cores_screen_integration_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc_app_template/features/cores/bloc/cores_bloc.dart';
import 'package:flutter_bloc_app_template/features/cores/cores_screen.dart';
import 'package:flutter_bloc_app_template/features/cores/widget/core_loading_content.dart';
import 'package:flutter_bloc_app_template/features/cores/widget/cores_empty_widget.dart';
import 'package:flutter_bloc_app_template/features/cores/widget/cores_error_widget.dart';
import 'package:flutter_bloc_app_template/features/cores/widget/cores_not_found_widget.dart';
import 'package:flutter_bloc_app_template/models/core/core_resource.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:mocktail/mocktail.dart';

import '../test/bloc/utils.dart';

class MockCoresBloc extends MockBloc<CoresEvent, CoresState>
implements CoresBloc {}

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

late MockCoresBloc mockBloc;

setUp(() {
mockBloc = MockCoresBloc();
});

testWidgets('renders LoadingContent when state is CoresLoadingState',
(tester) async {
when(() => mockBloc.state).thenReturn(const CoresLoadingState());

await tester.pumpLocalizedWidgetWithBloc<CoresBloc>(
bloc: mockBloc,
child: const CustomScrollView(
slivers: [CoresBlocContent()],
),
locale: const Locale('en'),
);
await tester.pump();

expect(find.byType(CoreLoadingContent), findsOneWidget);
});

testWidgets('renders CoresListWidget when state is CoresSuccessState',
(tester) async {
final cores = <CoreResource>[
const CoreResource(
coreSerial: 'B1051',
missions: [MissionResource(name: null, flight: 1)],
status: 'active',
),
]; // provide mock cores
when(() => mockBloc.state)
.thenReturn(CoresSuccessState(filteredCores: cores));

await tester.pumpLocalizedWidgetWithBloc<CoresBloc>(
bloc: mockBloc,
child: const CustomScrollView(
slivers: [CoresBlocContent()],
),
locale: const Locale('en'),
);
await tester.pumpAndSettle();

expect(find.byType(CoresListWidget), findsOneWidget);
});

testWidgets('renders CoresErrorWidget when state is CoresErrorState',
(tester) async {
const errorMessage = 'Error occurred';
when(() => mockBloc.state).thenReturn(const CoresErrorState(errorMessage));

await tester.pumpLocalizedWidgetWithBloc<CoresBloc>(
bloc: mockBloc,
child: const CustomScrollView(
slivers: [CoresBlocContent()],
),
locale: const Locale('en'),
);
await tester.pumpAndSettle();

expect(find.byType(CoresErrorWidget), findsOneWidget);
//expect(find.text(errorMessage), findsOneWidget);
});

testWidgets('renders CoresEmptyWidget when state is CoresEmptyState',
(tester) async {
when(() => mockBloc.state).thenReturn(const CoresEmptyState());

await tester.pumpLocalizedWidgetWithBloc<CoresBloc>(
bloc: mockBloc,
child: const CustomScrollView(
slivers: [CoresBlocContent()],
),
locale: const Locale('en'),
);
await tester.pumpAndSettle();

expect(find.byType(CoresEmptyWidget), findsOneWidget);
});

testWidgets('renders CoresNotFoundWidget when state is CoresNotFoundState',
(tester) async {
const query = 'Falcon';
when(() => mockBloc.state)
.thenReturn(const CoresNotFoundState(searchQuery: query));

await tester.pumpLocalizedWidgetWithBloc<CoresBloc>(
bloc: mockBloc,
child: const CustomScrollView(
slivers: [CoresBlocContent()],
),
locale: const Locale('en'),
);
await tester.pumpAndSettle();

expect(find.byType(CoresNotFoundWidget), findsOneWidget);
expect(find.textContaining(query), findsOneWidget);
});
}
48 changes: 48 additions & 0 deletions lib/data/network/data_source/cores_network_data_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:flutter_bloc_app_template/data/network/api_result.dart';
import 'package:flutter_bloc_app_template/data/network/model/core/network_core_model.dart';
import 'package:flutter_bloc_app_template/data/network/service/cores/cores_service.dart';

abstract class CoresDataSource {
Future<ApiResult<List<NetworkCoreModel>>> getCores({
bool? hasId = true,
int? limit,
int? offset,
});

Future<ApiResult<NetworkCoreModel>> getCore(String coreSerial);
}

class CoresNetworkDataSource implements CoresDataSource {
CoresNetworkDataSource(this._service);

final CoresService _service;

@override
Future<ApiResult<List<NetworkCoreModel>>> getCores({
bool? hasId = true,
int? limit,
int? offset,
}) async {
try {
final list = await _service.fetchCores(
hasId: hasId,
limit: limit,
offset: offset,
);

return ApiResult.success(list);
} catch (e) {
return Future.value(ApiResult.error(e.toString()));
}
}

@override
Future<ApiResult<NetworkCoreModel>> getCore(String coreSerial) async {
try {
final result = await _service.fetchCore(coreSerial);
return ApiResult.success(result);
} catch (e) {
return Future.value(ApiResult.error(e.toString()));
}
}
}
32 changes: 24 additions & 8 deletions lib/data/network/model/core/network_core_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,35 @@ part 'network_core_model.g.dart';
abstract class NetworkCoreModel with _$NetworkCoreModel {
const factory NetworkCoreModel({
@JsonKey(name: 'core_serial') String? coreSerial,
int? flight,
int? block,
bool? gridfins,
bool? legs,
bool? reused,
@JsonKey(name: 'land_success') bool? landSuccess,
@JsonKey(name: 'landing_intent') bool? landingIntent,
@JsonKey(name: 'landing_type') String? landingType,
@JsonKey(name: 'landing_vehicle') String? landingVehicle,
String? status,
@JsonKey(name: 'original_launch') String? originalLaunch,
@JsonKey(name: 'original_launch_unix') int? originalLaunchUnix,
List<NetworkMission>? missions,
@JsonKey(name: 'reuse_count') int? reuseCount,
@JsonKey(name: 'rtls_attempts') int? rtlsAttempts,
@JsonKey(name: 'rtls_landings') int? rtlsLandings,
@JsonKey(name: 'asds_attempts') int? asdsAttempts,
@JsonKey(name: 'asds_landings') int? asdsLandings,
@JsonKey(name: 'water_landing') bool? waterLanding,
String? details,
}) = _NetworkCoreModel;

const NetworkCoreModel._();

factory NetworkCoreModel.fromJson(Map<String, dynamic> json) =>
_$NetworkCoreModelFromJson(json);
}

@freezed
abstract class NetworkMission with _$NetworkMission {
const factory NetworkMission({
String? name,
int? flight,
}) = _NetworkMission;

const NetworkMission._();

factory NetworkMission.fromJson(Map<String, dynamic> json) =>
_$NetworkMissionFromJson(json);
}
Loading