Some checks failed
Security / Security check (push) Failing after 1s
The same single-select choice pattern appeared as four widgets: audit's hover pills, the store's SegmentedButton, the store filter dialog's ChoiceChips, and the approvals TabBar (usertest finding #14 / night-log decision 'pill segment as canon'). The audit pattern is promoted to a shared ChainSegments widget (optional icons, hover, selected border, button+selected semantics) and all four sites use it; approvals switches lists via IndexedStack so both stay alive and switching does not refetch. Guard per the no-bugfix-without-a-guard rule: widget tests for selection + semantics, plus a canon sweep that bans TabBar/TabBarView/TabController/SegmentedButton/ChoiceChip from lib/ (comments exempt). Deliberately out of scope: the flow editor's Graph/Text/Run tabs live in the separate editor package. Studio 0.75.0; guide images regenerated, dark + light verified. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
101 lines
3.3 KiB
Dart
101 lines
3.3 KiB
Dart
// ChainSegments — the canonical single-select segment control
|
|
// (night-log decision "pill segment as canon"; usertest finding
|
|
// #14: the same choice pattern appeared as four different
|
|
// widgets). Guards both the widget behaviour and the canon
|
|
// itself: no page may reintroduce TabBar / SegmentedButton /
|
|
// ChoiceChip for single-select segments.
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'dart:ui' show Tristate;
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:chain_studio/widgets/chain_segments.dart';
|
|
|
|
Widget _host(Widget child) =>
|
|
MaterialApp(home: Scaffold(body: Center(child: child)));
|
|
|
|
void main() {
|
|
testWidgets('tapping a segment reports the new value once', (tester) async {
|
|
final changes = <String>[];
|
|
await tester.pumpWidget(
|
|
_host(
|
|
ChainSegments<String>(
|
|
items: const [
|
|
ChainSegmentItem('all', 'Alle'),
|
|
ChainSegmentItem('flow', 'Flow'),
|
|
ChainSegmentItem('step', 'Schritt'),
|
|
],
|
|
value: 'all',
|
|
onChanged: changes.add,
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text('Flow'));
|
|
await tester.pump();
|
|
expect(changes, ['flow']);
|
|
// Tapping the already-selected value must NOT re-fire — call
|
|
// sites use the callback to trigger refetches.
|
|
await tester.tap(find.text('Alle'));
|
|
await tester.pump();
|
|
expect(changes, ['flow']);
|
|
});
|
|
|
|
testWidgets('segments expose button + selected semantics', (tester) async {
|
|
final handle = tester.ensureSemantics();
|
|
await tester.pumpWidget(
|
|
_host(
|
|
ChainSegments<int>(
|
|
items: const [
|
|
ChainSegmentItem(0, 'Ausstehend'),
|
|
ChainSegmentItem(1, 'Verlauf'),
|
|
],
|
|
value: 0,
|
|
onChanged: (_) {},
|
|
),
|
|
),
|
|
);
|
|
final pending = tester.getSemantics(find.text('Ausstehend'));
|
|
expect(pending.flagsCollection.isSelected, Tristate.isTrue);
|
|
final history = tester.getSemantics(find.text('Verlauf'));
|
|
expect(history.flagsCollection.isSelected, Tristate.isFalse);
|
|
handle.dispose();
|
|
});
|
|
|
|
test('canon guard: no page reintroduces TabBar/SegmentedButton/ChoiceChip',
|
|
() {
|
|
// The night-log decision made the pill segment the ONE way to
|
|
// render a single-select segment. This sweep keeps the three
|
|
// retired Material patterns out of lib/ for good (comments
|
|
// don't count; the widget file itself is exempt).
|
|
final offenders = <String>[];
|
|
final banned = RegExp(
|
|
r'\b(TabBar|TabBarView|TabController|SegmentedButton|ChoiceChip)\b',
|
|
);
|
|
final files = Directory('lib')
|
|
.listSync(recursive: true)
|
|
.whereType<File>()
|
|
.where((f) => f.path.endsWith('.dart'))
|
|
.where((f) => !f.path.contains('chain_segments.dart'));
|
|
for (final f in files) {
|
|
final lines = f.readAsLinesSync();
|
|
for (var i = 0; i < lines.length; i++) {
|
|
final line = lines[i];
|
|
final code = line.contains('//')
|
|
? line.substring(0, line.indexOf('//'))
|
|
: line;
|
|
if (banned.hasMatch(code)) {
|
|
offenders.add('${f.path}:${i + 1}: ${line.trim()}');
|
|
}
|
|
}
|
|
}
|
|
expect(
|
|
offenders,
|
|
isEmpty,
|
|
reason:
|
|
'Single-select segments use ChainSegments (canon). '
|
|
'Found retired patterns:\n${offenders.join('\n')}',
|
|
);
|
|
});
|
|
}
|