// 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 = []; await tester.pumpWidget( _host( ChainSegments( 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( 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 = []; final banned = RegExp( r'\b(TabBar|TabBarView|TabController|SegmentedButton|ChoiceChip)\b', ); final files = Directory('lib') .listSync(recursive: true) .whereType() .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')}', ); }); }