focus-flow/apps/focus_flow_flutter/test/app/settings_test.dart
pyr0ball 6f0da3c8aa feat(ui): wire Settings dialog to persisted OwnerSettings
Adds a getOwnerSettings read query to scheduler_core (previously only
an internal helper existed) and an updateOwnerSettings command on the
Flutter command controller, then builds a shared Settings dialog
surfacing timezone, day window, compact mode, and backlog staleness
thresholds. Both existing no-op entry points (sidebar nav item, top
bar button) now open the same dialog and persist edits through the
existing OwnerSettings/BacklogStalenessSettings backend.

Per the design spec (circuitforge-plans/focus-flow/superpowers/specs/
2026-07-07-settings-screen-design.md), the compact-mode toggle
persists but is disclosed in-UI as not yet affecting Today rendering,
since that wiring is separate scope (#11).

Closes: #16
2026-07-07 14:38:16 -07:00

115 lines
3.5 KiB
Dart

// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
/// Tests app-level settings dialog behavior.
library;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:scheduler_core/scheduler_core.dart';
import 'package:focus_flow_flutter/app/demo_scheduler_composition.dart';
import 'package:focus_flow_flutter/app/focus_flow_app.dart';
/// Runs app-level settings dialog tests.
void main() {
testWidgets('sidebar Settings opens a dialog seeded with persisted values', (
tester,
) async {
await _pumpPlanApp(tester);
await tester.tap(find.byKey(const ValueKey('nav-settings')));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
expect(find.text('UTC'), findsOneWidget);
final compactSwitch = tester.widget<SwitchListTile>(
find.byKey(const ValueKey('settings-compact-mode')),
);
expect(compactSwitch.value, isTrue);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
});
testWidgets('top bar Settings button opens the same dialog', (tester) async {
await _pumpPlanApp(tester);
await tester.tap(find.byKey(const ValueKey('top-bar-settings-button')));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('settings-timezone')), findsOneWidget);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
});
testWidgets('saving settings persists the change and reopens with it', (
tester,
) async {
await _pumpPlanApp(tester);
await tester.tap(find.byKey(const ValueKey('nav-settings')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('settings-timezone')));
await tester.pumpAndSettle();
await tester.tap(find.text('EST').last);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('settings-save')));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
await tester.tap(find.byKey(const ValueKey('nav-settings')));
await tester.pumpAndSettle();
expect(find.text('EST'), findsOneWidget);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
});
testWidgets('invalid staleness thresholds show a calm inline error', (
tester,
) async {
await _pumpPlanApp(tester);
await tester.tap(find.byKey(const ValueKey('nav-settings')));
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(const ValueKey('settings-aging-days')),
'1',
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('settings-save')));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
expect(
find.text('Aging must be a number of days at least as large as Fresh.'),
findsOneWidget,
);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
});
}
/// Pumps the FocusFlow app at the desktop test size.
Future<void> _pumpPlanApp(WidgetTester tester) async {
await tester.binding.setSurfaceSize(const Size(1586, 992));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(FocusFlowApp(composition: _composition()));
await tester.pumpAndSettle();
}
/// Builds the seeded demo composition used by settings dialog tests.
DemoSchedulerComposition _composition() {
return DemoSchedulerComposition.seeded(selectedDate: CivilDate(2025, 5, 20));
}