mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-05 04:22:55 +00:00
feat: Complete Phase 1 - Full Flutter app implementation with comprehensive features
Version: 1.1.0 Major changes: - Implemented complete Flutter app structure with all core features - Added comprehensive UI screens for auth, countdown, goals, profile, settings, and social features - Integrated Supabase backend with authentication and data repositories - Added offline support with Hive caching and local storage - Implemented comprehensive routing with go_router - Added location services with Google Maps integration - Implemented notifications and home widget support - Added voice recording capabilities and AI chat features - Created comprehensive test suite and documentation - Added Android and iOS platform configurations - Implemented achievements system and social features - Added calendar integration and bucket list functionality This represents a complete Phase 1 milestone with 3,775 additions across 31 files.
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lifetimer/core/utils/date_time_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('DateTimeUtils', () {
|
||||
group('calculateEndDate', () {
|
||||
test('should calculate end date correctly', () {
|
||||
final startDate = DateTime(2024, 1, 1);
|
||||
final endDate = DateTimeUtils.calculateEndDate(startDate);
|
||||
|
||||
final expectedEndDate = DateTime(2024, 1, 1).add(const Duration(days: 1356));
|
||||
expect(endDate, equals(expectedEndDate));
|
||||
});
|
||||
|
||||
test('should handle leap years correctly', () {
|
||||
final startDate = DateTime(2024, 2, 28); // 2024 is a leap year
|
||||
final endDate = DateTimeUtils.calculateEndDate(startDate);
|
||||
|
||||
final expectedEndDate = startDate.add(const Duration(days: 1356));
|
||||
expect(endDate, equals(expectedEndDate));
|
||||
});
|
||||
|
||||
test('should preserve time component', () {
|
||||
final startDate = DateTime(2024, 1, 1, 12, 30, 45);
|
||||
final endDate = DateTimeUtils.calculateEndDate(startDate);
|
||||
|
||||
final expectedEndDate = DateTime(2024, 1, 1, 12, 30, 45).add(const Duration(days: 1356));
|
||||
expect(endDate, equals(expectedEndDate));
|
||||
});
|
||||
});
|
||||
|
||||
group('formatCountdown', () {
|
||||
test('should format duration with all components', () {
|
||||
const duration = Duration(days: 5, hours: 3, minutes: 45, seconds: 30);
|
||||
final formatted = DateTimeUtils.formatCountdown(duration);
|
||||
|
||||
expect(formatted, equals('5d 3h 45m 30s'));
|
||||
});
|
||||
|
||||
test('should format duration with only days', () {
|
||||
const duration = Duration(days: 10);
|
||||
final formatted = DateTimeUtils.formatCountdown(duration);
|
||||
|
||||
expect(formatted, equals('10d 0h 0m 0s'));
|
||||
});
|
||||
|
||||
test('should format duration with only hours and minutes', () {
|
||||
const duration = Duration(hours: 2, minutes: 30);
|
||||
final formatted = DateTimeUtils.formatCountdown(duration);
|
||||
|
||||
expect(formatted, equals('0d 2h 30m 0s'));
|
||||
});
|
||||
|
||||
test('should format duration with only minutes and seconds', () {
|
||||
const duration = Duration(minutes: 15, seconds: 45);
|
||||
final formatted = DateTimeUtils.formatCountdown(duration);
|
||||
|
||||
expect(formatted, equals('0d 0h 15m 45s'));
|
||||
});
|
||||
|
||||
test('should format zero duration', () {
|
||||
const duration = Duration.zero;
|
||||
final formatted = DateTimeUtils.formatCountdown(duration);
|
||||
|
||||
expect(formatted, equals('0d 0h 0m 0s'));
|
||||
});
|
||||
});
|
||||
|
||||
group('formatCountdownCompact', () {
|
||||
test('should show days and hours when days > 0', () {
|
||||
const duration = Duration(days: 5, hours: 3, minutes: 30);
|
||||
final formatted = DateTimeUtils.formatCountdownCompact(duration);
|
||||
|
||||
expect(formatted, equals('5d 3h'));
|
||||
});
|
||||
|
||||
test('should show hours and minutes when days == 0 and hours > 0', () {
|
||||
const duration = Duration(hours: 3, minutes: 30);
|
||||
final formatted = DateTimeUtils.formatCountdownCompact(duration);
|
||||
|
||||
expect(formatted, equals('3h 30m'));
|
||||
});
|
||||
|
||||
test('should show only minutes when days == 0 and hours == 0', () {
|
||||
const duration = Duration(minutes: 30);
|
||||
final formatted = DateTimeUtils.formatCountdownCompact(duration);
|
||||
|
||||
expect(formatted, equals('30m'));
|
||||
});
|
||||
|
||||
test('should handle zero duration', () {
|
||||
const duration = Duration.zero;
|
||||
final formatted = DateTimeUtils.formatCountdownCompact(duration);
|
||||
|
||||
expect(formatted, equals('0m'));
|
||||
});
|
||||
});
|
||||
|
||||
group('calculateProgress', () {
|
||||
test('should calculate progress correctly', () {
|
||||
final startDate = DateTime(2024, 1, 1);
|
||||
final endDate = DateTime(2024, 1, 11); // 10 days total
|
||||
|
||||
// Mock current time as 5 days after start
|
||||
final progress = DateTimeUtils.calculateProgress(startDate, endDate);
|
||||
|
||||
// Since we can't mock DateTime.now(), we'll just verify the method works
|
||||
expect(progress, greaterThanOrEqualTo(0.0));
|
||||
expect(progress, lessThanOrEqualTo(1.0));
|
||||
});
|
||||
|
||||
test('should return 1.0 when countdown is finished', () {
|
||||
final startDate = DateTime(2024, 1, 1);
|
||||
final endDate = DateTime(2023, 12, 31); // Past date
|
||||
|
||||
final progress = DateTimeUtils.calculateProgress(startDate, endDate);
|
||||
|
||||
expect(progress, equals(1.0));
|
||||
});
|
||||
|
||||
test('should return value between 0 and 1', () {
|
||||
final startDate = DateTime.now().subtract(const Duration(days: 5));
|
||||
final endDate = DateTime.now().add(const Duration(days: 5));
|
||||
|
||||
final progress = DateTimeUtils.calculateProgress(startDate, endDate);
|
||||
|
||||
expect(progress, greaterThan(0.0));
|
||||
expect(progress, lessThan(1.0));
|
||||
});
|
||||
});
|
||||
|
||||
group('formatDate', () {
|
||||
test('should format date correctly', () {
|
||||
final date = DateTime(2024, 1, 15);
|
||||
final formatted = DateTimeUtils.formatDate(date);
|
||||
|
||||
expect(formatted, equals('Jan 15, 2024'));
|
||||
});
|
||||
|
||||
test('should handle different months', () {
|
||||
final date = DateTime(2024, 12, 25);
|
||||
final formatted = DateTimeUtils.formatDate(date);
|
||||
|
||||
expect(formatted, equals('Dec 25, 2024'));
|
||||
});
|
||||
});
|
||||
|
||||
group('formatShortDate', () {
|
||||
test('should format short date correctly', () {
|
||||
final date = DateTime(2024, 1, 15);
|
||||
final formatted = DateTimeUtils.formatShortDate(date);
|
||||
|
||||
expect(formatted, equals('Jan 2024'));
|
||||
});
|
||||
|
||||
test('should handle different years', () {
|
||||
final date = DateTime(2025, 6, 30);
|
||||
final formatted = DateTimeUtils.formatShortDate(date);
|
||||
|
||||
expect(formatted, equals('Jun 2025'));
|
||||
});
|
||||
});
|
||||
|
||||
group('formatDateTime', () {
|
||||
test('should format date and time correctly', () {
|
||||
final dateTime = DateTime(2024, 1, 15, 14, 30);
|
||||
final formatted = DateTimeUtils.formatDateTime(dateTime);
|
||||
|
||||
expect(formatted, equals('Jan 15, 2024 • 14:30'));
|
||||
});
|
||||
});
|
||||
|
||||
group('formatRelativeTime', () {
|
||||
test('should show "Just now" for very recent times', () {
|
||||
final dateTime = DateTime.now().subtract(const Duration(seconds: 30));
|
||||
final formatted = DateTimeUtils.formatRelativeTime(dateTime);
|
||||
|
||||
expect(formatted, equals('Just now'));
|
||||
});
|
||||
|
||||
test('should show minutes for times less than an hour ago', () {
|
||||
final dateTime = DateTime.now().subtract(const Duration(minutes: 30));
|
||||
final formatted = DateTimeUtils.formatRelativeTime(dateTime);
|
||||
|
||||
expect(formatted, equals('30m ago'));
|
||||
});
|
||||
|
||||
test('should show hours for times less than a day ago', () {
|
||||
final dateTime = DateTime.now().subtract(const Duration(hours: 5));
|
||||
final formatted = DateTimeUtils.formatRelativeTime(dateTime);
|
||||
|
||||
expect(formatted, equals('5h ago'));
|
||||
});
|
||||
|
||||
test('should show days for times less than a week ago', () {
|
||||
final dateTime = DateTime.now().subtract(const Duration(days: 3));
|
||||
final formatted = DateTimeUtils.formatRelativeTime(dateTime);
|
||||
|
||||
expect(formatted, equals('3d ago'));
|
||||
});
|
||||
|
||||
test('should show formatted date for times older than a week', () {
|
||||
final dateTime = DateTime(2024, 1, 1);
|
||||
final formatted = DateTimeUtils.formatRelativeTime(dateTime);
|
||||
|
||||
expect(formatted, contains('Jan'));
|
||||
expect(formatted, contains('2024'));
|
||||
});
|
||||
});
|
||||
|
||||
group('isCountdownFinished', () {
|
||||
test('should return true when end date is in the past', () {
|
||||
final endDate = DateTime(2023, 1, 1);
|
||||
final isFinished = DateTimeUtils.isCountdownFinished(endDate);
|
||||
|
||||
expect(isFinished, isTrue);
|
||||
});
|
||||
|
||||
test('should return false when end date is in the future', () {
|
||||
final endDate = DateTime.now().add(const Duration(days: 10));
|
||||
final isFinished = DateTimeUtils.isCountdownFinished(endDate);
|
||||
|
||||
expect(isFinished, isFalse);
|
||||
});
|
||||
|
||||
test('should return true when end date is exactly now', () {
|
||||
final endDate = DateTime.now();
|
||||
final isFinished = DateTimeUtils.isCountdownFinished(endDate);
|
||||
|
||||
// This might be true or false depending on exact timing
|
||||
expect(isFinished, isA<bool>());
|
||||
});
|
||||
});
|
||||
|
||||
group('countdownDays constant', () {
|
||||
test('should be 1356 days', () {
|
||||
expect(DateTimeUtils.countdownDays, equals(1356));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lifetimer/core/utils/validators.dart';
|
||||
|
||||
void main() {
|
||||
group('Validators', () {
|
||||
group('validateEmail', () {
|
||||
test('should return error for empty email', () {
|
||||
expect(Validators.validateEmail(''), equals('Email is required'));
|
||||
expect(Validators.validateEmail(null), equals('Email is required'));
|
||||
});
|
||||
|
||||
test('should return error for invalid email format', () {
|
||||
expect(Validators.validateEmail('invalid'), equals('Please enter a valid email address'));
|
||||
expect(Validators.validateEmail('invalid@'), equals('Please enter a valid email address'));
|
||||
expect(Validators.validateEmail('@example.com'), equals('Please enter a valid email address'));
|
||||
expect(Validators.validateEmail('test@'), equals('Please enter a valid email address'));
|
||||
});
|
||||
|
||||
test('should return null for valid email', () {
|
||||
expect(Validators.validateEmail('test@example.com'), isNull);
|
||||
expect(Validators.validateEmail('user.name@domain.co.uk'), isNull);
|
||||
expect(Validators.validateEmail('test_user+tag@example.com'), isNull);
|
||||
});
|
||||
|
||||
test('should handle edge cases', () {
|
||||
expect(Validators.validateEmail('a@b.c'), isNull);
|
||||
expect(Validators.validateEmail('test@test.test'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('validatePassword', () {
|
||||
test('should return error for empty password', () {
|
||||
expect(Validators.validatePassword(''), equals('Password must be at least 6 characters'));
|
||||
expect(Validators.validatePassword(null), equals('Password is required'));
|
||||
});
|
||||
|
||||
test('should return error for password less than 6 characters', () {
|
||||
expect(Validators.validatePassword('12345'), equals('Password must be at least 6 characters'));
|
||||
expect(Validators.validatePassword('abc'), equals('Password must be at least 6 characters'));
|
||||
});
|
||||
|
||||
test('should return null for valid password', () {
|
||||
expect(Validators.validatePassword('123456'), isNull);
|
||||
expect(Validators.validatePassword('password'), isNull);
|
||||
expect(Validators.validatePassword('P@ssw0rd!'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('validateUsername', () {
|
||||
test('should return error for empty username', () {
|
||||
expect(Validators.validateUsername(''), equals('Username is required'));
|
||||
expect(Validators.validateUsername(null), equals('Username is required'));
|
||||
});
|
||||
|
||||
test('should return error for username less than 3 characters', () {
|
||||
expect(Validators.validateUsername('ab'), equals('Username must be at least 3 characters'));
|
||||
expect(Validators.validateUsername('a'), equals('Username must be at least 3 characters'));
|
||||
});
|
||||
|
||||
test('should return error for username more than 20 characters', () {
|
||||
expect(Validators.validateUsername('a' * 21), equals('Username must not exceed 20 characters'));
|
||||
});
|
||||
|
||||
test('should return error for username with invalid characters', () {
|
||||
expect(Validators.validateUsername('user name'), equals('Username can only contain letters, numbers, and underscores'));
|
||||
expect(Validators.validateUsername('user-name'), equals('Username can only contain letters, numbers, and underscores'));
|
||||
expect(Validators.validateUsername('user.name'), equals('Username can only contain letters, numbers, and underscores'));
|
||||
expect(Validators.validateUsername('user@name'), equals('Username can only contain letters, numbers, and underscores'));
|
||||
});
|
||||
|
||||
test('should return null for valid username', () {
|
||||
expect(Validators.validateUsername('user'), isNull);
|
||||
expect(Validators.validateUsername('user123'), isNull);
|
||||
expect(Validators.validateUsername('user_name'), isNull);
|
||||
expect(Validators.validateUsername('User_Name_123'), isNull);
|
||||
expect(Validators.validateUsername('a' * 20), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('validateGoalTitle', () {
|
||||
test('should return error for empty title', () {
|
||||
expect(Validators.validateGoalTitle(''), equals('Goal title is required'));
|
||||
expect(Validators.validateGoalTitle(null), equals('Goal title is required'));
|
||||
});
|
||||
|
||||
test('should return error for title more than 100 characters', () {
|
||||
expect(Validators.validateGoalTitle('a' * 101), equals('Goal title must not exceed 100 characters'));
|
||||
});
|
||||
|
||||
test('should return null for valid title', () {
|
||||
expect(Validators.validateGoalTitle('Learn to play guitar'), isNull);
|
||||
expect(Validators.validateGoalTitle('a' * 100), isNull);
|
||||
expect(Validators.validateGoalTitle('Run a marathon'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('validateGoalDescription', () {
|
||||
test('should return null for empty description', () {
|
||||
expect(Validators.validateGoalDescription(''), isNull);
|
||||
expect(Validators.validateGoalDescription(null), isNull);
|
||||
});
|
||||
|
||||
test('should return error for description more than 500 characters', () {
|
||||
expect(Validators.validateGoalDescription('a' * 501), equals('Description must not exceed 500 characters'));
|
||||
});
|
||||
|
||||
test('should return null for valid description', () {
|
||||
expect(Validators.validateGoalDescription('A short description'), isNull);
|
||||
expect(Validators.validateGoalDescription('a' * 500), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('validateGoalProgress', () {
|
||||
test('should return error for null progress', () {
|
||||
expect(Validators.validateGoalProgress(null), equals('Progress is required'));
|
||||
});
|
||||
|
||||
test('should return error for negative progress', () {
|
||||
expect(Validators.validateGoalProgress(-1), equals('Progress must be between 0 and 100'));
|
||||
expect(Validators.validateGoalProgress(-100), equals('Progress must be between 0 and 100'));
|
||||
});
|
||||
|
||||
test('should return error for progress greater than 100', () {
|
||||
expect(Validators.validateGoalProgress(101), equals('Progress must be between 0 and 100'));
|
||||
expect(Validators.validateGoalProgress(150), equals('Progress must be between 0 and 100'));
|
||||
});
|
||||
|
||||
test('should return null for valid progress', () {
|
||||
expect(Validators.validateGoalProgress(0), isNull);
|
||||
expect(Validators.validateGoalProgress(50), isNull);
|
||||
expect(Validators.validateGoalProgress(100), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('validateRequired', () {
|
||||
test('should return error for empty value', () {
|
||||
expect(Validators.validateRequired('', 'Name'), equals('Name is required'));
|
||||
expect(Validators.validateRequired(null, 'Name'), equals('Name is required'));
|
||||
});
|
||||
|
||||
test('should return null for valid value', () {
|
||||
expect(Validators.validateRequired('John', 'Name'), isNull);
|
||||
expect(Validators.validateRequired('123', 'Code'), isNull);
|
||||
});
|
||||
|
||||
test('should use provided field name in error message', () {
|
||||
expect(Validators.validateRequired('', 'Email'), equals('Email is required'));
|
||||
expect(Validators.validateRequired('', 'Password'), equals('Password is required'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user