connection_screen_test.dart 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import 'dart:convert';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_test/flutter_test.dart';
  4. import 'package:http/http.dart' as http;
  5. import 'package:http/testing.dart';
  6. import 'package:rackpeek_mobile/data/settings_store.dart';
  7. import 'package:rackpeek_mobile/screens/connection_screen.dart';
  8. import 'support/memory_storage.dart';
  9. void main() {
  10. testWidgets('API key field can be shown and hidden', (tester) async {
  11. await tester.pumpWidget(
  12. MaterialApp(home: ConnectionScreen(settings: SettingsStore())),
  13. );
  14. final apiKeyField = find.byType(TextField).at(1);
  15. expect(tester.widget<TextField>(apiKeyField).obscureText, isTrue);
  16. await tester.tap(find.byTooltip('Show API key'));
  17. await tester.pump();
  18. expect(tester.widget<TextField>(apiKeyField).obscureText, isFalse);
  19. await tester.tap(find.byTooltip('Hide API key'));
  20. await tester.pump();
  21. expect(tester.widget<TextField>(apiKeyField).obscureText, isTrue);
  22. });
  23. testWidgets('skip opens a locally stored inventory without a server', (
  24. tester,
  25. ) async {
  26. final settings = SettingsStore(storage: MemoryKeyValueStorage());
  27. await tester.pumpWidget(
  28. MaterialApp(home: ConnectionScreen(settings: settings)),
  29. );
  30. await tester.tap(find.text('Skip and use offline'));
  31. await tester.pump();
  32. await tester.pump(const Duration(milliseconds: 500));
  33. expect(
  34. find.text('Offline mode · changes stay on this device'),
  35. findsNothing,
  36. );
  37. expect(find.text('Infrastructure'), findsOneWidget);
  38. expect(await settings.isLocalMode(), isTrue);
  39. });
  40. for (final upload in [true, false]) {
  41. testWidgets(
  42. upload
  43. ? 'connecting offers to upload local changes'
  44. : 'connecting offers to discard local changes',
  45. (tester) async {
  46. final settings = SettingsStore(storage: MemoryKeyValueStorage());
  47. await settings.saveOfflineState({
  48. 'document': {
  49. 'version': 5,
  50. 'resources': [
  51. {'kind': 'Server', 'name': 'nas01', 'model': 'Local NAS'},
  52. ],
  53. 'connections': [],
  54. },
  55. 'pendingChanges': [
  56. {'kind': 'Server', 'name': 'nas01', 'model': 'Local NAS'},
  57. ],
  58. });
  59. final posted = <Map<String, dynamic>>[];
  60. final serverDocument = <String, dynamic>{
  61. 'version': 5,
  62. 'resources': <Map<String, dynamic>>[
  63. {'kind': 'Server', 'name': 'server-copy', 'model': 'Remote NAS'},
  64. ],
  65. 'connections': [],
  66. };
  67. final client = MockClient((request) async {
  68. if (request.url.path == '/api/status') {
  69. return http.Response(
  70. jsonEncode({'apiVersion': '1', 'appVersion': 'test'}),
  71. 200,
  72. );
  73. }
  74. if (request.method == 'POST' &&
  75. request.url.path == '/api/inventory') {
  76. final payload = jsonDecode(request.body) as Map<String, dynamic>;
  77. posted.add(payload);
  78. final document = payload['json'] as Map<String, dynamic>;
  79. (serverDocument['resources'] as List).addAll(
  80. (document['resources'] as List).cast<Map<String, dynamic>>(),
  81. );
  82. return http.Response('{}', 200);
  83. }
  84. if (request.url.path == '/api/inventory/summary') {
  85. return http.Response(
  86. jsonEncode({
  87. 'hardware': {'totalHardware': 0, 'hardwareByKind': {}},
  88. 'systems': {
  89. 'totalSystems': 0,
  90. 'systemsByType': {},
  91. 'systemsByOs': {},
  92. },
  93. 'services': {'totalServices': 0, 'totalIpAddresses': 0},
  94. 'tags': {},
  95. 'labels': {},
  96. }),
  97. 200,
  98. );
  99. }
  100. if (request.url.path == '/api/inventory') {
  101. return http.Response(jsonEncode(serverDocument), 200);
  102. }
  103. return http.Response('{}', 404);
  104. });
  105. await tester.pumpWidget(
  106. MaterialApp(
  107. home: ConnectionScreen(settings: settings, httpClient: client),
  108. ),
  109. );
  110. await tester.enterText(
  111. find.byType(TextField).first,
  112. 'http://rackpeek.test',
  113. );
  114. await tester.enterText(find.byType(TextField).at(1), 'test-key');
  115. await tester.tap(find.text('Connect'));
  116. await tester.pump();
  117. await tester.pump(const Duration(milliseconds: 500));
  118. expect(find.text('Local changes found'), findsOneWidget);
  119. await tester.tap(
  120. find.text(upload ? 'Upload to server' : 'Discard local changes'),
  121. );
  122. await tester.pump();
  123. await tester.pump(const Duration(milliseconds: 500));
  124. expect(posted.length, upload ? 1 : 0);
  125. final offlineState = await settings.loadOfflineState();
  126. expect(offlineState!['pendingChanges'], isEmpty);
  127. final resources = offlineState['document']['resources'] as List;
  128. expect(
  129. resources.any(
  130. (resource) =>
  131. resource['name'] == (upload ? 'nas01' : 'server-copy'),
  132. ),
  133. isTrue,
  134. );
  135. expect((await settings.load())!.baseUrl, 'http://rackpeek.test');
  136. await tester.pumpWidget(const SizedBox.shrink());
  137. client.close();
  138. },
  139. );
  140. }
  141. }