fix(approvals): usertest-panel hardening (0.81.0)
Panel findings against the reworked approvals page, fixed in place: - Never fabricate the request time: ApprovalRecord.createdAt is nullable now; a missing created_at omits the line instead of rendering DateTime.now() (which drifted on refresh). Guard: approvals_origin_test pins the omit-on-null invariant. - Copyable errors on approve/reject/batch via showChainErrorSnack (the hard project rule) — batch surfaces the first real cause. - Reject requires a reason: ChainInlineHelp strip + confirm disabled while empty, no more silent close-and-nothing-happens. - Batch approve applies the same no-data confirmation as the single path, naming how many selected requests carry no show: data. - Plainer language: glossary "Vorgang (Flow)", history label FRAGE (was PROMPT), no-data hint drops developer jargon. - One-click copy of the run id; history payload pretty-prints like the card. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
28f6fe1a9a
commit
f7d7427d91
9 changed files with 411 additions and 122 deletions
|
|
@ -5,6 +5,10 @@
|
|||
// the Dart mapping layer; this pins that it reaches the card, next
|
||||
// to the rest of the origin facts and the plain-language intro.
|
||||
//
|
||||
// It also pins the honesty invariant from the usertest: a missing
|
||||
// created_at must NOT be rendered as a fabricated "now" — the line
|
||||
// is simply omitted, exactly like the run id on a legacy hub.
|
||||
//
|
||||
// Runs against the scriptable FakeHubService — never a real hub.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -31,46 +35,57 @@ String _allText(WidgetTester tester) {
|
|||
return buf.toString();
|
||||
}
|
||||
|
||||
ApprovalRecord _pending({DateTime? createdAt, String? runId = 'run-abc123'}) {
|
||||
return ApprovalRecord(
|
||||
id: 'apr-1',
|
||||
flowName: 'classify-and-file',
|
||||
stepId: 'review',
|
||||
prompt: 'Bitte die Klassifikation prüfen',
|
||||
payloadPreview: '{"label":"Rechnung"}',
|
||||
createdAt: createdAt,
|
||||
expiresAt: null,
|
||||
status: 'pending',
|
||||
decidedAt: null,
|
||||
decidedBy: '',
|
||||
reason: '',
|
||||
project: 'lbs',
|
||||
flowExecution: runId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _renderApprovals(
|
||||
WidgetTester tester,
|
||||
List<ApprovalRecord> approvals,
|
||||
) async {
|
||||
final fake = installFakeHub();
|
||||
fake.approvals = approvals;
|
||||
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);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pump(const Duration(minutes: 1));
|
||||
return text;
|
||||
}
|
||||
|
||||
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);
|
||||
final text = await _renderApprovals(tester, [
|
||||
_pending(createdAt: DateTime.utc(2026, 7, 26, 12, 0, 0)),
|
||||
]);
|
||||
// The human question (headline).
|
||||
expect(text, contains('Bitte die Klassifikation prüfen'));
|
||||
// The origin block — every fact a reviewer needs to place it.
|
||||
|
|
@ -82,9 +97,29 @@ void main() {
|
|||
// 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'));
|
||||
expect(text, contains('pausierte'));
|
||||
});
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pump(const Duration(minutes: 1));
|
||||
testWidgets('a missing requested-at time is omitted, never faked as '
|
||||
'"now"', (tester) async {
|
||||
// created_at unknown: the honest behaviour is to drop the line,
|
||||
// not to invent a timestamp (which would even drift on refresh).
|
||||
final text = await _renderApprovals(tester, [_pending(createdAt: null)]);
|
||||
// The rest of the origin still renders.
|
||||
expect(text, contains('HERKUNFT'));
|
||||
expect(text, contains('Flow: classify-and-file'));
|
||||
expect(text, contains('Lauf: run-abc123'));
|
||||
// But no requested-at line at all.
|
||||
expect(text, isNot(contains('Angefordert:')));
|
||||
});
|
||||
|
||||
testWidgets('a legacy approval without a run id omits the run line', (
|
||||
tester,
|
||||
) async {
|
||||
final text = await _renderApprovals(tester, [
|
||||
_pending(createdAt: DateTime.utc(2026, 7, 26, 12, 0, 0), runId: null),
|
||||
]);
|
||||
expect(text, contains('HERKUNFT'));
|
||||
expect(text, isNot(contains('Lauf:')));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue