refactor(ui): one canonical segment control (ChainSegments) everywhere
Some checks failed
Security / Security check (push) Failing after 1s
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>
This commit is contained in:
parent
bfd58baa75
commit
ed680c507a
9 changed files with 311 additions and 147 deletions
101
test/chain_segments_test.dart
Normal file
101
test/chain_segments_test.dart
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// 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')}',
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue