Compare commits

...

5 Commits

6 changed files with 464 additions and 28 deletions

View File

@@ -0,0 +1,162 @@
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
import 'package:device_management/src/core/domain/repositories/app_usage_schedules_repository.dart';
import 'package:device_management/src/core/providers/app_usage_schedules_repository_provider.dart';
import 'package:device_management/src/features/app_usage_schedules/presentation/providers/app_usage_schedules_controller.dart';
import 'package:device_management/src/features/app_usage_schedules/presentation/providers/app_usage_schedules_editor_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:sf_shared/testing.dart';
class MockAppUsageSchedulesRepository extends Mock
implements AppUsageSchedulesRepository {}
const _period = AppUsageSchedulePeriodEntity(
periodStart: '08:00',
periodEnd: '12:00',
isPeriodEnabled: true,
);
const _period2 = AppUsageSchedulePeriodEntity(
periodStart: '14:00',
periodEnd: '18:00',
isPeriodEnabled: false,
);
void main() {
setUpAll(() {
registerFallbackValue(const <AppUsageSchedulePeriodEntity>[]);
});
ProviderContainer buildContainer(AppUsageSchedulesRepository repo) {
return makeContainer(
overrides: [
appUsageSchedulesRepositoryProvider.overrideWithValue(repo),
],
);
}
group('AppUsageSchedulesController.save', () {
test('persists periods and transitions to AsyncData', () async {
final repo = MockAppUsageSchedulesRepository();
when(
() => repo.upsertSchedules(
identificator: any(named: 'identificator'),
periods: any(named: 'periods'),
),
).thenAnswer((_) async => const [_period]);
final container = buildContainer(repo);
addTearDown(container.dispose);
await container
.read(appUsageSchedulesControllerProvider.notifier)
.save(identificator: 'imei-1', periods: const [_period]);
expect(
container.read(appUsageSchedulesControllerProvider),
isA<AsyncData<void>>(),
);
verify(
() => repo.upsertSchedules(
identificator: 'imei-1',
periods: const [_period],
),
).called(1);
});
test('exposes AsyncError when repository fails', () async {
final repo = MockAppUsageSchedulesRepository();
when(
() => repo.upsertSchedules(
identificator: any(named: 'identificator'),
periods: any(named: 'periods'),
),
).thenThrow(Exception('boom'));
final container = buildContainer(repo);
addTearDown(container.dispose);
await container
.read(appUsageSchedulesControllerProvider.notifier)
.save(identificator: 'imei-1', periods: const [_period]);
expect(
container.read(appUsageSchedulesControllerProvider),
isA<AsyncError<void>>(),
);
});
});
group('AppUsageSchedulesEditor', () {
test('init sets periods', () {
final container = makeContainer();
addTearDown(container.dispose);
expect(container.read(appUsageSchedulesEditorProvider), isNull);
container
.read(appUsageSchedulesEditorProvider.notifier)
.init(const [_period]);
expect(
container.read(appUsageSchedulesEditorProvider),
equals(const [_period]),
);
});
test('add appends period', () {
final container = makeContainer();
addTearDown(container.dispose);
container
.read(appUsageSchedulesEditorProvider.notifier)
.init(const [_period]);
container
.read(appUsageSchedulesEditorProvider.notifier)
.add(_period2);
expect(
container.read(appUsageSchedulesEditorProvider),
equals(const [_period, _period2]),
);
});
test('replace updates period at index', () {
final container = makeContainer();
addTearDown(container.dispose);
container
.read(appUsageSchedulesEditorProvider.notifier)
.init(const [_period, _period2]);
const replacement = AppUsageSchedulePeriodEntity(
periodStart: '10:00',
periodEnd: '16:00',
isPeriodEnabled: true,
);
container
.read(appUsageSchedulesEditorProvider.notifier)
.replace(0, replacement);
expect(
container.read(appUsageSchedulesEditorProvider),
equals(const [replacement, _period2]),
);
});
test('clear resets to null', () {
final container = makeContainer();
addTearDown(container.dispose);
container
.read(appUsageSchedulesEditorProvider.notifier)
.init(const [_period]);
container.read(appUsageSchedulesEditorProvider.notifier).clear();
expect(container.read(appUsageSchedulesEditorProvider), isNull);
});
});
}

View File

