Wire design_system into CI, and drop EmptyState's speculative action slot

The pre-push coverage gate only ran for whoever had the local hook
installed, which is deployment hygiene rather than a gate. Adds the
mirrored workflow sync_settings_ui already has, so the 100% bar is
enforced on push and PR.

EmptyState.action was invented surface: no donor had it. home_inventory's
three call sites are pure placeholders, and kuhylog's action-bearing
_empty was explicitly excluded from the survey as a different element.
Shipping a parameter nobody asked for on a widget that has no consumer
yet is the failure mode this package is supposed to avoid, so it goes,
with a comment recording why rather than leaving the next reader to
rediscover it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
This commit is contained in:
Krzysztof Rudnicki 2026-08-14 23:08:59 +02:00
parent cae0870fd0
commit 522fa1dff0
3 changed files with 46 additions and 23 deletions

View File

@ -0,0 +1,36 @@
name: design_system tests
on:
push:
branches: [main]
paths: ['design_system/**']
pull_request:
branches: [main]
paths: ['design_system/**']
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: design_system
steps:
- uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Install dependencies
run: flutter pub get
- name: Analyze
run: flutter analyze
- name: Run tests with coverage
run: flutter test --coverage
- name: Enforce 100% line coverage
run: scripts/check_coverage.sh coverage/lcov.info 100

View File

@ -14,7 +14,6 @@ class EmptyState extends StatelessWidget {
required this.icon,
required this.title,
required this.message,
this.action,
super.key,
});
@ -27,15 +26,14 @@ class EmptyState extends StatelessWidget {
/// One line explaining what to do about it.
final String message;
/// Optional call to action below the message — usually the button that
/// creates the first item. Omit it where the empty state is a *filter*
/// result rather than an empty collection: there is nothing to create.
final Widget? action;
// Deliberately no `action` slot. No donor had one: home_inventory's three
// call sites are pure placeholders, and kuhylog's action-bearing `_empty`
// is a different element. Adding one here would be speculative surface --
// the widget nobody imports, one parameter down.
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final action = this.action;
return Center(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.xl),
@ -57,10 +55,6 @@ class EmptyState extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant,
),
),
if (action != null) ...[
const SizedBox(height: AppSpacing.lg),
action,
],
],
),
),

View File

@ -2,16 +2,15 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
Future<void> _pumpEmptyState(WidgetTester tester, {Widget? action}) {
Future<void> _pumpEmptyState(WidgetTester tester) {
return tester.pumpWidget(
MaterialApp(
theme: buildDarkTheme(),
home: Scaffold(
home: const Scaffold(
body: EmptyState(
icon: Icons.inbox_outlined,
title: 'Nothing here yet',
message: 'Add your first item to get started.',
action: action,
),
),
),
@ -49,17 +48,11 @@ void main() {
expect(title.style?.color, isNot(scheme.onSurfaceVariant));
});
testWidgets('omits the action slot by default', (tester) async {
testWidgets('is a pure placeholder with no action', (tester) async {
// Deliberate: no donor had an action slot, so adding one would be
// speculative surface on a widget that has no consumer yet.
await _pumpEmptyState(tester);
expect(find.byType(ElevatedButton), findsNothing);
});
testWidgets('renders an action when one is given', (tester) async {
await _pumpEmptyState(
tester,
action: ElevatedButton(onPressed: () {}, child: const Text('Add item')),
);
expect(find.text('Add item'), findsOneWidget);
expect(find.byType(ButtonStyleButton), findsNothing);
});
testWidgets('stays centred and shrink-wrapped', (tester) async {