// State-matrix sweep — every sidebar page against every hub // condition, with the app-wide invariants that single-page tests // keep missing: // // 1. healthy-empty -> no page may claim "hub not reachable", // no raw error text anywhere, and the fake // must cover every HubService member the // pages touch (hermeticity guarantee). // 2. hub gone -> the data pages show the honest // unreachable state; still no raw errors. // 3. hub too old -> RPCs answer UNIMPLEMENTED: NO page may // say "not reachable" while the sidebar // shows connected (the runs-page bug class, // now pinned for every page). // 4. feature off -> the detached gate error renders the // plain-language feature-off empty state. // // The suite runs against the scriptable FakeHubService — never a // real hub — so results cannot depend on the operator's machine. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:chain_studio/data/hub.dart'; import 'package:chain_studio/main.dart'; import 'support/fake_hub.dart'; const _destinations = [ 'welcome', 'store', 'doctor', 'flows', 'audit', 'approvals', 'runs', 'federation', ]; /// Substrings that must NEVER appear in rendered text — they mean /// a raw thrown object reached the UI instead of a classified, /// localised state. const _rawErrorMarkers = [ 'gRPC Error', 'UnimplementedError', 'SocketException', 'Instance of', 'StackTrace', ]; /// Page-primary read RPCs — the ones an older hub would be /// missing. Scenario 3 fails exactly these with UNIMPLEMENTED. const _pageReads = { 'doctor', 'searchStore', 'recentEvents', 'streamEvents', 'pendingApprovals', 'listApprovalsRecords', 'listDetachedRuns', 'listSatellites', }; String _renderedText(WidgetTester tester) { final buf = StringBuffer(); for (final w in tester.widgetList(find.byType(Text))) { buf.writeln(w.data ?? w.textSpan?.toPlainText() ?? ''); } for (final w in tester.widgetList( find.byType(SelectableText), )) { buf.writeln(w.data ?? ''); } return buf.toString(); } Future _boot(WidgetTester tester) async { SharedPreferences.setMockInitialValues({}); tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); await tester.pumpWidget( const StudioApp( initialThemeMode: ThemeModeValue.dark, initialLocale: Locale('de'), ), ); await tester.pump(const Duration(milliseconds: 100)); } Future _goto(WidgetTester tester, String id) async { await tester.tap(find.byKey(ValueKey('sidebar-item-$id'))); await tester.pump(const Duration(milliseconds: 400)); await tester.pump(const Duration(milliseconds: 400)); } Future _teardownApp(WidgetTester tester) async { await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(const Duration(minutes: 1)); } void main() { testWidgets('healthy hub: no unreachable claims, no raw errors, ' 'fake covers all members', (tester) async { final fake = installFakeHub(); await _boot(tester); final violations = []; for (final id in _destinations) { await _goto(tester, id); final text = _renderedText(tester); if (text.contains('nicht erreichbar')) { violations.add('page "$id" claims unreachable on a healthy hub'); } for (final marker in _rawErrorMarkers) { if (text.contains(marker)) { violations.add('page "$id" renders raw error text "$marker"'); } } } await _teardownApp(tester); expect( fake.missing, isEmpty, reason: 'pages touched HubService members the fake does not ' 'implement — add healthy defaults for: ${fake.missing}', ); expect(violations, isEmpty, reason: violations.join('\n')); }); testWidgets('hub gone: data pages say unreachable, never raw errors', ( tester, ) async { final fake = installFakeHub(); fake.failWith(kUnavailable); await _boot(tester); final violations = []; // Pages that render a full-page load state from the hub. for (final id in ['store', 'doctor', 'audit', 'approvals', 'runs', 'federation']) { await _goto(tester, id); final text = _renderedText(tester); if (!text.contains('nicht erreichbar')) { violations.add('page "$id" hides that the hub is unreachable'); } for (final marker in _rawErrorMarkers) { if (text.contains(marker)) { violations.add('page "$id" renders raw error text "$marker"'); } } } await _teardownApp(tester); expect(violations, isEmpty, reason: violations.join('\n')); }); testWidgets('hub too old (UNIMPLEMENTED): connected is never shown ' 'as unreachable', (tester) async { final fake = installFakeHub(); fake.failWith(kUnimplemented, only: _pageReads); await _boot(tester); final violations = []; for (final id in _destinations) { await _goto(tester, id); final text = _renderedText(tester); // THE invariant: the hub answered, so no page may // contradict the sidebar's connected state. if (text.contains('nicht erreichbar')) { violations.add( 'page "$id" claims unreachable although the hub answered ' '(UNIMPLEMENTED)', ); } for (final marker in _rawErrorMarkers) { if (text.contains(marker)) { violations.add('page "$id" renders raw error text "$marker"'); } } } // Spot-check the classified rendition on an affected page. await _goto(tester, 'doctor'); expect( find.textContaining('neuere Hub-Version'), findsWidgets, reason: 'the too-old state must say so in plain language', ); await _teardownApp(tester); expect(violations, isEmpty, reason: violations.join('\n')); }); testWidgets('detached gate error renders the feature-off state', ( tester, ) async { final fake = installFakeHub(); fake.failWith(kDetachedOff, only: {'listDetachedRuns'}); await _boot(tester); await _goto(tester, 'runs'); expect(find.text('Keine Läufe im Hintergrund'), findsOneWidget); expect(find.textContaining('nicht erreichbar'), findsNothing); await _teardownApp(tester); }); }