Approvals: the pending card now shows its full origin — flow, step, run id (previously dropped at the Dart mapping layer), project, and requested-at — under an ORIGIN heading, led by a one-line intro strip that says what the inbox is and what Approve/Reject do. Approve/Reject buttons carry tooltips; the history dialog gains project + run id. Fixes the approvals doc drift (title/details/reviewer -> prompt/show/timeout_seconds). Guard: approvals_origin_test renders the card via the hermetic fake hub and pins every origin fact. Runs: the "hub too old" state now leads with an in-place update button (same `chain update apply` path as the Diagnose page), the Diagnose deeplink demoted to secondary, with a CLI-absent fallback. Guard: two new RunsLoadErrorView widget tests. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
90 lines
3.2 KiB
Dart
90 lines
3.2 KiB
Dart
// 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<Text>(find.byType(Text))) {
|
|
buf.writeln(w.data ?? w.textSpan?.toPlainText() ?? '');
|
|
}
|
|
for (final w in tester.widgetList<SelectableText>(
|
|
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));
|
|
});
|
|
}
|