@@ -2,6 +2,7 @@ import 'package:device_management/src/core/data/datasources/do_not_disturb_remot
import 'package:device_management/src/core/providers/do_not_disturb_providers.dart';
import 'package:device_management/src/features/do_not_disturb/domain/do_not_disturb_period.dart';
import 'package:device_management/src/features/do_not_disturb/presentation/providers/do_not_disturb_controller.dart';
import 'package:device_management/src/features/do_not_disturb/presentation/providers/do_not_disturb_editor_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@@ -90,5 +91,78 @@ void main() {
isA<AsyncError<void>>(),
);
});
test('sends periods with enabled field to repository', () async {
final ds = MockDoNotDisturbRemoteDatasource();
final disabledPeriod = _period.copyWith(enabled: false);
when(
() => ds.updatePeriods(
identificator: any(named: 'identificator'),
periods: any(named: 'periods'),
),
).thenAnswer((_) async => DoNotDisturbSchedule(
id: 's1',
deviceIdentificator: 'imei-1',
periods: [disabledPeriod],
));
final container = buildContainer(ds);
addTearDown(container.dispose);
await container.read(doNotDisturbControllerProvider.notifier).save(
deviceIdentificator: 'imei-1',
periods: [disabledPeriod],
);
expect(
container.read(doNotDisturbControllerProvider),
isA<AsyncData<void>>(),
);
verify(
() => ds.updatePeriods(
identificator: 'imei-1',
periods: [disabledPeriod],
),
).called(1);
});
});
group('DoNotDisturbPeriod', () {
test('enabled defaults to true', () {
expect(_period.enabled, isTrue);
});
});
group('DoNotDisturbEditor.toggle', () {
test('changes enabled state at given index', () {
final container = makeContainer();
addTearDown(container.dispose);
final editor = container.read(doNotDisturbEditorProvider.notifier);
editor.add(current: const [], period: _period);
editor.toggle(current: const [], index: 0, enabled: false);
final periods = container.read(doNotDisturbEditorProvider);
expect(periods, isNotNull);
expect(periods!.first.enabled, isFalse);
});
test('with enabled false sets period disabled', () {
final container = makeContainer();
addTearDown(container.dispose);
final editor = container.read(doNotDisturbEditorProvider.notifier);
final periods = [_period, _period.copyWith(start: '10:00', end: '12:00')];
editor.add(current: const [], period: periods[0]);
editor.add(current: const [], period: periods[1]);
editor.toggle(current: const [], index: 1, enabled: false);
final result = container.read(doNotDisturbEditorProvider);
expect(result, isNotNull);
expect(result![0].enabled, isTrue);
expect(result[1].enabled, isFalse);
});
});
}

View File

@@ -0,0 +1,110 @@
import 'package:device_management/src/core/domain/repositories/installed_apps_repository.dart';
import 'package:device_management/src/core/providers/installed_apps_repository_provider.dart';
import 'package:device_management/src/features/installed_apps/presentation/providers/installed_apps_controller.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/testing.dart';
import 'package:sf_tracking/sf_tracking.dart';
class MockInstalledAppsRepository extends Mock
implements InstalledAppsRepository {}
void main() {
ProviderContainer buildContainer(InstalledAppsRepository repository) {
return makeContainer(
overrides: [
installedAppsRepositoryProvider.overrideWithValue(repository),
sfTrackingProvider.overrideWithValue(
SfTrackingRepository(clients: const []),
),
],
);
}
group('InstalledAppsController.toggleApp', () {
test('transitions to AsyncData on success', () async {
final repository = MockInstalledAppsRepository();
when(
() => repository.updateInstalledApps(
identificator: any(named: 'identificator'),
apps: any(named: 'apps'),
),
).thenAnswer((_) async => []);
final container = buildContainer(repository);
addTearDown(container.dispose);
await container
.read(installedAppsControllerProvider.notifier)
.toggleApp(
identificator: 'imei-1',
appUid: 'app-1',
isEnabled: false,
);
expect(
container.read(installedAppsControllerProvider),
isA<AsyncData<void>>(),
);
});
test('calls repository with correct parameters', () async {
final repository = MockInstalledAppsRepository();
when(
() => repository.updateInstalledApps(
identificator: any(named: 'identificator'),
apps: any(named: 'apps'),
),
).thenAnswer((_) async => []);
final container = buildContainer(repository);
addTearDown(container.dispose);
await container
.read(installedAppsControllerProvider.notifier)
.toggleApp(
identificator: 'imei-1',
appUid: 'app-1',
isEnabled: false,
);
verify(
() => repository.updateInstalledApps(
identificator: 'imei-1',
apps: [
{'appUid': 'app-1', 'isEnabled': false},
],
),
).called(1);
});
test('exposes AsyncError when repository fails', () async {
final repository = MockInstalledAppsRepository();
when(
() => repository.updateInstalledApps(
identificator: any(named: 'identificator'),
apps: any(named: 'apps'),
),
).thenThrow(
const ApiException(message: 'boom', isNetworkError: true));
final container = buildContainer(repository);
addTearDown(container.dispose);
await container
.read(installedAppsControllerProvider.notifier)
.toggleApp(
identificator: 'imei-1',
appUid: 'app-1',
isEnabled: true,
);
expect(
container.read(installedAppsControllerProvider),
isA<AsyncError<void>>(),
);
});
});
}

