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
10 changes: 9 additions & 1 deletion pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ packages:
source: hosted
version: "2.0.7"
fake_async:
dependency: transitive
dependency: "direct main"
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
Expand Down Expand Up @@ -333,6 +333,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mocktail:
dependency: "direct main"
description:
name: mocktail
sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
nested:
dependency: transitive
description:
Expand Down
4 changes: 4 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ dependencies:
# Splash
flutter_native_splash: ^2.4.6

# Tests
mocktail: ^1.0.4
fake_async: ^1.3.3

dev_dependencies:
flutter_test:
sdk: flutter
Expand Down
143 changes: 143 additions & 0 deletions test/blocs/achievements_bloc_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:mocktail/mocktail.dart';
import 'package:quiz_master/features/achievements/presentation/blocs/achievements_bloc.dart';
import 'package:quiz_master/utils/enums.dart';

class MockStorage extends Mock implements Storage {}

void main() {
late Storage storage;
late AchievementsBloc bloc;

setUp(() {
storage = MockStorage();
when(() => storage.write(any(), any<dynamic>())).thenAnswer((_) async {});
when(() => storage.read(any())).thenReturn(null);
HydratedBloc.storage = storage;

bloc = AchievementsBloc();
});

group('AchievementsBloc', () {
test('initial state is empty', () {
expect(bloc.state.unlockedLevels, {});
expect(bloc.state.totalQuizzes, 0);
expect(bloc.state.totalCorrectAnswers, 0);
expect(bloc.state.currentLevel, 1);
expect(bloc.state.bestQuizTimeSeconds, 9999);
});

test('CheckAchievements unlocks leveling achievements correctly', () {
bloc.add(
CheckAchievements(
quizCount: 0,
correctAnswers: 0,
level: 5,
quizTimeSeconds: 999,
),
);
bloc.stream.listen(
expectAsync1((state) {
expect(
state.unlockedLevels[AchievementCategory.leveling],
AchievementLevel.bronze,
);
}),
);
});

test('CheckAchievements unlocks answerMaster achievements correctly', () {
bloc.add(
CheckAchievements(
quizCount: 0,
correctAnswers: 50,
level: 1,
quizTimeSeconds: 999,
),
);
bloc.stream.listen(
expectAsync1((state) {
expect(
state.unlockedLevels[AchievementCategory.answerMaster],
AchievementLevel.gold,
);
}),
);
});

test('CheckAchievements unlocks quizAbuser achievements correctly', () {
bloc.add(
CheckAchievements(
quizCount: 25,
correctAnswers: 0,
level: 1,
quizTimeSeconds: 999,
),
);
bloc.stream.listen(
expectAsync1((state) {
expect(
state.unlockedLevels[AchievementCategory.quizAbuser],
AchievementLevel.gold,
);
}),
);
});

test('CheckAchievements unlocks speedster achievements correctly', () {
bloc.add(
CheckAchievements(
quizCount: 0,
correctAnswers: 0,
level: 1,
quizTimeSeconds: 30,
),
);
bloc.stream.listen(
expectAsync1((state) {
expect(
state.unlockedLevels[AchievementCategory.speedster],
AchievementLevel.platinum,
);
}),
);
});

test('CheckAchievements updates totals and bestQuizTimeSeconds', () {
bloc.add(
CheckAchievements(
quizCount: 3,
correctAnswers: 5,
level: 2,
quizTimeSeconds: 50,
),
);
bloc.stream.listen(
expectAsync1((state) {
expect(state.totalQuizzes, 3);
expect(state.totalCorrectAnswers, 5);
expect(state.currentLevel, 2);
expect(state.bestQuizTimeSeconds, 50);
}),
);
});

test('fromJson and toJson work correctly', () {
final state = AchievementsState(
unlockedLevels: {AchievementCategory.leveling: AchievementLevel.bronze},
totalQuizzes: 1,
totalCorrectAnswers: 2,
currentLevel: 3,
bestQuizTimeSeconds: 100,
);
final json = bloc.toJson(state)!;
final newState = bloc.fromJson(json)!;
expect(newState.unlockedLevels, state.unlockedLevels);
expect(newState.totalQuizzes, state.totalQuizzes);
expect(newState.totalCorrectAnswers, state.totalCorrectAnswers);
expect(newState.currentLevel, state.currentLevel);
expect(newState.bestQuizTimeSeconds, state.bestQuizTimeSeconds);
});
});
}
68 changes: 68 additions & 0 deletions test/blocs/browse_bloc_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:quiz_master/features/browse/data/model/quiz.dart';
import 'package:quiz_master/features/browse/presentation/blocs/browse/browse_bloc.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

