Basic framework

This commit is contained in:
2025-09-23 19:12:34 +00:00
parent 321f449433
commit e193efcd76
27 changed files with 1958 additions and 94 deletions

View File

@@ -1,12 +1,27 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttery/fluttery.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttery/logger.dart';
import 'package:kiwi/kiwi.dart';
import 'mocks/mocks.dart';
void main() {
test('adds one to input values', () {
final calculator = Calculator();
expect(calculator.addOne(2), 3);
expect(calculator.addOne(-7), -6);
expect(calculator.addOne(0), 1);
group('App Service Tests', () {
// Clear the singleton state before each test to ensure isolation
setUp(() {
// KiwiContainer provides a clear method to remove all registered services
KiwiContainer().clear();
});
test('should register and resolve a custom service', () {
// Register a mock logger service
final mockLogger = MockLogger();
App.registerService<Logger>(() => mockLogger);
// Resolve the service and check if it's the same instance
final resolvedLogger = App.service<Logger>();
expect(resolvedLogger, isA<MockLogger>());
expect(resolvedLogger, same(mockLogger));
});
});
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:logging/logging.dart' as lib;
import 'package:fluttery/src/logger/logger_impl.dart';
// Mock class for the lib.Logger
class MockLibLogger extends Mock implements lib.Logger {}
void main() {
final mockLibLogger = MockLibLogger();
group('LoggerImpl', () {
late LoggerImpl loggerImpl;
setUp(() {
loggerImpl = LoggerImpl.forTest(mockLibLogger);
});
test('info method logs an info message', () {
loggerImpl.info('Info message');
// Verify that the info method was called on the mock logger with the correct message
verify(() => mockLibLogger.info('Info message')).called(1);
});
test('warning method logs a warning message', () {
loggerImpl.warning('Warning message');
// Verify that the warning method was called on the mock logger with the correct message
verify(() => mockLibLogger.warning('Warning message')).called(1);
});
test('error method logs an error message with optional parameters', () {
final exception = Exception('Test exception');
final stackTrace = StackTrace.current;
loggerImpl.error('Error message', exception, stackTrace);
// Verify that the severe method was called on the mock logger with the correct parameters
verify(
() => mockLibLogger.severe('Error message', exception, stackTrace),
).called(1);
});
test('debug method logs a debug message', () {
loggerImpl.debug('Debug message');
// Verify that the fine method was called on the mock logger with the correct message
verify(() => mockLibLogger.fine('Debug message')).called(1);
});
test('setLogLevel method sets the logger level', () {
// This is to capture the change in the logger level
var capturedLevel = lib.Level.INFO;
// Capture the setter call
when(() => mockLibLogger.level = any()).thenAnswer((invocation) {
capturedLevel = invocation.positionalArguments.first as lib.Level;
return capturedLevel;
});
// Set the log level
loggerImpl.setLogLevel(lib.Level.WARNING);
// Verify that the logger level is set to the expected level
expect(capturedLevel, lib.Level.WARNING);
});
});
}

View File

@@ -0,0 +1,25 @@
import 'package:fluttery/logger.dart';
import 'package:mocktail/mocktail.dart';
class MockLogger extends Mock implements Logger {}
class MockUtils {
static Logger mockLogger() {
final logger = MockLogger();
when(() => logger.debug(any())).thenAnswer((a) {
print("[DEBUG] ${a.positionalArguments[0]}");
});
when(() => logger.info(any())).thenAnswer((a) {
print("[INFO] ${a.positionalArguments[0]}");
});
when(() => logger.warning(any())).thenAnswer((a) {
print("[WARN] ${a.positionalArguments[0]}");
});
when(() => logger.error(any(), any(), any())).thenAnswer((a) {
print("[ERROR] ${a.positionalArguments[0]}\n${a.positionalArguments[2]}");
});
return logger;
}
}

View File

@@ -0,0 +1,176 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttery/src/preferences/preferences_impl.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
late PreferencesImpl preferences;
setUp(() async {
// Clear any existing data and set up a fresh in-memory instance
SharedPreferences.setMockInitialValues({});
// Create preferences instance that will use the real SharedPreferences
// but with in-memory storage for testing
preferences = PreferencesImpl();
// Give time for initialization
await Future.delayed(Duration.zero);
});
group('PreferencesImpl Tests', () {
test('setString and getString work with real implementation', () async {
const key = 'testKey';
const value = 'testValue';
await preferences.setString(key, value);
final result = await preferences.getString(key);
expect(result, value);
});
test('setInt and getInt work with real implementation', () async {
const key = 'testKey';
const value = 42;
await preferences.setInt(key, value);
final result = await preferences.getInt(key);
expect(result, value);
});
test('setBool and getBool work with real implementation', () async {
const key = 'testKey';
const value = true;
await preferences.setBool(key, value);
final result = await preferences.getBool(key);
expect(result, value);
});
test('setDouble and getDouble work with real implementation', () async {
const key = 'testKey';
const value = 3.14;
await preferences.setDouble(key, value);
final result = await preferences.getDouble(key);
expect(result, value);
});
test(
'setStringList and getStringList work with real implementation',
() async {
const key = 'testKey';
const value = ['one', 'two', 'three'];
await preferences.setStringList(key, value);
final result = await preferences.getStringList(key);
expect(result, value);
},
);
test('remove deletes key-value pair', () async {
const key = 'testKey';
const value = 'testValue';
// Set a value first
await preferences.setString(key, value);
expect(await preferences.getString(key), value);
// Remove it
await preferences.remove(key);
final result = await preferences.getString(key);
expect(result, isNull);
});
test('clear removes all data', () async {
// Set multiple values
await preferences.setString('key1', 'value1');
await preferences.setInt('key2', 42);
await preferences.setBool('key3', true);
// Verify they exist
expect(await preferences.getString('key1'), 'value1');
expect(await preferences.getInt('key2'), 42);
expect(await preferences.getBool('key3'), true);
// Clear all
await preferences.clear();
// Verify they're gone
expect(await preferences.getString('key1'), isNull);
expect(await preferences.getInt('key2'), isNull);
expect(await preferences.getBool('key3'), isNull);
});
test('getting non-existent keys returns null', () async {
expect(await preferences.getString('nonExistent'), isNull);
expect(await preferences.getInt('nonExistent'), isNull);
expect(await preferences.getBool('nonExistent'), isNull);
expect(await preferences.getDouble('nonExistent'), isNull);
expect(await preferences.getStringList('nonExistent'), isNull);
});
test('can overwrite existing values of the same type', () async {
const key = 'testKey';
const initialValue = 'initialValue';
const newValue = 'newValue';
await preferences.setString(key, initialValue);
expect(await preferences.getString(key), initialValue);
await preferences.setString(key, newValue);
expect(await preferences.getString(key), newValue);
});
test(
'different keys can store different data types simultaneously',
() async {
await preferences.setString('stringKey', 'value');
await preferences.setInt('intKey', 42);
await preferences.setBool('boolKey', true);
await preferences.setDouble('doubleKey', 3.14);
await preferences.setStringList('listKey', ['a', 'b', 'c']);
expect(await preferences.getString('stringKey'), 'value');
expect(await preferences.getInt('intKey'), 42);
expect(await preferences.getBool('boolKey'), true);
expect(await preferences.getDouble('doubleKey'), 3.14);
expect(await preferences.getStringList('listKey'), ['a', 'b', 'c']);
},
);
test('values can be overwritten with different data types', () async {
const key = 'testKey';
// Store a string value
await preferences.setString(key, 'stringValue');
expect(await preferences.getString(key), 'stringValue');
// Overwrite with an int - this replaces the string value
await preferences.setInt(key, 42);
expect(await preferences.getInt(key), 42);
// Overwrite with a bool - this replaces the int value
await preferences.setBool(key, true);
expect(await preferences.getBool(key), true);
});
test('persistence works across multiple operations', () async {
// Test that values persist through multiple set/get operations
await preferences.setString('key1', 'value1');
await preferences.setInt('key2', 100);
expect(await preferences.getString('key1'), 'value1');
expect(await preferences.getInt('key2'), 100);
// Modify one value and ensure the other remains
await preferences.setString('key1', 'newValue1');
expect(await preferences.getString('key1'), 'newValue1');
expect(await preferences.getInt('key2'), 100);
});
});
}

View File

@@ -0,0 +1,352 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:fluttery/src/storage/secure/secure_storage_impl.dart';
import 'package:mocktail/mocktail.dart';
class MockFlutterSecureStorage extends Mock implements FlutterSecureStorage {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late SecureStorageImpl secureStorage;
late MockFlutterSecureStorage mockStorage;
// Single instance of SecureStorageImpl used throughout all tests
setUpAll(() {
mockStorage = MockFlutterSecureStorage();
secureStorage = SecureStorageImpl.forTesting(instance: mockStorage);
});
setUp(() {
// Reset mock between tests
reset(mockStorage);
});
group('SecureStorageImpl String Tests', () {
test('write and read string work', () async {
const key = 'testStringKey';
const value = 'testStringValue';
when(
() => mockStorage.write(key: key, value: value),
).thenAnswer((_) async {});
when(() => mockStorage.read(key: key)).thenAnswer((_) async => value);
await secureStorage.write(key, value);
final result = await secureStorage.read(key);
expect(result, value);
verify(() => mockStorage.write(key: key, value: value)).called(1);
verify(() => mockStorage.read(key: key)).called(1);
});
test('read returns null for non-existent string key', () async {
const key = 'nonExistentKey';
when(() => mockStorage.read(key: key)).thenAnswer((_) async => null);
final result = await secureStorage.read(key);
expect(result, isNull);
verify(() => mockStorage.read(key: key)).called(1);
});
test('can overwrite existing string values', () async {
const key = 'overwriteKey';
const initialValue = 'initialValue';
const newValue = 'newValue';
when(
() => mockStorage.write(key: key, value: initialValue),
).thenAnswer((_) async {});
when(
() => mockStorage.write(key: key, value: newValue),
).thenAnswer((_) async {});
when(
() => mockStorage.read(key: key),
).thenAnswer((_) async => initialValue);
await secureStorage.write(key, initialValue);
final firstResult = await secureStorage.read(key);
expect(firstResult, initialValue);
when(() => mockStorage.read(key: key)).thenAnswer((_) async => newValue);
await secureStorage.write(key, newValue);
final secondResult = await secureStorage.read(key);
expect(secondResult, newValue);
});
});
group('SecureStorageImpl Integer Tests', () {
test('writeInt and readInt work', () async {
const key = 'testIntKey';
const value = 42;
const stringValue = '42';
when(
() => mockStorage.write(key: key, value: stringValue),
).thenAnswer((_) async {});
when(
() => mockStorage.read(key: key),
).thenAnswer((_) async => stringValue);
await secureStorage.writeInt(key, value);
final result = await secureStorage.readInt(key);
expect(result, value);
verify(() => mockStorage.write(key: key, value: stringValue)).called(1);
verify(() => mockStorage.read(key: key)).called(1);
});
test('readInt returns null for non-existent key', () async {
const key = 'nonExistentIntKey';
when(() => mockStorage.read(key: key)).thenAnswer((_) async => null);
final result = await secureStorage.readInt(key);
expect(result, isNull);
verify(() => mockStorage.read(key: key)).called(1);
});
test('readInt handles negative numbers', () async {
const key = 'negativeIntKey';
const value = -123;
const stringValue = '-123';
when(
() => mockStorage.write(key: key, value: stringValue),
).thenAnswer((_) async {});
when(
() => mockStorage.read(key: key),
).thenAnswer((_) async => stringValue);
await secureStorage.writeInt(key, value);
final result = await secureStorage.readInt(key);
expect(result, value);
});
test('readInt returns null for invalid integer string', () async {
const key = 'invalidIntKey';
const invalidValue = 'not_a_number';
when(
() => mockStorage.read(key: key),
).thenAnswer((_) async => invalidValue);
final result = await secureStorage.readInt(key);
expect(result, isNull);
});
});
group('SecureStorageImpl Boolean Tests', () {
test('writeBool and readBool work with true', () async {
const key = 'testBoolTrueKey';
const value = true;
const stringValue = 'true';
when(
() => mockStorage.write(key: key, value: stringValue),
).thenAnswer((_) async {});
when(
() => mockStorage.read(key: key),
).thenAnswer((_) async => stringValue);
await secureStorage.writeBool(key, value);
final result = await secureStorage.readBool(key);
expect(result, value);
});
test('writeBool and readBool work with false', () async {
const key = 'testBoolFalseKey';
const value = false;
const stringValue = 'false';
when(
() => mockStorage.write(key: key, value: stringValue),
).thenAnswer((_) async {});
when(
() => mockStorage.read(key: key),
).thenAnswer((_) async => stringValue);
await secureStorage.writeBool(key, value);
final result = await secureStorage.readBool(key);
expect(result, value);
});
test('readBool handles case insensitive true/false', () async {
const key = 'caseInsensitiveKey';
// Test "TRUE"
when(() => mockStorage.read(key: key)).thenAnswer((_) async => 'TRUE');
expect(await secureStorage.readBool(key), true);
// Test "False"
when(() => mockStorage.read(key: key)).thenAnswer((_) async => 'False');
expect(await secureStorage.readBool(key), false);
});
test('readBool returns null for invalid boolean string', () async {
const key = 'invalidBoolKey';
const invalidValue = 'maybe';
when(
() => mockStorage.read(key: key),
).thenAnswer((_) async => invalidValue);
final result = await secureStorage.readBool(key);
expect(result, isNull);
});
});
group('SecureStorageImpl Double Tests', () {
test('writeDouble and readDouble work', () async {
const key = 'testDoubleKey';
const value = 3.14159;
const stringValue = '3.14159';
when(
() => mockStorage.write(key: key, value: stringValue),
).thenAnswer((_) async {});
when(
() => mockStorage.read(key: key),
).thenAnswer((_) async => stringValue);
await secureStorage.writeDouble(key, value);
final result = await secureStorage.readDouble(key);
expect(result, value);
});
test('readDouble returns null for invalid double string', () async {
const key = 'invalidDoubleKey';
const invalidValue = 'not_a_double';
when(
() => mockStorage.read(key: key),
).thenAnswer((_) async => invalidValue);
final result = await secureStorage.readDouble(key);
expect(result, isNull);
});
});
group('SecureStorageImpl Utility Tests', () {
test('containsKey returns true for existing key', () async {
const key = 'existingKey';
when(
() => mockStorage.containsKey(key: key),
).thenAnswer((_) async => true);
final exists = await secureStorage.containsKey(key);
expect(exists, isTrue);
verify(() => mockStorage.containsKey(key: key)).called(1);
});
test('containsKey returns false for non-existent key', () async {
const key = 'nonExistentKey';
when(
() => mockStorage.containsKey(key: key),
).thenAnswer((_) async => false);
final exists = await secureStorage.containsKey(key);
expect(exists, isFalse);
});
test('delete removes key-value pair', () async {
const key = 'deleteTestKey';
when(() => mockStorage.delete(key: key)).thenAnswer((_) async {});
await secureStorage.delete(key);
verify(() => mockStorage.delete(key: key)).called(1);
});
test('readAll and readAllKeys return all stored keys', () async {
final testData = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'};
when(() => mockStorage.readAll()).thenAnswer((_) async => testData);
final allKeys = await secureStorage.readAll();
final allKeysAlt = await secureStorage.readAllKeys();
expect(allKeys, containsAll(testData.keys));
expect(allKeys.length, testData.length);
expect(allKeysAlt, equals(allKeys));
});
test('deleteAll removes all key-value pairs', () async {
when(() => mockStorage.deleteAll()).thenAnswer((_) async {});
await secureStorage.deleteAll();
verify(() => mockStorage.deleteAll()).called(1);
});
});
group('SecureStorageImpl Mixed Data Type Tests', () {
test('different data types work correctly', () async {
const stringKey = 'stringKey';
const intKey = 'intKey';
const boolKey = 'boolKey';
const doubleKey = 'doubleKey';
const stringValue = 'hello world';
const intValue = 123;
const boolValue = true;
const doubleValue = 45.67;
// Mock all write operations
when(
() => mockStorage.write(key: stringKey, value: stringValue),
).thenAnswer((_) async {});
when(
() => mockStorage.write(key: intKey, value: intValue.toString()),
).thenAnswer((_) async {});
when(
() => mockStorage.write(key: boolKey, value: boolValue.toString()),
).thenAnswer((_) async {});
when(
() => mockStorage.write(key: doubleKey, value: doubleValue.toString()),
).thenAnswer((_) async {});
// Mock all read operations
when(
() => mockStorage.read(key: stringKey),
).thenAnswer((_) async => stringValue);
when(
() => mockStorage.read(key: intKey),
).thenAnswer((_) async => intValue.toString());
when(
() => mockStorage.read(key: boolKey),
).thenAnswer((_) async => boolValue.toString());
when(
() => mockStorage.read(key: doubleKey),
).thenAnswer((_) async => doubleValue.toString());
// Store different types
await secureStorage.write(stringKey, stringValue);
await secureStorage.writeInt(intKey, intValue);
await secureStorage.writeBool(boolKey, boolValue);
await secureStorage.writeDouble(doubleKey, doubleValue);
// Retrieve and verify all types
expect(await secureStorage.read(stringKey), stringValue);
expect(await secureStorage.readInt(intKey), intValue);
expect(await secureStorage.readBool(boolKey), boolValue);
expect(await secureStorage.readDouble(doubleKey), doubleValue);
});
});
}

View File

@@ -0,0 +1,54 @@
import 'dart:io' show Platform;
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttery/src/system/environment/environment_impl.dart';
import 'package:package_info_plus/package_info_plus.dart';
void main() {
setUp(() {
PackageInfo.setMockInitialValues(
appName: 'Test App',
packageName: 'com.example.testapp',
version: '1.2.3',
buildNumber: '42',
buildSignature: 'mock-signature',
installerStore: 'mock-store',
);
});
group('EnvironmentImpl', () {
test('defaults before loadPackageInfo()', () {
final env = EnvironmentImpl();
expect(env.appName, equals('Unknown'));
expect(env.packageName, equals('Unknown'));
expect(env.version, equals('0.0.0'));
expect(env.buildNumber, equals('0'));
});
test('loadPackageInfo() populates fields from PackageInfo', () async {
final env = EnvironmentImpl();
await env.loadPackageInfo();
expect(env.appName, equals('Test App'));
expect(env.packageName, equals('com.example.testapp'));
expect(env.version, equals('1.2.3'));
expect(env.buildNumber, equals('42'));
});
test('platform flags mirror dart:io Platform', () {
final env = EnvironmentImpl();
expect(env.isAndroid, equals(Platform.isAndroid));
expect(env.isIOS, equals(Platform.isIOS));
});
test('build mode flags reflect Flutter constants', () {
final env = EnvironmentImpl();
// In unit tests, this should typically be: debug=true, release/profile=false.
expect(env.isDebug, isTrue);
expect(env.isRelease, isFalse);
expect(env.isProfile, isFalse);
});
});
}

View File

@@ -0,0 +1,413 @@
import 'dart:async';
import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttery/fluttery.dart';
import 'package:fluttery/logger.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fluttery/src/system/worker/worker_impl.dart';
import 'package:fluttery/worker.dart';
import '../../mocks/mocks.dart';
Future<void> pumpMicro([int times = 10]) => pumpEventQueue(times: times);
Future<void> waitFor(
bool Function() predicate, {
Duration timeout = const Duration(seconds: 2),
Duration step = const Duration(milliseconds: 20),
}) async {
final deadline = DateTime.now().add(timeout);
while (DateTime.now().isBefore(deadline)) {
if (predicate()) return;
await Future<void>.delayed(step);
}
fail('Condition not met within $timeout');
}
void main() {
setUpAll(() async {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
expect(ServicesBinding.rootIsolateToken, isNotNull);
App.registerService<Logger>(() => MockUtils.mockLogger());
});
group('WorkerImpl', () {
late WorkerImpl worker;
setUp(() {
worker = WorkerImpl();
SharedPreferences.setMockInitialValues({});
});
test('spawn returns value; preTask runs; active->history tracking', () async {
// We'll verify preTask runs by checking the task itself can access
// what the preTask sets up (SharedPreferences mock)
final future = worker.spawn<int>(
'successful_task',
() async {
await Future.delayed(const Duration(milliseconds: 20));
// This would fail if preTask didn't run to set up SharedPreferences mock
SharedPreferences.setMockInitialValues({'test': 'verified'});
final prefs = await SharedPreferences.getInstance();
await prefs.setString('preTaskRan', 'true');
return 42;
},
preTask: () {
// Set up the SharedPreferences mock so the task can use it
SharedPreferences.setMockInitialValues({});
},
);
// Verify worker is registered as active shortly after spawn
await Future<void>.delayed(const Duration(milliseconds: 10));
expect(worker.getActiveWorkers().length, 1);
final activeWorkers = worker.getActiveWorkers();
expect(activeWorkers.first.name, 'successful_task');
expect(activeWorkers.first.status, WorkerStatus.running);
// Wait for completion
final result = await future;
expect(result, 42);
// The fact that the task completed successfully without throwing an exception
// when trying to use SharedPreferences proves that preTask ran
// Wait for the completion handlers to run and move worker to history
await waitFor(() => worker.getActiveWorkers().isEmpty);
// Verify the worker was moved to history with completed status
final historyWorkers = worker.getAllWorkers();
expect(historyWorkers.length, 1);
expect(historyWorkers.first.status, WorkerStatus.completed);
expect(historyWorkers.first.name, 'successful_task');
expect(historyWorkers.first.endedAt, isNotNull);
});
test('timeout marks job as timedOut and throws TimeoutException', () async {
final timedWorker = WorkerImpl(
defaultTimeout: const Duration(milliseconds: 50),
);
await expectLater(
timedWorker.spawn<void>(
'timeout_task',
() async => Future.delayed(const Duration(milliseconds: 200)),
preTask: () => SharedPreferences.setMockInitialValues({}),
),
throwsA(isA<TimeoutException>()),
);
// Wait for worker to update history
await waitFor(() => timedWorker.getAllWorkers().isNotEmpty);
final allWorkers = timedWorker.getAllWorkers();
expect(allWorkers.first.status, WorkerStatus.timedOut);
expect(allWorkers.first.name, 'timeout_task');
});
test('custom timeout overrides default timeout', () async {
final timedWorker = WorkerImpl(
defaultTimeout: const Duration(milliseconds: 200), // Long default
);
await expectLater(
timedWorker.spawn<void>(
'custom_timeout_task',
() async => Future.delayed(const Duration(milliseconds: 100)),
timeout: const Duration(milliseconds: 50), // Short custom timeout
preTask: () => SharedPreferences.setMockInitialValues({}),
),
throwsA(isA<TimeoutException>()),
);
await waitFor(() => timedWorker.getAllWorkers().isNotEmpty);
expect(timedWorker.getAllWorkers().first.status, WorkerStatus.timedOut);
});
test('failure marks job as failed and surfaces exception', () async {
// Create a variable to capture the actual exception
Object? caughtException;
try {
await worker.spawn<void>('failing_task', () async {
await Future<void>.delayed(const Duration(milliseconds: 10));
throw StateError('intentional failure');
}, preTask: () => SharedPreferences.setMockInitialValues({}));
fail('Expected an exception to be thrown');
} catch (e) {
caughtException = e;
}
// Verify that an exception was thrown (could be RemoteError or the original StateError)
expect(caughtException, isNotNull);
expect(
caughtException is RemoteError || caughtException is StateError,
isTrue,
reason:
'Should throw either RemoteError or StateError, got: ${caughtException.runtimeType}',
);
// Wait for worker to update history
await waitFor(() => worker.getAllWorkers().isNotEmpty);
final allWorkers = worker.getAllWorkers();
expect(allWorkers.first.status, WorkerStatus.failed);
expect(allWorkers.first.name, 'failing_task');
expect(allWorkers.first.error, isNotNull);
});
test('getWorker finds active and completed workers by ID', () async {
final future = worker.spawn<int>('trackable_task', () async {
await Future.delayed(const Duration(milliseconds: 50));
return 123;
}, preTask: () => SharedPreferences.setMockInitialValues({}));
// Find worker while active
await Future<void>.delayed(const Duration(milliseconds: 10));
final activeWorkers = worker.getActiveWorkers();
expect(activeWorkers.length, 1);
final workerId = activeWorkers.first.id;
final activeWorker = worker.getWorker(workerId);
expect(activeWorker, isNotNull);
expect(activeWorker!.status, WorkerStatus.running);
// Wait for completion
await future;
await waitFor(() => worker.getActiveWorkers().isEmpty);
// Find worker in history
final completedWorker = worker.getWorker(workerId);
expect(completedWorker, isNotNull);
expect(completedWorker!.status, WorkerStatus.completed);
});
test('getWorker returns null for non-existent ID', () {
expect(worker.getWorker('non-existent'), isNull);
expect(worker.getWorker('999999'), isNull);
});
test('worker ID generation uses timestamp format', () async {
final futures = <Future>[];
final Set<String> generatedIds = <String>{};
// Spawn multiple workers with sufficient delay to ensure unique timestamps
for (int i = 0; i < 3; i++) {
futures.add(
worker.spawn<void>(
'task_$i',
() async => Future.delayed(const Duration(milliseconds: 10)),
preTask: () => SharedPreferences.setMockInitialValues({}),
),
);
// Ensure sufficient delay for different timestamps
await Future<void>.delayed(const Duration(milliseconds: 1));
}
// Wait for all workers to be registered as active
await waitFor(() => worker.getActiveWorkers().length == 3);
final activeWorkers = worker.getActiveWorkers();
expect(activeWorkers.length, 3);
// Verify each ID follows the timestamp format and is unique
for (final workerInfo in activeWorkers) {
expect(workerInfo.id, startsWith('iso-'));
expect(
generatedIds.contains(workerInfo.id),
isFalse,
reason: 'Worker ID should be unique: ${workerInfo.id}',
);
generatedIds.add(workerInfo.id);
final timestampPart = workerInfo.id.substring(
4,
); // Remove 'iso-' prefix
final timestamp = int.tryParse(timestampPart);
expect(
timestamp,
isNotNull,
reason: 'Timestamp part should be a valid integer: $timestampPart',
);
expect(
timestamp,
greaterThan(0),
reason: 'Timestamp should be positive: $timestamp',
);
// Verify timestamp is reasonable (not too old, not in future)
final now = DateTime.now().millisecondsSinceEpoch;
expect(
timestamp,
lessThanOrEqualTo(now),
reason: 'Timestamp should not be in the future',
);
expect(
timestamp,
greaterThan(now - 10000),
reason: 'Timestamp should be recent (within 10 seconds)',
);
}
// Verify we generated 3 unique IDs
expect(generatedIds.length, 3);
await Future.wait(futures);
});
test('getAllWorkers combines active and history workers', () async {
// Spawn and complete a short task first
await worker.spawn<void>(
'short_task',
() async => Future.delayed(const Duration(milliseconds: 10)),
preTask: () => SharedPreferences.setMockInitialValues({}),
);
// Wait for short task to complete and move to history
await waitFor(() => worker.getAllWorkers().length == 1);
await waitFor(() => worker.getActiveWorkers().isEmpty);
// Verify we have one completed worker in history
expect(worker.getAllWorkers().length, 1);
expect(worker.getAllWorkers().first.status, WorkerStatus.completed);
expect(worker.getAllWorkers().first.name, 'short_task');
// Now spawn a long-running task
final longTask = worker.spawn<void>(
'long_task',
() async => Future.delayed(const Duration(milliseconds: 100)),
preTask: () => SharedPreferences.setMockInitialValues({}),
);
// Wait briefly for long task to be registered as active
await Future<void>.delayed(const Duration(milliseconds: 10));
// Now we should have 2 workers: 1 active (long_task) and 1 in history (short_task)
final allWorkers = worker.getAllWorkers();
expect(allWorkers.length, 2);
final activeCount = allWorkers
.where((w) => w.status == WorkerStatus.running)
.length;
final completedCount = allWorkers
.where((w) => w.status == WorkerStatus.completed)
.length;
expect(activeCount, 1);
expect(completedCount, 1);
// Verify the active worker is the long task
final activeWorkers = worker.getActiveWorkers();
expect(activeWorkers.length, 1);
expect(activeWorkers.first.name, 'long_task');
await longTask;
});
test('purge removes old workers from history', () async {
// Complete a task
await worker.spawn<void>(
'old_task',
() async => Future.delayed(const Duration(milliseconds: 10)),
preTask: () => SharedPreferences.setMockInitialValues({}),
);
await waitFor(() => worker.getAllWorkers().isNotEmpty);
expect(worker.getAllWorkers().length, 1);
// Purge with zero max age (removes everything)
worker.purge(maxAge: Duration.zero);
await pumpMicro();
expect(worker.getAllWorkers(), isEmpty);
});
test('maxHistory limit is enforced', () async {
final limitedWorker = WorkerImpl(maxHistory: 2);
// Complete 3 tasks
for (int i = 0; i < 3; i++) {
await limitedWorker.spawn<void>(
'task_$i',
() async => Future.delayed(const Duration(milliseconds: 5)),
preTask: () => SharedPreferences.setMockInitialValues({}),
);
await waitFor(() => limitedWorker.getActiveWorkers().isEmpty);
}
// Should only keep the last 2 in history
final allWorkers = limitedWorker.getAllWorkers();
expect(allWorkers.length, 2);
// Should be the most recent tasks (task_1 and task_2)
final names = allWorkers.map((w) => w.name).toSet();
expect(names.contains('task_1'), isTrue);
expect(names.contains('task_2'), isTrue);
expect(names.contains('task_0'), isFalse);
});
test(
'no timeout when defaultTimeout is null and timeout is null',
() async {
final noTimeoutWorker = WorkerImpl(defaultTimeout: null);
final result = await noTimeoutWorker.spawn<int>(
'no_timeout_task',
() async {
await Future.delayed(const Duration(milliseconds: 50));
return 999;
},
preTask: () => SharedPreferences.setMockInitialValues({}),
);
expect(result, 999);
await waitFor(() => noTimeoutWorker.getAllWorkers().isNotEmpty);
expect(
noTimeoutWorker.getAllWorkers().first.status,
WorkerStatus.completed,
);
},
);
test('active workers are sorted by start time', () async {
final futures = <Future>[];
// Spawn workers with small delays between them
for (int i = 0; i < 3; i++) {
futures.add(
worker.spawn<void>(
'timed_task_$i',
() async => Future.delayed(const Duration(milliseconds: 100)),
preTask: () => SharedPreferences.setMockInitialValues({}),
),
);
await Future<void>.delayed(const Duration(milliseconds: 10));
}
final activeWorkers = worker.getActiveWorkers();
expect(activeWorkers.length, 3);
// Verify they are sorted by start time (earliest first)
for (int i = 1; i < activeWorkers.length; i++) {
expect(
activeWorkers[i - 1].startedAt.isBefore(activeWorkers[i].startedAt) ||
activeWorkers[i - 1].startedAt.isAtSameMomentAs(
activeWorkers[i].startedAt,
),
isTrue,
reason: 'Workers should be sorted by start time',
);
}
await Future.wait(futures);
});
});
}