feat: configurable menu bar position for mailbox view (#298) #303
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -124,7 +125,6 @@ class _AccountTile extends ConsumerWidget {
|
||||
if (h == null) return const Text('Sync health: Not verified yet');
|
||||
final date = h.lastVerifiedAt.toLocal().toString().split('.')[0];
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Sync health: '),
|
||||
Icon(
|
||||
@@ -133,7 +133,13 @@ class _AccountTile extends ConsumerWidget {
|
||||
color: h.isHealthy ? Colors.green : Colors.orange,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(h.isHealthy ? 'Healthy' : 'Discrepancies found'),
|
||||
Flexible(
|
||||
child: Text(
|
||||
h.isHealthy
|
||||
? 'Healthy'
|
||||
: _formatDiscrepancies(h.discrepancySummary),
|
||||
),
|
||||
),
|
||||
Text(' ($date)', style: const TextStyle(fontSize: 10)),
|
||||
],
|
||||
);
|
||||
@@ -293,6 +299,30 @@ class _AccountTile extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDiscrepancies(String? summary) {
|
||||
if (summary == null) return 'Discrepancies found';
|
||||
try {
|
||||
final decoded = jsonDecode(summary) as Map<String, dynamic>;
|
||||
var missingLocally = 0;
|
||||
var missingOnServer = 0;
|
||||
var flagMismatches = 0;
|
||||
for (final v in decoded.values) {
|
||||
final m = v as Map<String, dynamic>;
|
||||
missingLocally += (m['missingLocally'] as int? ?? 0);
|
||||
missingOnServer += (m['missingOnServer'] as int? ?? 0);
|
||||
flagMismatches += (m['flagMismatches'] as int? ?? 0);
|
||||
}
|
||||
final parts = <String>[];
|
||||
if (missingLocally > 0) parts.add('missing locally: $missingLocally');
|
||||
if (missingOnServer > 0) parts.add('missing on server: $missingOnServer');
|
||||
if (flagMismatches > 0) parts.add('flag mismatches: $flagMismatches');
|
||||
if (parts.isEmpty) return 'Discrepancies found';
|
||||
return 'Discrepancies found (${parts.join(', ')})';
|
||||
} catch (_) {
|
||||
return 'Discrepancies found';
|
||||
}
|
||||
}
|
||||
|
||||
class _OnboardingView extends StatelessWidget {
|
||||
const _OnboardingView();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sharedinbox/data/db/database.dart' show SyncHealthRow;
|
||||
|
||||
import 'helpers.dart';
|
||||
|
||||
@@ -206,5 +207,50 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('sharedinbox.de'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows Healthy when sync health is healthy', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
buildApp(
|
||||
initialLocation: '/accounts',
|
||||
overrides: baseOverrides(
|
||||
accounts: [kTestAccount],
|
||||
syncHealth: SyncHealthRow(
|
||||
accountId: kTestAccount.id,
|
||||
lastVerifiedAt: DateTime(2024, 6),
|
||||
isHealthy: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Healthy'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'shows discrepancy details when sync health has discrepancies',
|
||||
(tester) async {
|
||||
const summary =
|
||||
'{"INBOX":{"missingLocally":3,"missingOnServer":0,"flagMismatches":1}}';
|
||||
await tester.pumpWidget(
|
||||
buildApp(
|
||||
initialLocation: '/accounts',
|
||||
overrides: baseOverrides(
|
||||
accounts: [kTestAccount],
|
||||
syncHealth: SyncHealthRow(
|
||||
accountId: kTestAccount.id,
|
||||
lastVerifiedAt: DateTime(2024, 6),
|
||||
isHealthy: false,
|
||||
discrepancySummary: summary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('missing locally: 3'), findsOneWidget);
|
||||
expect(find.textContaining('flag mismatches: 1'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import 'package:sharedinbox/core/services/account_discovery_service.dart';
|
||||
import 'package:sharedinbox/core/services/connection_test_service.dart';
|
||||
import 'package:sharedinbox/core/services/managesieve_probe_service.dart';
|
||||
import 'package:sharedinbox/core/services/share_encryption_service.dart';
|
||||
import 'package:sharedinbox/data/db/database.dart' show SyncHealthRow;
|
||||
import 'package:sharedinbox/di.dart';
|
||||
import 'package:sharedinbox/ui/screens/account_list_screen.dart';
|
||||
import 'package:sharedinbox/ui/screens/account_receive_screen.dart';
|
||||
@@ -505,13 +506,12 @@ Widget buildApp({
|
||||
return ProviderScope(
|
||||
// Defaults come first so tests can override them via [overrides].
|
||||
//
|
||||
// syncHealthProvider and syncLogRepositoryProvider are backed by Drift
|
||||
// StreamQueries. When a StreamProvider that wraps a Drift query is disposed,
|
||||
// Drift schedules a Timer.run() for cache debouncing. Flutter's test
|
||||
// framework then fails the test with "A Timer is still pending". Replacing
|
||||
// these with simple synchronous streams avoids the pending-timer assertion.
|
||||
// syncLogRepositoryProvider is backed by a Drift StreamQuery. When the
|
||||
// provider is disposed, Drift schedules a Timer.run() for cache
|
||||
// debouncing. Flutter's test framework then fails the test with "A Timer
|
||||
// is still pending". Replacing it with a synchronous stream avoids this.
|
||||
// syncHealthProvider has the same issue and is overridden in baseOverrides.
|
||||
overrides: [
|
||||
syncHealthProvider.overrideWith((ref, _) => Stream.value(null)),
|
||||
syncLogRepositoryProvider.overrideWithValue(
|
||||
const NoOpSyncLogRepository(),
|
||||
),
|
||||
@@ -541,6 +541,7 @@ List<Override> baseOverrides({
|
||||
Exception? connectionError,
|
||||
ShareKeyRepository? shareKeyRepository,
|
||||
bool hasStoredPassword = true,
|
||||
SyncHealthRow? syncHealth,
|
||||
}) =>
|
||||
[
|
||||
accountRepositoryProvider.overrideWithValue(
|
||||
@@ -559,6 +560,9 @@ List<Override> baseOverrides({
|
||||
shareKeyRepositoryProvider.overrideWithValue(
|
||||
shareKeyRepository ?? FakeShareKeyRepository(),
|
||||
),
|
||||
// syncHealthProvider is backed by a Drift StreamQuery; override with a
|
||||
// plain stream to avoid "A Timer is still pending" in tests.
|
||||
syncHealthProvider.overrideWith((ref, _) => Stream.value(syncHealth)),
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user