feat(studio): show a source module's data terms before the install button
Some checks failed
Security / Security check (push) Failing after 2s

The store detail sheet now carries a data-source block for source.*
modules: publisher, upstream url, the terms in plain words, and any
attribution the operator has to carry with the output. It sits above
maintainers and above the install button, because it is a decision
input rather than a footnote.

The values are selectable: compliance notes get written by copying,
not retyping. A note names whose terms these are, so nobody reads them
as the module's own licence. Four guards, including that an empty
attribution renders no empty row.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-09-09 12:36:45 +02:00
parent fb809a4cbd
commit 35a79d0bb6
8 changed files with 307 additions and 44 deletions

View file

@ -0,0 +1,85 @@
// Guards for the data-provenance block in the store detail sheet.
//
// The rule it protects (docs/architecture/store-format.md): the terms
// of the material a source module fetches are separate from the
// module's own licence, and an operator must see them before the
// install button, not in a README afterwards.
import 'package:chain_studio/data/hub.dart';
import 'package:chain_studio/l10n/app_localizations.dart';
import 'package:chain_studio/pages/store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
Widget _host(Widget child) => MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('de'),
home: Scaffold(body: SingleChildScrollView(child: child)),
);
void main() {
const bund = DataProvenance(
name: 'gesetze-im-internet.de (BMJ / juris GmbH)',
url: 'https://www.gesetze-im-internet.de/',
license: 'Amtliches Werk (§ 5 UrhG), gemeinfrei',
attribution: 'Quelle: gesetze-im-internet.de (Bundesministerium der Justiz)',
);
testWidgets('publisher, terms and attribution are all shown',
(tester) async {
await tester.pumpWidget(_host(const StoreDataSourceSection(source: bund)));
await tester.pumpAndSettle();
expect(find.text(bund.name), findsOneWidget);
expect(find.text(bund.url), findsOneWidget);
expect(find.text(bund.license), findsOneWidget);
expect(find.text(bund.attribution), findsOneWidget,
reason: 'an attribution duty must be visible before installing');
});
testWidgets('the attribution row is omitted when none is required',
(tester) async {
const noAttribution = DataProvenance(
name: 'Vom Betreiber gepflegter Metadaten-Katalog',
url: '',
license: 'Katalog des Betreibers',
attribution: '',
);
await tester
.pumpWidget(_host(const StoreDataSourceSection(source: noAttribution)));
await tester.pumpAndSettle();
expect(find.text(noAttribution.name), findsOneWidget);
final l = AppLocalizations.of(
tester.element(find.byType(StoreDataSourceSection)),
)!;
expect(find.text(l.storeDataSourceAttribution), findsNothing,
reason: 'an empty attribution must not render an empty row');
});
testWidgets('the terms are selectable so they can be copied',
(tester) async {
await tester.pumpWidget(_host(const StoreDataSourceSection(source: bund)));
await tester.pumpAndSettle();
// Compliance notes get written by copying, not retyping.
expect(
find.byWidgetPredicate(
(w) => w is SelectableText && w.data == bund.attribution,
),
findsOneWidget,
);
});
testWidgets('the note says whose terms these are', (tester) async {
await tester.pumpWidget(_host(const StoreDataSourceSection(source: bund)));
await tester.pumpAndSettle();
final l = AppLocalizations.of(
tester.element(find.byType(StoreDataSourceSection)),
)!;
// Without this line an operator reads the terms as the module's.
expect(find.text(l.storeDataSourceNote), findsOneWidget);
});
}