View File

@@ -9,6 +9,12 @@ enum _FeedbackKind { error, success, info }
final _queue = Queue<_FeedbackRequest>();
bool _isShowing = false;
@visibleForTesting
void resetFeedbackDialogQueue() {
_queue.clear();
_isShowing = false;
}
class _FeedbackRequest {
final BuildContext context;
final String messageKey;

View File

@@ -0,0 +1,109 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:sf_shared/src/domain/entities/device_entity.dart';
void main() {
group('DeviceEntity.queueCommands', () {
test('defaults to false', () {
final device = DeviceEntity(id: '1', identificator: 'dev-1');
expect(device.queueCommands, isFalse);
});
test('can be set to true', () {
final device = DeviceEntity(
id: '1',
identificator: 'dev-1',
queueCommands: true,
);
expect(device.queueCommands, isTrue);
});
test('can be set to false explicitly', () {
final device = DeviceEntity(
id: '1',
identificator: 'dev-1',
queueCommands: false,
);
expect(device.queueCommands, isFalse);
});
});
group('DeviceEntityFlags.isDisconnected', () {
test('returns true when flags contain isDisconnect as true', () {
final device = DeviceEntity(
id: '1',
identificator: 'dev-1',
flags: {'isDisconnect': true},
);
expect(device.isDisconnected, isTrue);
});
test('returns false when flags contain isDisconnect as false', () {
final device = DeviceEntity(
id: '1',
identificator: 'dev-1',
flags: {'isDisconnect': false},
);
expect(device.isDisconnected, isFalse);
});
test('returns false when flags do not contain isDisconnect', () {
final device = DeviceEntity(
id: '1',
identificator: 'dev-1',
flags: {'someOtherFlag': true},
);
expect(device.isDisconnected, isFalse);
});
test('returns false when flags are empty', () {
final device = DeviceEntity(id: '1', identificator: 'dev-1');
expect(device.isDisconnected, isFalse);
});
});
group('guardDeviceCommand logic (unit-level)', () {
test('queueCommands true and disconnected allows command', () {
final device = DeviceEntity(
id: '1',
identificator: 'dev-1',
queueCommands: true,
flags: {'isDisconnect': true},
);
final shouldBlock = !device.queueCommands && device.isDisconnected;
expect(shouldBlock, isFalse);
});
test('queueCommands false and disconnected blocks command', () {
final device = DeviceEntity(
id: '1',
identificator: 'dev-1',
queueCommands: false,
flags: {'isDisconnect': true},
);
final shouldBlock = !device.queueCommands && device.isDisconnected;
expect(shouldBlock, isTrue);
});
test('queueCommands false and connected allows command', () {
final device = DeviceEntity(
id: '1',
identificator: 'dev-1',
queueCommands: false,
flags: {'isDisconnect': false},
);
final shouldBlock = !device.queueCommands && device.isDisconnected;
expect(shouldBlock, isFalse);
});
test('queueCommands true and connected allows command', () {
final device = DeviceEntity(
id: '1',
identificator: 'dev-1',
queueCommands: true,
flags: {'isDisconnect': false},
);
final shouldBlock = !device.queueCommands && device.isDisconnected;
expect(shouldBlock, isFalse);
});
});
}

View File

@@ -1,36 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_shared/testing.dart';
// Only the error variant is smoke-tested. success/info rely on Timer + Navigator.pop
// and hit cross-test SFLocalizations state, so they're covered by feature integration
// tests (Fase 1+) instead.
void main() {
testWidgets('showErrorDialog displays AlertDialog with an accept button', (tester) async {
await pumpApp(
tester,
withLocalizations: true,
child: Builder(
builder: (ctx) => ElevatedButton(
onPressed: () {},
child: const Text('go'),
),
),
);
await tester.pump();
await tester.pump();
setUp(resetFeedbackDialogQueue);
final ctx = tester.element(find.text('go'));
final future = showErrorDialog(ctx, I18n.errorInvalidCredentials);
await tester.pump();
expect(find.byType(AlertDialog), findsOneWidget);
expect(find.byType(TextButton), findsOneWidget);
await tester.tap(find.byType(TextButton));
await tester.pump();
await future;
test('resetFeedbackDialogQueue clears state', () {
resetFeedbackDialogQueue();
});
}