feat: type remote device models

This commit is contained in:
Artem Kokos
2026-04-23 20:44:51 +07:00
parent 736a61d54b
commit fa403bfcce
9 changed files with 619 additions and 189 deletions

View File

@@ -2,10 +2,13 @@ import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ignis_app/app/load_state.dart';
import 'package:ignis_app/models/ignis_group.dart';
import 'package:ignis_app/providers/providers.dart';
import 'package:ignis_app/services/api_client.dart';
class FakeIgnisApi extends IgnisApi {
Object? groupsData;
Object? groupStatusData;
Object? devicesData;
Object? scenesData;
Object? tasksData;
@@ -18,6 +21,8 @@ class FakeIgnisApi extends IgnisApi {
Object? statsError;
Object? eventLogError;
Object? apiKeysError;
Object? groupsError;
Object? groupStatusError;
Object? controlGroupError;
Object? cancelTaskError;
Object? revokeApiKeyError;
@@ -29,6 +34,8 @@ class FakeIgnisApi extends IgnisApi {
String? revokedApiKey;
FakeIgnisApi({
this.groupsData,
this.groupStatusData,
this.devicesData,
this.scenesData,
this.tasksData,
@@ -59,9 +66,29 @@ class FakeIgnisApi extends IgnisApi {
@override
Future<Response> getGroups() async {
final error = groupsError;
if (error != null) throw error;
return Response(
requestOptions: RequestOptions(path: '/devices/groups'),
data: <Object>[],
data: groupsData ?? <Object>[],
);
}
@override
Future<Response> getGroupStatus(String id) async {
final error = groupStatusError;
if (error != null) throw error;
return Response(
requestOptions: RequestOptions(path: '/control/group/$id/status'),
data:
groupStatusData ??
{
'results': [
{
'status': {'state': false, 'dimming': 100, 'temp': 4000},
},
],
},
);
}
@@ -169,6 +196,20 @@ void main() {
expect(api.requestedDays, 14);
});
test('group status parser maps backend status shape', () {
final state = IgnisGroupState.firstFromStatusResponse({
'results': [
{
'status': {'state': true, 'dimming': 42, 'temp': 3000},
},
],
});
expect(state?.isOn, isTrue);
expect(state?.brightness, 42);
expect(state?.temp, 3000);
});
test('devices load exposes data state', () async {
final api = FakeIgnisApi(
devicesData: {
@@ -184,6 +225,8 @@ void main() {
final state = container.read(devicesProvider);
expect(state.status, LoadStatus.data);
expect(state.data, hasLength(1));
expect(state.data.single.groupMemberId, 'AA:BB');
expect(state.data.single.name, 'Kitchen bulb');
});
test('devices load exposes empty state', () async {
@@ -309,7 +352,25 @@ void main() {
final state = container.read(scenesProvider);
expect(state.status, LoadStatus.data);
expect(state.data, hasLength(2));
expect(state.data.first, containsPair('id', 'party'));
expect(state.data.first.id, 'party');
expect(state.data.first.displayName, 'Party');
});
test('scenes load maps numeric scene ids to display names', () async {
final api = FakeIgnisApi(
scenesData: {
'scenes': ['1', '4'],
},
);
final container = containerWith(api);
await container.read(scenesProvider.notifier).load();
final state = container.read(scenesProvider);
expect(state.status, LoadStatus.data);
expect(state.data.first.id, '1');
expect(state.data.first.displayName, 'Океан');
expect(state.data.last.displayName, 'Вечеринка');
});
test('scenes load exposes empty state', () async {
@@ -418,6 +479,44 @@ void main() {
expect(state.errorMessage, contains('Backend недоступен'));
});
test('groups refresh maps groups and status to typed state', () async {
final api = FakeIgnisApi(
groupsData: {
'kitchen': {
'name': 'Kitchen',
'macs': ['AA:BB'],
},
},
groupStatusData: {
'results': [
{
'status': {
'state': true,
'dimming': 42,
'temp': 3000,
'r': 1,
'g': 2,
'b': 3,
'scene': '4',
},
},
],
},
);
final container = containerWith(api);
await container.read(groupsProvider.notifier).refresh();
final groups = container.read(groupsProvider);
expect(groups, hasLength(1));
expect(groups.single.id, 'kitchen');
expect(groups.single.name, 'Kitchen');
expect(groups.single.macs, ['AA:BB']);
expect(groups.single.state.isOn, isTrue);
expect(groups.single.state.brightness, 42);
expect(groups.single.state.sceneId, '4');
});
test('task cancel error is not swallowed', () async {
final api = FakeIgnisApi(tasksData: <Object>[]);
final container = containerWith(api);