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
56 changes: 56 additions & 0 deletions .github/workflows/flutter-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Flutter CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable

- name: Flutter pub get
run: flutter pub get

- name: Flutter analyze
run: flutter analyze

- name: Flutter test (unit + widget)
run: flutter test

firebase_app_distribution:
needs: build_and_test
if: ${{ secrets.FIREBASE_APP_ID_ANDROID != '' && secrets.FIREBASE_TOKEN != '' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable

- name: Flutter pub get
run: flutter pub get

- name: Build APK
run: flutter build apk

# Terintegrasi langsung dengan FAD
- name: Upload to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID_ANDROID }}
token: ${{ secrets.FIREBASE_TOKEN }}
file: build/app/outputs/apk/debug/app-debug.apk
releaseNotes: "Hello from GitHub Actions! :)"
50 changes: 50 additions & 0 deletions integration_test/app_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Integration test flow:
// 1) Buka Explore screen
// 2) Tap “Open Filters” untuk berpindah halaman FilterScreen.
// 3) Verifikasi
// Connect device/emulator, lalu jalankan `flutter test integration_test`

import 'package:codexia/presentation/providers/auth_provider.dart';
import 'package:codexia/presentation/screens/explore/filter_screen.dart';
import 'package:codexia/presentation/screens/main/explore_screen.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('Explore flow: open filter screen and return', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
// Avoid hitting real Firebase auth; keep auth state empty.
authStateProvider.overrideWith((ref) => const Stream<User?>.empty()),
],
child: const MaterialApp(home: ExploreScreen()),
),
);

// On Explore screen
expect(
find.text('Pick how you want to explore books today.'),
findsOneWidget,
);
expect(find.text('Open Filters'), findsOneWidget);

// Tap Filter option -> navigates to FilterScreen
await tester.tap(find.text('Open Filters'));
await tester.pumpAndSettle();

expect(find.byType(FilterScreen), findsOneWidget);
expect(find.text('Filters'), findsOneWidget);

// Back to Explore
await tester.pageBack();
await tester.pumpAndSettle();

expect(find.byType(ExploreScreen), findsOneWidget);
});
}
31 changes: 28 additions & 3 deletions lib/data/services/api_client.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'dart:developer';

import 'package:dio/dio.dart';

/// Shared HTTP client wrapper to keep Dio config (baseUrl, timeout, interceptors) in one place.
// * Shared HTTP client wrapper to keep Dio config (baseUrl, timeout, interceptors) in one place.
class ApiClient {
ApiClient({Dio? dio})
ApiClient({Dio? dio, bool enableLogging = true})
: _dio =
dio ??
Dio(
Expand All @@ -12,7 +14,30 @@ class ApiClient {
receiveTimeout: const Duration(seconds: 15),
),
) {
// Add logging / interceptors here when needed.
// * Enables logging capability
if (enableLogging) {
_dio.interceptors.add(
InterceptorsWrapper(
onResponse: (resp, handler) {
log(
'API ${resp.requestOptions.method} ${resp.requestOptions.path} -> ${resp.statusCode}',
name: 'ApiClient',
);
handler.next(resp);
},
onError: (err, handler) {
log(
'API ERROR ${err.requestOptions.method} ${err.requestOptions.path} -> ${err.response?.statusCode ?? 'no-status'} ${err.message}',
name: 'ApiClient',
);
if (err.response?.data != null) {
log('Body: ${err.response?.data}', name: 'ApiClient');
}
handler.next(err);
},
),
);
}
}

static const String _baseUrl =
Expand Down
47 changes: 47 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.2"
flutter_driver:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
Expand Down Expand Up @@ -448,6 +453,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
Expand Down Expand Up @@ -512,6 +522,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.7.2"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
io:
dependency: transitive
description:
Expand Down Expand Up @@ -656,6 +671,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
Expand All @@ -680,6 +703,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.3"
process:
dependency: transitive
description:
name: process
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
url: "https://pub.dev"
source: hosted
version: "5.0.5"
pub_semver:
dependency: transitive
description:
Expand Down Expand Up @@ -837,6 +868,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
sync_http:
dependency: transitive
description:
name: sync_http
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
term_glyph:
dependency: transitive
description:
Expand Down Expand Up @@ -1029,6 +1068,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webdriver:
dependency: transitive
description:
name: webdriver
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
webkit_inspection_protocol:
dependency: transitive
description:
Expand Down
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ dev_dependencies:
riverpod_generator: ^3.0.3
build_runner: ^2.10.4
flutter_launcher_icons: ^0.14.4
integration_test:
sdk: flutter

flutter:
uses-material-design: true
Expand Down
43 changes: 43 additions & 0 deletions test/data/models/book_model_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// * Unit Testing 1: Book Model Mapping Test
import 'package:codexia/data/models/book_model.dart';
import 'package:flutter_test/flutter_test.dart';

Map<String, dynamic> _baseMap({required dynamic publishedDate}) {
return {
'_id': 'bk-1',
'title': 'Chronicles of Testing',
'cover_image': 'https://example.com/cover.png',
'author': {'name': 'QA Author'},
'category': {'name': 'Adventure'},
'summary': 'An adventure in unit testing.',
'details': {
'price': 'Rp 10.000',
'total_pages': '420 pages',
'published_date': publishedDate,
},
'publisher': 'Test House',
};
}

void main() {
group('BookModel.fromMap Dates', () {
test('natural language to date', () {
final model = BookModel.fromMap(
_baseMap(publishedDate: '29 November 2023'),
);

expect(model.publishedDate.year, 2023);
expect(model.publishedDate.month, 11);
expect(model.publishedDate.day, 29);
expect(model.totalPages, 420);
});

test('falls back to epoch when published_date is invalid', () {
final model = BookModel.fromMap(_baseMap(publishedDate: 'not a date'));

expect(model.publishedDate.year, 1970);
expect(model.publishedDate.month, 1);
expect(model.publishedDate.day, 1);
});
});
}
Loading