feat(doctor): deep-link findings to the page where they are actioned

Doctor findings used to be dead-end statements — 'approvals
waiting for review' left the operator to find the approvals inbox
on their own. Every finding with a dedicated surface is now one
tap away from it:

- summary tiles: modules -> store, approvals -> approvals inbox,
  audit chain -> audit log (chevron affordance, tooltip + semantics
  button; the services tile stays plain — no dedicated page)
- modules/approvals panel rows link the same way
- the event-log headline opens the audit page next to the
  existing verify button
- host-services empty state gains a 'view the configuration'
  button opening the in-Studio config viewer the hint refers to
- the update banner's release-notes URL is now an underlined,
  clickable link instead of dead text

Tiles and the panel are public callback-driven widgets so the
widget tests pump them without a live hub. New DE+EN link labels.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-18 18:02:04 +02:00
parent c18bb7f357
commit e67e0c9e73
7 changed files with 525 additions and 139 deletions

View file

@ -0,0 +1,122 @@
// Doctor deep links findings with a dedicated page must be one
// tap away from it (usertest follow-up: "approvals waiting for
// review" was a dead-end statement). The tiles and panel rows are
// public callback-driven widgets so these tests pump each variant
// without a live hub.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chain_studio/data/hub.dart';
import 'package:chain_studio/l10n/app_localizations.dart';
import 'package:chain_studio/pages/doctor.dart';
const _snapshot = DoctorSnapshot(
moduleCount: 2,
capabilityCount: 3,
pendingApprovals: 1,
eventChainTotal: 5,
eventChainVerified: 5,
eventChainTamperedAt: null,
services: [],
update: UpdateStatus(
channel: 'stable',
localVersion: '0.22.0',
latestVersion: '0.22.0',
updateAvailable: false,
manifestReachable: true,
),
paths: DaemonPathsSnapshot(
logPath: '',
dbPath: '',
modulesDir: '',
flowsDir: '',
configPath: '',
pidPath: '',
),
);
Widget _host(Widget child) => MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('de'),
home: Scaffold(body: child),
);
void main() {
testWidgets('a linked stat tile is a labelled button that fires its target', (
tester,
) async {
var opened = false;
await tester.pumpWidget(
_host(
Row(
children: [
DoctorStatTile(
label: 'Freigaben',
value: '1',
subtitle: 'warten auf Prüfung',
icon: Icons.inbox_outlined,
onTap: () => opened = true,
linkLabel: 'Freigaben öffnen',
),
],
),
),
);
await tester.pumpAndSettle();
// Affordance: chevron + semantics button with the link label.
expect(find.byIcon(Icons.chevron_right), findsOneWidget);
expect(
find.bySemanticsLabel(RegExp('Freigaben öffnen')),
findsOneWidget,
);
await tester.tap(find.text('1'));
expect(opened, isTrue);
});
testWidgets('an unlinked stat tile stays a plain card', (tester) async {
await tester.pumpWidget(
_host(
const Row(
children: [
DoctorStatTile(
label: 'Dienste',
value: '0',
subtitle: 'deklariert',
icon: Icons.dns_outlined,
),
],
),
),
);
await tester.pumpAndSettle();
expect(find.byIcon(Icons.chevron_right), findsNothing);
expect(find.byType(InkWell), findsNothing);
});
testWidgets(
'the approvals row links to approvals, the modules row to the store',
(tester) async {
var storeOpened = false;
var approvalsOpened = false;
await tester.pumpWidget(
_host(
DoctorModulesPanel(
snapshot: _snapshot,
onOpenStore: () => storeOpened = true,
onOpenApprovals: () => approvalsOpened = true,
),
),
);
await tester.pumpAndSettle();
// The pending-approvals finding must be tappable ("1 Freigabe
// wartet auf Prüfung" -> approvals inbox).
await tester.tap(find.textContaining('wartet auf Prüfung'));
expect(approvalsOpened, isTrue);
expect(storeOpened, isFalse);
await tester.tap(find.textContaining('2 Module'));
expect(storeOpened, isTrue);
},
);
}