late BrowseBloc bloc;

final quiz1 = Quiz(
id: 1,
title: 'Flutter Basics',
category: 1,
icon: Icons.code,
description: 'Step by step guide to Flutter',
);

final quiz2 = Quiz(
id: 2,
title: 'Dart Advanced',
category: 1,
icon: Icons.code,
description: 'Deep dive into Dart',
);

group('BrowseBloc Tests', () {
setUp(() {
bloc = BrowseBloc();

bloc.emit(
bloc.state.copyWith(
allQuizzes: [quiz1, quiz2],
filteredQuizzes: [quiz1, quiz2],
),
);
});

test('initial state is empty', () {
final newBloc = BrowseBloc();
expect(newBloc.state.allQuizzes, []);
expect(newBloc.state.filteredQuizzes, []);
expect(newBloc.state.query, '');
});

test('BrowseQueryChanged filters quizzes correctly', () async {
bloc.add(const BrowseQueryChanged('flutter'));

await expectLater(
bloc.stream,
emits(bloc.state.copyWith(filteredQuizzes: [quiz1], query: 'flutter')),
);
});

test('BrowseReset resets filtered quizzes', () async {
bloc.emit(
bloc.state.copyWith(filteredQuizzes: [quiz1], query: 'flutter'),
);

bloc.add(BrowseReset());

await expectLater(
bloc.stream,
emits(bloc.state.copyWith(filteredQuizzes: [quiz1, quiz2], query: '')),
);
});
});
}
83 changes: 83 additions & 0 deletions test/blocs/quiz_cubit_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:quiz_master/features/quiz/domain/entity/quiz_params.dart';
import 'package:quiz_master/features/quiz/domain/usecase/get_quiz.dart';
import 'package:quiz_master/features/quiz/domain/entity/question.dart';
import 'package:quiz_master/features/quiz/presentation/blocs/quiz/quiz_cubit.dart';

class MockGetQuiz extends Mock implements GetQuiz {}

class FakeQuizParams extends Fake implements QuizParams {}

void main() {
late QuizCubit cubit;
late MockGetQuiz mockGetQuiz;

final sampleQuestions = [
Question(
category: 'General',
type: 'multiple',
difficulty: 'easy',
question: 'Sample question 1?',
correctAnswer: 'Answer 1',
incorrectAnswers: ['Wrong 1', 'Wrong 2', 'Wrong 3'],
),
];

setUpAll(() {
registerFallbackValue(FakeQuizParams());
});

setUp(() {
mockGetQuiz = MockGetQuiz();
cubit = QuizCubit(getQuiz: mockGetQuiz);
});

group('QuizCubit Tests', () {
test('initial state is QuizInitial', () {
expect(cubit.state, isA<QuizInitial>());
});

test('loadQuiz emits QuizLoading then QuizLoaded on success', () async {
when(() => mockGetQuiz(any())).thenAnswer((_) async => sampleQuestions);

final future = expectLater(
cubit.stream,
emitsInOrder([
isA<QuizLoading>(),
isA<QuizLoaded>().having(
(s) => s.questions,
'questions',
sampleQuestions,
),
]),
);

await cubit.loadQuiz(const QuizParams(amount: 1));

await future;
});

test('loadQuiz emits QuizLoading then QuizError on failure', () async {
when(() => mockGetQuiz(any())).thenThrow(Exception('Failed'));

final future = expectLater(
cubit.stream,
emitsInOrder([
isA<QuizLoading>(),
isA<QuizError>().having(
(s) => s.message,
'message',
contains('Failed'),
),
]),
);

await cubit.loadQuiz(const QuizParams(amount: 1));

await future;

verify(() => mockGetQuiz(any())).called(1);
});
});
}
Loading