// Approvals origin block — a reviewer must be able to see WHERE a // pending approval comes from (which flow, step, run, project, and // when it was requested) and WHAT approve / reject will do, without // leaving the card. The run id in particular used to be dropped at // the Dart mapping layer; this pins that it reaches the card, next // to the rest of the origin facts and the plain-language intro. // // Runs against the scriptable FakeHubService — never a real hub. 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'; /// Every rendered Text + SelectableText joined — the run id is a /// SelectableText (copyable), so a Text-only sweep would miss it. String _allText(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(); } void main() { testWidgets('a pending approval card shows its full origin and an ' 'intro explaining approve / reject', (tester) async { final fake = installFakeHub(); fake.approvals = [ ApprovalRecord( id: 'apr-1', flowName: 'classify-and-file', stepId: 'review', prompt: 'Bitte die Klassifikation prüfen', payloadPreview: '{"label":"Rechnung"}', createdAt: DateTime.utc(2026, 7, 26, 12, 0, 0), expiresAt: null, status: 'pending', decidedAt: null, decidedBy: '', reason: '', project: 'lbs', flowExecution: 'run-abc123', ), ]; SharedPreferences.setMockInitialValues({}); tester.view.physicalSize = const Size(1280, 900); 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)); await tester.tap(find.byKey(const ValueKey('sidebar-item-approvals'))); for (var i = 0; i < 12; i++) { await tester.pump(const Duration(milliseconds: 200)); } final text = _allText(tester); // The human question (headline). expect(text, contains('Bitte die Klassifikation prüfen')); // The origin block — every fact a reviewer needs to place it. expect(text, contains('HERKUNFT')); expect(text, contains('Flow: classify-and-file')); expect(text, contains('Schritt: review')); expect(text, contains('Projekt: lbs')); expect(text, contains('Angefordert:')); // The run id — the field that used to be dropped at the mapping. expect(text, contains('Lauf: run-abc123')); // The intro strip says what this inbox is and what the actions do. expect(text, contains('pausierte Flows')); await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(const Duration(minutes: 1)); }); }