Compare commits
No commits in common. "87afa4dc056493358cb26c8172367d45f250a935" and "588f4373956ddbd88655e48e04a300b691252f47" have entirely different histories.
87afa4dc05
...
588f437395
15 changed files with 15 additions and 616 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -6,24 +6,6 @@ lockstep.
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added — explain it where it happens (0.76.0)
|
|
||||||
|
|
||||||
- **In-place help pattern.** New `ChainInlineHelp` (a one-sentence
|
|
||||||
intro strip that says what a surface is and what will happen, with
|
|
||||||
an optional "Learn more" link into the doc sheet) and
|
|
||||||
`ChainFieldHelp` / `ChainFieldLabel` (a "?" affordance for a single
|
|
||||||
field). First applied to the add-satellite dialog, which used to
|
|
||||||
ask for a "name" with no hint of what a satellite even is
|
|
||||||
(usertest): it now leads with a plain explanation + a federation
|
|
||||||
"Learn more", and the name field carries a "?".
|
|
||||||
- **Page help opens the RIGHT topic.** The Federation and Runs help
|
|
||||||
buttons silently fell back to the architecture doc — `federation`
|
|
||||||
and `runs` had markdown assets but no catalog entry, so
|
|
||||||
`showFaiDoc` resolved them to the first doc. Both are registered
|
|
||||||
now; a new `doc_help_wiring_test.dart` asserts every
|
|
||||||
`showFaiDoc('slug')` call has a registered entry AND both locale
|
|
||||||
assets, so a help button can never open the wrong topic again.
|
|
||||||
|
|
||||||
### Security (0.75.0)
|
### Security (0.75.0)
|
||||||
|
|
||||||
- **Sealed-area names are confidential by default.** The workspace
|
- **Sealed-area names are confidential by default.** The workspace
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
// Dialog capture harness — visual proof for dialogs the page-level
|
|
||||||
// guide harness doesn't reach. Renders the target dialog against
|
|
||||||
// the scriptable fake hub and writes a PNG per theme via a
|
|
||||||
// RepaintBoundary (driverless, headed macOS `flutter test`).
|
|
||||||
//
|
|
||||||
// flutter test integration_test/dialog_shots_test.dart -d macos
|
|
||||||
//
|
|
||||||
// Output: build/dialog-shots/<name>-<theme>.png (or $DIALOG_SHOTS_OUT).
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:integration_test/integration_test.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
import 'package:chain_studio/l10n/app_localizations.dart';
|
|
||||||
import 'package:chain_studio/pages/federation.dart';
|
|
||||||
|
|
||||||
import '../test/support/fake_hub.dart';
|
|
||||||
|
|
||||||
final GlobalKey _shotKey = GlobalKey();
|
|
||||||
|
|
||||||
String get _outDir =>
|
|
||||||
Platform.environment['DIALOG_SHOTS_OUT'] ?? 'build/dialog-shots';
|
|
||||||
|
|
||||||
Future<void> _shot(WidgetTester tester, String name) async {
|
|
||||||
await tester.pump(const Duration(milliseconds: 150));
|
|
||||||
await tester.pump(const Duration(milliseconds: 150));
|
|
||||||
final boundary =
|
|
||||||
_shotKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
|
|
||||||
final image = await boundary.toImage(pixelRatio: 2.0);
|
|
||||||
final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
||||||
image.dispose();
|
|
||||||
final file = File('$_outDir/$name.png');
|
|
||||||
file.parent.createSync(recursive: true);
|
|
||||||
file.writeAsBytesSync(bytes!.buffer.asUint8List());
|
|
||||||
// ignore: avoid_print
|
|
||||||
print('dialog-shot: ${file.path}');
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
||||||
|
|
||||||
for (final (themeName, mode) in [
|
|
||||||
('light', ThemeMode.light),
|
|
||||||
('dark', ThemeMode.dark),
|
|
||||||
]) {
|
|
||||||
testWidgets('add-satellite dialog — $themeName', (tester) async {
|
|
||||||
SharedPreferences.setMockInitialValues({});
|
|
||||||
installFakeHub();
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
themeMode: mode,
|
|
||||||
theme: ThemeData.light(useMaterial3: true),
|
|
||||||
darkTheme: ThemeData.dark(useMaterial3: true),
|
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
|
||||||
locale: const Locale('de'),
|
|
||||||
// Wrap the whole navigator (incl. the overlay dialogs
|
|
||||||
// render into) so the capture catches the dialog, not
|
|
||||||
// just the page beneath it.
|
|
||||||
builder: (context, child) =>
|
|
||||||
RepaintBoundary(key: _shotKey, child: child),
|
|
||||||
home: const FederationPage(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
|
||||||
await tester.tap(find.text('Satellit hinzufügen').first);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await _shot(tester, 'add-satellite-$themeName');
|
|
||||||
expect(find.textContaining('Ein Satellit ist ein weiterer Hub'),
|
|
||||||
findsOneWidget);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
/// Studio's own build version. Bump on every UI release so the
|
/// Studio's own build version. Bump on every UI release so the
|
||||||
/// running app self-identifies.
|
/// running app self-identifies.
|
||||||
const String kStudioVersion = '0.76.0';
|
const String kStudioVersion = '0.75.0';
|
||||||
|
|
||||||
const String kProductName = 'Ch∆In Studio';
|
const String kProductName = 'Ch∆In Studio';
|
||||||
const String kVendorName = 'Flemming.AI (F∆I)';
|
const String kVendorName = 'Flemming.AI (F∆I)';
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,6 @@
|
||||||
"welcomeDocFlowsBlurb": "YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.",
|
"welcomeDocFlowsBlurb": "YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.",
|
||||||
"welcomeDocApprovalsTitle": "Freigaben",
|
"welcomeDocApprovalsTitle": "Freigaben",
|
||||||
"welcomeDocApprovalsBlurb": "Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.",
|
"welcomeDocApprovalsBlurb": "Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.",
|
||||||
"welcomeDocFederationTitle": "Föderation",
|
|
||||||
"welcomeDocFederationBlurb": "Weitere Hubs (Satelliten) anbinden, damit ihre Fähigkeiten hier nutzbar werden — was ein Satellit ist und wie die Anbindung abläuft.",
|
|
||||||
"welcomeDocRunsTitle": "Hintergrund-Läufe",
|
|
||||||
"welcomeDocRunsBlurb": "Flows, die im Hintergrund weiterlaufen — wozu, wie man die Funktion einschaltet und die Läufe überwacht.",
|
|
||||||
"helpTooltip": "Hilfe",
|
"helpTooltip": "Hilfe",
|
||||||
"errApprovalRejected": "Freigabe vom Reviewer abgelehnt.",
|
"errApprovalRejected": "Freigabe vom Reviewer abgelehnt.",
|
||||||
"errApprovalRejectedHint": "Der Reviewer hat den Step abgelehnt. Begründung steht im Audit-Log.",
|
"errApprovalRejectedHint": "Der Reviewer hat den Step abgelehnt. Begründung steht im Audit-Log.",
|
||||||
|
|
@ -108,7 +104,6 @@
|
||||||
"sidebarSettingsTooltip": "Einstellungen (⌘;)",
|
"sidebarSettingsTooltip": "Einstellungen (⌘;)",
|
||||||
"sidebarChannelTooltip": "Aktiver Kanal — klicken zum Wechseln.\nproduction = stabil · beta = Vorabversion · dev = laufend · local = Arbeitsumgebung",
|
"sidebarChannelTooltip": "Aktiver Kanal — klicken zum Wechseln.\nproduction = stabil · beta = Vorabversion · dev = laufend · local = Arbeitsumgebung",
|
||||||
"buttonCancel": "Abbrechen",
|
"buttonCancel": "Abbrechen",
|
||||||
"buttonLearnMore": "Mehr erfahren",
|
|
||||||
"buttonSave": "Speichern",
|
"buttonSave": "Speichern",
|
||||||
"buttonClose": "Schließen",
|
"buttonClose": "Schließen",
|
||||||
"buttonRetry": "Erneut versuchen",
|
"buttonRetry": "Erneut versuchen",
|
||||||
|
|
@ -1769,8 +1764,6 @@
|
||||||
"federationCapabilities": "{count} angebotene Fähigkeiten",
|
"federationCapabilities": "{count} angebotene Fähigkeiten",
|
||||||
"federationAddDialogTitle": "Registrierungs-Token ausstellen",
|
"federationAddDialogTitle": "Registrierungs-Token ausstellen",
|
||||||
"federationNameLabel": "Satelliten-Name",
|
"federationNameLabel": "Satelliten-Name",
|
||||||
"federationAddIntro": "Ein Satellit ist ein weiterer Hub, den dieser Hub anbindet — dessen Fähigkeiten werden dann hier in Flows nutzbar. Sie vergeben einen Namen und erhalten ein einmaliges Token, mit dem sich der Satellit bei diesem Hub anmeldet.",
|
|
||||||
"federationNameHelp": "Frei wählbarer Name, um diesen Satelliten in der Liste wiederzuerkennen (z. B. Standort oder Zweck). Rein zur Anzeige — ändert nichts an der Technik.",
|
|
||||||
"federationIssueButton": "Ausstellen",
|
"federationIssueButton": "Ausstellen",
|
||||||
"federationIssueFailed": "Token konnte nicht ausgestellt werden: {error}",
|
"federationIssueFailed": "Token konnte nicht ausgestellt werden: {error}",
|
||||||
"federationEnrollmentTitle": "{name} einrichten",
|
"federationEnrollmentTitle": "{name} einrichten",
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,6 @@
|
||||||
"welcomeDocFlowsBlurb": "YAML basics, templating reference, the extract→summarize example.",
|
"welcomeDocFlowsBlurb": "YAML basics, templating reference, the extract→summarize example.",
|
||||||
"welcomeDocApprovalsTitle": "Approvals",
|
"welcomeDocApprovalsTitle": "Approvals",
|
||||||
"welcomeDocApprovalsBlurb": "Human-in-the-loop checkpoints — when to use them, how the audit log records them.",
|
"welcomeDocApprovalsBlurb": "Human-in-the-loop checkpoints — when to use them, how the audit log records them.",
|
||||||
"welcomeDocFederationTitle": "Federation",
|
|
||||||
"welcomeDocFederationBlurb": "Connect other hubs (satellites) so their capabilities become usable here — what a satellite is and how enrolment works.",
|
|
||||||
"welcomeDocRunsTitle": "Background runs",
|
|
||||||
"welcomeDocRunsBlurb": "Flows that keep working in the background — what they are for, how to switch the feature on and monitor the runs.",
|
|
||||||
"helpTooltip": "Help",
|
"helpTooltip": "Help",
|
||||||
"errApprovalRejected": "Approval rejected by reviewer.",
|
"errApprovalRejected": "Approval rejected by reviewer.",
|
||||||
"errApprovalRejectedHint": "The reviewer marked this step as rejected. Check the audit log for the reviewer's reason.",
|
"errApprovalRejectedHint": "The reviewer marked this step as rejected. Check the audit log for the reviewer's reason.",
|
||||||
|
|
@ -116,7 +112,6 @@
|
||||||
"sidebarSettingsTooltip": "Settings (⌘;)",
|
"sidebarSettingsTooltip": "Settings (⌘;)",
|
||||||
"sidebarChannelTooltip": "Active channel — click to switch.\nproduction = stable · beta = pre-release · dev = rolling · local = workspace",
|
"sidebarChannelTooltip": "Active channel — click to switch.\nproduction = stable · beta = pre-release · dev = rolling · local = workspace",
|
||||||
"buttonCancel": "Cancel",
|
"buttonCancel": "Cancel",
|
||||||
"buttonLearnMore": "Learn more",
|
|
||||||
"buttonSave": "Save",
|
"buttonSave": "Save",
|
||||||
"buttonClose": "Close",
|
"buttonClose": "Close",
|
||||||
"buttonRetry": "Retry",
|
"buttonRetry": "Retry",
|
||||||
|
|
@ -1798,8 +1793,6 @@
|
||||||
},
|
},
|
||||||
"federationAddDialogTitle": "Issue an enrollment token",
|
"federationAddDialogTitle": "Issue an enrollment token",
|
||||||
"federationNameLabel": "Satellite name",
|
"federationNameLabel": "Satellite name",
|
||||||
"federationAddIntro": "A satellite is another hub this hub connects to — its capabilities then become usable here in flows. You give it a name and receive a one-time token the satellite uses to enrol with this hub.",
|
|
||||||
"federationNameHelp": "A free-form name to recognize this satellite in the list (e.g. its location or purpose). Display only — it changes nothing technical.",
|
|
||||||
"federationIssueButton": "Issue",
|
"federationIssueButton": "Issue",
|
||||||
"federationIssueFailed": "Could not issue token: {error}",
|
"federationIssueFailed": "Could not issue token: {error}",
|
||||||
"@federationIssueFailed": {
|
"@federationIssueFailed": {
|
||||||
|
|
|
||||||
|
|
@ -398,30 +398,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Human-in-the-loop checkpoints — when to use them, how the audit log records them.'**
|
/// **'Human-in-the-loop checkpoints — when to use them, how the audit log records them.'**
|
||||||
String get welcomeDocApprovalsBlurb;
|
String get welcomeDocApprovalsBlurb;
|
||||||
|
|
||||||
/// No description provided for @welcomeDocFederationTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Federation'**
|
|
||||||
String get welcomeDocFederationTitle;
|
|
||||||
|
|
||||||
/// No description provided for @welcomeDocFederationBlurb.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Connect other hubs (satellites) so their capabilities become usable here — what a satellite is and how enrolment works.'**
|
|
||||||
String get welcomeDocFederationBlurb;
|
|
||||||
|
|
||||||
/// No description provided for @welcomeDocRunsTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Background runs'**
|
|
||||||
String get welcomeDocRunsTitle;
|
|
||||||
|
|
||||||
/// No description provided for @welcomeDocRunsBlurb.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Flows that keep working in the background — what they are for, how to switch the feature on and monitor the runs.'**
|
|
||||||
String get welcomeDocRunsBlurb;
|
|
||||||
|
|
||||||
/// No description provided for @helpTooltip.
|
/// No description provided for @helpTooltip.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -704,12 +680,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Cancel'**
|
/// **'Cancel'**
|
||||||
String get buttonCancel;
|
String get buttonCancel;
|
||||||
|
|
||||||
/// No description provided for @buttonLearnMore.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Learn more'**
|
|
||||||
String get buttonLearnMore;
|
|
||||||
|
|
||||||
/// No description provided for @buttonSave.
|
/// No description provided for @buttonSave.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -5377,18 +5347,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Satellite name'**
|
/// **'Satellite name'**
|
||||||
String get federationNameLabel;
|
String get federationNameLabel;
|
||||||
|
|
||||||
/// No description provided for @federationAddIntro.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'A satellite is another hub this hub connects to — its capabilities then become usable here in flows. You give it a name and receive a one-time token the satellite uses to enrol with this hub.'**
|
|
||||||
String get federationAddIntro;
|
|
||||||
|
|
||||||
/// No description provided for @federationNameHelp.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'A free-form name to recognize this satellite in the list (e.g. its location or purpose). Display only — it changes nothing technical.'**
|
|
||||||
String get federationNameHelp;
|
|
||||||
|
|
||||||
/// No description provided for @federationIssueButton.
|
/// No description provided for @federationIssueButton.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -180,20 +180,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get welcomeDocApprovalsBlurb =>
|
String get welcomeDocApprovalsBlurb =>
|
||||||
'Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.';
|
'Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get welcomeDocFederationTitle => 'Föderation';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get welcomeDocFederationBlurb =>
|
|
||||||
'Weitere Hubs (Satelliten) anbinden, damit ihre Fähigkeiten hier nutzbar werden — was ein Satellit ist und wie die Anbindung abläuft.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get welcomeDocRunsTitle => 'Hintergrund-Läufe';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get welcomeDocRunsBlurb =>
|
|
||||||
'Flows, die im Hintergrund weiterlaufen — wozu, wie man die Funktion einschaltet und die Läufe überwacht.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get helpTooltip => 'Hilfe';
|
String get helpTooltip => 'Hilfe';
|
||||||
|
|
||||||
|
|
@ -352,9 +338,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get buttonCancel => 'Abbrechen';
|
String get buttonCancel => 'Abbrechen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get buttonLearnMore => 'Mehr erfahren';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get buttonSave => 'Speichern';
|
String get buttonSave => 'Speichern';
|
||||||
|
|
||||||
|
|
@ -3181,14 +3164,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get federationNameLabel => 'Satelliten-Name';
|
String get federationNameLabel => 'Satelliten-Name';
|
||||||
|
|
||||||
@override
|
|
||||||
String get federationAddIntro =>
|
|
||||||
'Ein Satellit ist ein weiterer Hub, den dieser Hub anbindet — dessen Fähigkeiten werden dann hier in Flows nutzbar. Sie vergeben einen Namen und erhalten ein einmaliges Token, mit dem sich der Satellit bei diesem Hub anmeldet.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get federationNameHelp =>
|
|
||||||
'Frei wählbarer Name, um diesen Satelliten in der Liste wiederzuerkennen (z. B. Standort oder Zweck). Rein zur Anzeige — ändert nichts an der Technik.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get federationIssueButton => 'Ausstellen';
|
String get federationIssueButton => 'Ausstellen';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,20 +181,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get welcomeDocApprovalsBlurb =>
|
String get welcomeDocApprovalsBlurb =>
|
||||||
'Human-in-the-loop checkpoints — when to use them, how the audit log records them.';
|
'Human-in-the-loop checkpoints — when to use them, how the audit log records them.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get welcomeDocFederationTitle => 'Federation';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get welcomeDocFederationBlurb =>
|
|
||||||
'Connect other hubs (satellites) so their capabilities become usable here — what a satellite is and how enrolment works.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get welcomeDocRunsTitle => 'Background runs';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get welcomeDocRunsBlurb =>
|
|
||||||
'Flows that keep working in the background — what they are for, how to switch the feature on and monitor the runs.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get helpTooltip => 'Help';
|
String get helpTooltip => 'Help';
|
||||||
|
|
||||||
|
|
@ -352,9 +338,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get buttonCancel => 'Cancel';
|
String get buttonCancel => 'Cancel';
|
||||||
|
|
||||||
@override
|
|
||||||
String get buttonLearnMore => 'Learn more';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get buttonSave => 'Save';
|
String get buttonSave => 'Save';
|
||||||
|
|
||||||
|
|
@ -3176,14 +3159,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get federationNameLabel => 'Satellite name';
|
String get federationNameLabel => 'Satellite name';
|
||||||
|
|
||||||
@override
|
|
||||||
String get federationAddIntro =>
|
|
||||||
'A satellite is another hub this hub connects to — its capabilities then become usable here in flows. You give it a name and receive a one-time token the satellite uses to enrol with this hub.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get federationNameHelp =>
|
|
||||||
'A free-form name to recognize this satellite in the list (e.g. its location or purpose). Display only — it changes nothing technical.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get federationIssueButton => 'Issue';
|
String get federationIssueButton => 'Issue';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,38 +61,13 @@ class _FederationPageState extends State<FederationPage> {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: Text(l.federationAddDialogTitle),
|
title: Text(l.federationAddDialogTitle),
|
||||||
content: ConstrainedBox(
|
content: TextField(
|
||||||
constraints: const BoxConstraints(maxWidth: 420),
|
controller: controller,
|
||||||
child: Column(
|
autofocus: true,
|
||||||
mainAxisSize: MainAxisSize.min,
|
decoration: InputDecoration(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
labelText: l.federationNameLabel,
|
||||||
children: [
|
hintText: 'satellite-a',
|
||||||
// Say what this is and what will happen before asking
|
border: const OutlineInputBorder(),
|
||||||
// for input — a bare "name" field left the operator
|
|
||||||
// guessing what a satellite even is (usertest).
|
|
||||||
ChainInlineHelp(
|
|
||||||
text: l.federationAddIntro,
|
|
||||||
icon: Icons.hub_outlined,
|
|
||||||
onLearnMore: () => showFaiDoc(ctx, 'federation'),
|
|
||||||
learnMoreLabel: l.buttonLearnMore,
|
|
||||||
),
|
|
||||||
const SizedBox(height: ChainSpace.lg),
|
|
||||||
ChainFieldLabel(
|
|
||||||
label: l.federationNameLabel,
|
|
||||||
help: l.federationNameHelp,
|
|
||||||
),
|
|
||||||
const SizedBox(height: ChainSpace.xs),
|
|
||||||
TextField(
|
|
||||||
controller: controller,
|
|
||||||
autofocus: true,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
hintText: 'satellite-a',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
isDense: true,
|
|
||||||
),
|
|
||||||
onSubmitted: (v) => Navigator.pop(ctx, v.trim()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
|
||||||
|
|
@ -1048,28 +1048,14 @@ class _DocEntry {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String Function(AppLocalizations) title;
|
final String Function(AppLocalizations) title;
|
||||||
final String Function(AppLocalizations) blurb;
|
final String Function(AppLocalizations) blurb;
|
||||||
|
|
||||||
/// Whether this entry is shown as a card in the Welcome page's
|
|
||||||
/// doc grid. Every entry is reachable via [showFaiDoc] regardless;
|
|
||||||
/// `onWelcome: false` keeps advanced topics (federation, runs) out
|
|
||||||
/// of the newcomer grid while still wiring their page help buttons.
|
|
||||||
final bool onWelcome;
|
|
||||||
|
|
||||||
const _DocEntry({
|
const _DocEntry({
|
||||||
required this.slug,
|
required this.slug,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.blurb,
|
required this.blurb,
|
||||||
this.onWelcome = true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every doc topic Studio can open in-app. The slug maps to
|
|
||||||
/// `assets/docs/<slug>[_<locale>].md`. Adding a page help button
|
|
||||||
/// (`showFaiDoc(context, '<slug>')`) requires a matching entry here
|
|
||||||
/// AND the asset files — `doc_help_wiring_test.dart` enforces both,
|
|
||||||
/// so a help button can never silently fall back to the wrong topic
|
|
||||||
/// again (federation/runs used to resolve to architecture).
|
|
||||||
final List<_DocEntry> _kDocs = <_DocEntry>[
|
final List<_DocEntry> _kDocs = <_DocEntry>[
|
||||||
_DocEntry(
|
_DocEntry(
|
||||||
slug: 'architecture',
|
slug: 'architecture',
|
||||||
|
|
@ -1101,47 +1087,17 @@ final List<_DocEntry> _kDocs = <_DocEntry>[
|
||||||
title: (l) => l.welcomeDocApprovalsTitle,
|
title: (l) => l.welcomeDocApprovalsTitle,
|
||||||
blurb: (l) => l.welcomeDocApprovalsBlurb,
|
blurb: (l) => l.welcomeDocApprovalsBlurb,
|
||||||
),
|
),
|
||||||
_DocEntry(
|
|
||||||
slug: 'federation',
|
|
||||||
icon: Icons.hub_outlined,
|
|
||||||
title: (l) => l.welcomeDocFederationTitle,
|
|
||||||
blurb: (l) => l.welcomeDocFederationBlurb,
|
|
||||||
onWelcome: false,
|
|
||||||
),
|
|
||||||
_DocEntry(
|
|
||||||
slug: 'runs',
|
|
||||||
icon: Icons.rocket_launch_outlined,
|
|
||||||
title: (l) => l.welcomeDocRunsTitle,
|
|
||||||
blurb: (l) => l.welcomeDocRunsBlurb,
|
|
||||||
onWelcome: false,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// The doc cards shown on the Welcome page (curated newcomer set).
|
|
||||||
final List<_DocEntry> _kWelcomeDocs =
|
|
||||||
_kDocs.where((d) => d.onWelcome).toList();
|
|
||||||
|
|
||||||
/// Slugs Studio can resolve in-app — exposed for the wiring guard
|
|
||||||
/// test so it can assert every `showFaiDoc` call has a home.
|
|
||||||
final Set<String> kKnownDocSlugs = _kDocs.map((d) => d.slug).toSet();
|
|
||||||
|
|
||||||
/// Public entry-point for the doc-reader sheet. Pass a slug
|
/// Public entry-point for the doc-reader sheet. Pass a slug
|
||||||
/// ("approvals", "audit", "security", "architecture", "flows")
|
/// ("approvals", "audit", "security", "architecture", "flows")
|
||||||
/// and the bottom sheet opens with the localized markdown.
|
/// and the bottom sheet opens with the localized markdown.
|
||||||
/// Returns null when the slug isn't registered — caller can
|
/// Returns null when the slug isn't registered — caller can
|
||||||
/// log + show a SnackBar.
|
/// log + show a SnackBar.
|
||||||
Future<void>? showFaiDoc(BuildContext context, String slug) {
|
Future<void>? showFaiDoc(BuildContext context, String slug) {
|
||||||
// A slug with no entry is a wiring bug (doc_help_wiring_test.dart
|
|
||||||
// guards against it). The fallback keeps the UI alive in release,
|
|
||||||
// but we assert in debug so the mistake surfaces during
|
|
||||||
// development rather than silently opening the wrong topic.
|
|
||||||
final entry = _kDocs.firstWhere(
|
final entry = _kDocs.firstWhere(
|
||||||
(d) => d.slug == slug,
|
(d) => d.slug == slug,
|
||||||
orElse: () {
|
orElse: () => _kDocs.first,
|
||||||
assert(false, 'showFaiDoc: unknown doc slug "$slug" — register it '
|
|
||||||
'in _kDocs and add assets/docs/$slug[_de].md');
|
|
||||||
return _kDocs.first;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return _DocReaderSheet.show(context, entry);
|
return _DocReaderSheet.show(context, entry);
|
||||||
}
|
}
|
||||||
|
|
@ -1174,9 +1130,9 @@ class _DocsRow extends StatelessWidget {
|
||||||
if (!twoCols) {
|
if (!twoCols) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
for (var i = 0; i < _kWelcomeDocs.length; i++) ...[
|
for (var i = 0; i < _kDocs.length; i++) ...[
|
||||||
if (i > 0) const SizedBox(height: ChainSpace.md),
|
if (i > 0) const SizedBox(height: ChainSpace.md),
|
||||||
_DocCard(entry: _kWelcomeDocs[i]),
|
_DocCard(entry: _kDocs[i]),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -1188,12 +1144,12 @@ class _DocsRow extends StatelessWidget {
|
||||||
// card spans the full width so it reads as intentional
|
// card spans the full width so it reads as intentional
|
||||||
// rather than a lonely half-box with dead space beside it.
|
// rather than a lonely half-box with dead space beside it.
|
||||||
final rows = <Widget>[];
|
final rows = <Widget>[];
|
||||||
for (var i = 0; i < _kWelcomeDocs.length; i += 2) {
|
for (var i = 0; i < _kDocs.length; i += 2) {
|
||||||
if (rows.isNotEmpty) {
|
if (rows.isNotEmpty) {
|
||||||
rows.add(const SizedBox(height: ChainSpace.md));
|
rows.add(const SizedBox(height: ChainSpace.md));
|
||||||
}
|
}
|
||||||
final left = _kWelcomeDocs[i];
|
final left = _kDocs[i];
|
||||||
final right = i + 1 < _kWelcomeDocs.length ? _kWelcomeDocs[i + 1] : null;
|
final right = i + 1 < _kDocs.length ? _kDocs[i + 1] : null;
|
||||||
if (right == null) {
|
if (right == null) {
|
||||||
rows.add(_DocCard(entry: left));
|
rows.add(_DocCard(entry: left));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
// In-place help — the "explain it where it happens" pattern
|
|
||||||
// (usertest: the Add-satellite dialog asked for a name with no hint
|
|
||||||
// of what a satellite is or what the name does). Two pieces:
|
|
||||||
//
|
|
||||||
// • ChainInlineHelp — a calm intro strip at the top of a dialog
|
|
||||||
// or surface: one plain sentence saying what this is and what
|
|
||||||
// will happen, with an optional "Learn more" link into the
|
|
||||||
// full doc sheet.
|
|
||||||
// • ChainFieldHelp — a small "?" affordance to sit next to a
|
|
||||||
// single field's label; tap/hover reveals a one-line
|
|
||||||
// explanation. Use it only where a field genuinely needs it.
|
|
||||||
//
|
|
||||||
// Both are intentionally quiet: help should be present, not loud.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../theme/tokens.dart';
|
|
||||||
|
|
||||||
/// A one-sentence intro strip for the top of a dialog or panel.
|
|
||||||
/// [onLearnMore] wires the "Learn more" link to a doc sheet
|
|
||||||
/// (`showFaiDoc`), shown only when provided.
|
|
||||||
class ChainInlineHelp extends StatelessWidget {
|
|
||||||
final String text;
|
|
||||||
final IconData icon;
|
|
||||||
final VoidCallback? onLearnMore;
|
|
||||||
final String? learnMoreLabel;
|
|
||||||
|
|
||||||
const ChainInlineHelp({
|
|
||||||
super.key,
|
|
||||||
required this.text,
|
|
||||||
this.icon = Icons.info_outline,
|
|
||||||
this.onLearnMore,
|
|
||||||
this.learnMoreLabel,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(ChainSpace.md),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
|
||||||
borderRadius: BorderRadius.circular(ChainRadius.sm),
|
|
||||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 16, color: theme.colorScheme.primary),
|
|
||||||
const SizedBox(width: ChainSpace.sm),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (onLearnMore != null) ...[
|
|
||||||
const SizedBox(height: ChainSpace.xs),
|
|
||||||
InkWell(
|
|
||||||
onTap: onLearnMore,
|
|
||||||
borderRadius: BorderRadius.circular(ChainRadius.sm),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
learnMoreLabel ?? 'Learn more',
|
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 2),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward,
|
|
||||||
size: 12,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A "?" info affordance for a single field. Sit it next to the
|
|
||||||
/// field's label; hover shows the [message] as a tooltip, and a
|
|
||||||
/// tap reveals it too (touch / keyboard users who don't hover).
|
|
||||||
/// Semantics carry [message] for screen readers.
|
|
||||||
class ChainFieldHelp extends StatefulWidget {
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
const ChainFieldHelp({super.key, required this.message});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ChainFieldHelp> createState() => _ChainFieldHelpState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChainFieldHelpState extends State<ChainFieldHelp> {
|
|
||||||
final _tooltipKey = GlobalKey<TooltipState>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return Tooltip(
|
|
||||||
key: _tooltipKey,
|
|
||||||
message: widget.message,
|
|
||||||
waitDuration: const Duration(milliseconds: 300),
|
|
||||||
triggerMode: TooltipTriggerMode.manual,
|
|
||||||
preferBelow: false,
|
|
||||||
child: Semantics(
|
|
||||||
button: true,
|
|
||||||
label: widget.message,
|
|
||||||
child: InkResponse(
|
|
||||||
radius: 14,
|
|
||||||
// Manual trigger so a tap (not just hover) reveals it.
|
|
||||||
onTap: () => _tooltipKey.currentState?.ensureTooltipVisible(),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(2),
|
|
||||||
child: Icon(
|
|
||||||
Icons.help_outline,
|
|
||||||
size: 14,
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A field label with a trailing [ChainFieldHelp]. Convenience for
|
|
||||||
/// the common "label + ?" row above a TextField.
|
|
||||||
class ChainFieldLabel extends StatelessWidget {
|
|
||||||
final String label;
|
|
||||||
final String help;
|
|
||||||
|
|
||||||
const ChainFieldLabel({super.key, required this.label, required this.help});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: theme.textTheme.labelMedium?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
ChainFieldHelp(message: help),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,6 @@ export 'chain_delta_mark.dart';
|
||||||
export 'chain_empty_state.dart';
|
export 'chain_empty_state.dart';
|
||||||
export 'chain_en_badge.dart';
|
export 'chain_en_badge.dart';
|
||||||
export 'chain_error_box.dart';
|
export 'chain_error_box.dart';
|
||||||
export 'chain_field_help.dart';
|
|
||||||
export 'chain_segments.dart';
|
export 'chain_segments.dart';
|
||||||
export 'hub_load_error_view.dart';
|
export 'hub_load_error_view.dart';
|
||||||
export 'chain_flow_output.dart';
|
export 'chain_flow_output.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: chain_studio
|
name: chain_studio
|
||||||
description: "Ch∆In Studio — desktop GUI for the Ch∆In hub"
|
description: "Ch∆In Studio — desktop GUI for the Ch∆In hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.76.0
|
version: 0.75.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
// Doc-help wiring guard — a page help button must open the RIGHT
|
|
||||||
// topic. Every `showFaiDoc(context, '<slug>')` call in lib/ needs:
|
|
||||||
//
|
|
||||||
// 1. a registered _DocEntry (exposed as kKnownDocSlugs), and
|
|
||||||
// 2. the backing assets assets/docs/<slug>.md + <slug>_de.md.
|
|
||||||
//
|
|
||||||
// Without this, a slug with no entry silently fell back to the
|
|
||||||
// first doc ('architecture'): the Federation and Runs help buttons
|
|
||||||
// opened the architecture sheet even though federation.md/runs.md
|
|
||||||
// existed — the author wrote the docs, set the icon, but never
|
|
||||||
// wired the catalog, and nothing caught it. This test catches that
|
|
||||||
// whole class (per the no-bugfix-without-a-guard rule).
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'package:chain_studio/pages/welcome.dart' show kKnownDocSlugs;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
final callSlugs = _showFaiDocSlugsInLib();
|
|
||||||
|
|
||||||
test('lib/ actually calls showFaiDoc somewhere (sanity)', () {
|
|
||||||
expect(
|
|
||||||
callSlugs,
|
|
||||||
isNotEmpty,
|
|
||||||
reason: 'no showFaiDoc calls found — did the scan regex go stale?',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('every showFaiDoc slug is registered in the doc catalog', () {
|
|
||||||
final unregistered = callSlugs.difference(kKnownDocSlugs);
|
|
||||||
expect(
|
|
||||||
unregistered,
|
|
||||||
isEmpty,
|
|
||||||
reason:
|
|
||||||
'These slugs are opened by a help button but have no _DocEntry, '
|
|
||||||
'so they silently fall back to the wrong topic. Register them in '
|
|
||||||
'_kDocs (lib/pages/welcome.dart): $unregistered',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('every registered doc slug has both locale assets', () {
|
|
||||||
final missing = <String>[];
|
|
||||||
for (final slug in kKnownDocSlugs) {
|
|
||||||
for (final path in ['assets/docs/$slug.md', 'assets/docs/${slug}_de.md']) {
|
|
||||||
if (!File(path).existsSync()) missing.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(
|
|
||||||
missing,
|
|
||||||
isEmpty,
|
|
||||||
reason: 'Registered doc slugs missing their markdown assets: $missing',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scan lib/ for `showFaiDoc(context, 'slug')` and collect the slugs.
|
|
||||||
Set<String> _showFaiDocSlugsInLib() {
|
|
||||||
final re = RegExp(r'''showFaiDoc\(\s*context\s*,\s*['"]([a-z0-9_-]+)['"]''');
|
|
||||||
final slugs = <String>{};
|
|
||||||
final dir = Directory('lib');
|
|
||||||
for (final f in dir.listSync(recursive: true).whereType<File>()) {
|
|
||||||
if (!f.path.endsWith('.dart')) continue;
|
|
||||||
for (final m in re.allMatches(f.readAsStringSync())) {
|
|
||||||
slugs.add(m.group(1)!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return slugs;
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
// In-place help widgets + the satellite dialog that consumes them.
|
|
||||||
// The pattern (usertest): explain a surface where it happens —
|
|
||||||
// an intro strip saying what this is and what will happen, plus a
|
|
||||||
// per-field "?" for the details.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
import 'package:chain_studio/l10n/app_localizations.dart';
|
|
||||||
import 'package:chain_studio/pages/federation.dart';
|
|
||||||
import 'package:chain_studio/widgets/chain_field_help.dart';
|
|
||||||
|
|
||||||
import 'support/fake_hub.dart';
|
|
||||||
|
|
||||||
Widget _host(Widget child) => MaterialApp(
|
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
|
||||||
locale: const Locale('de'),
|
|
||||||
home: Scaffold(body: child),
|
|
||||||
);
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('ChainInlineHelp shows the text and fires Learn more', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
var learned = false;
|
|
||||||
await tester.pumpWidget(
|
|
||||||
_host(
|
|
||||||
ChainInlineHelp(
|
|
||||||
text: 'Ein Satellit ist ein weiterer Hub.',
|
|
||||||
onLearnMore: () => learned = true,
|
|
||||||
learnMoreLabel: 'Mehr erfahren',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(find.text('Ein Satellit ist ein weiterer Hub.'), findsOneWidget);
|
|
||||||
await tester.tap(find.text('Mehr erfahren'));
|
|
||||||
expect(learned, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('ChainInlineHelp hides Learn more when no callback', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
_host(const ChainInlineHelp(text: 'Nur Text, kein Link.')),
|
|
||||||
);
|
|
||||||
expect(find.text('Nur Text, kein Link.'), findsOneWidget);
|
|
||||||
expect(find.byIcon(Icons.arrow_forward), findsNothing);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('ChainFieldHelp tap reveals the tooltip message', (tester) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
_host(
|
|
||||||
const ChainFieldLabel(
|
|
||||||
label: 'Satelliten-Name',
|
|
||||||
help: 'Frei wählbarer Anzeigename.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// The help text is not shown until the affordance is used.
|
|
||||||
expect(find.text('Frei wählbarer Anzeigename.'), findsNothing);
|
|
||||||
await tester.tap(find.byIcon(Icons.help_outline));
|
|
||||||
await tester.pump(const Duration(milliseconds: 400));
|
|
||||||
expect(find.text('Frei wählbarer Anzeigename.'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Add-satellite dialog explains what a satellite is', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
SharedPreferences.setMockInitialValues({});
|
|
||||||
installFakeHub();
|
|
||||||
await tester.pumpWidget(_host(const FederationPage()));
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
|
||||||
|
|
||||||
// Open the add flow via the AppBar action (the label also
|
|
||||||
// appears in the empty-state CTA; either opens the dialog).
|
|
||||||
await tester.tap(find.text('Satellit hinzufügen').first);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// The intro strip + its Learn-more link are present BEFORE any
|
|
||||||
// input is asked for — no more bare "name" field.
|
|
||||||
expect(find.textContaining('Ein Satellit ist ein weiterer Hub'),
|
|
||||||
findsOneWidget);
|
|
||||||
expect(find.text('Mehr erfahren'), findsOneWidget);
|
|
||||||
// Field-level "?" is wired too.
|
|
||||||
expect(find.byType(ChainFieldHelp), findsWidgets);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue