import 'dart:async'; import 'package:display/theme/light.dart'; import 'package:display/theme/palette.dart'; import 'package:display/theme/sun.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final dispatcher = TestWidgetsFlutterBinding.instance.platformDispatcher; tearDown(dispatcher.clearPlatformBrightnessTestValue); test( 'auto follows Android, the quick switch holds one, and it is kept', () async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); Themes make() => Themes('t', Palette.fallbackDark, Palette.fallbackBright, prefs); final t = make(); var notified = 0; t.addListener(() => notified++); expect(t.mode, VariantMode.auto); dispatcher.platformBrightnessTestValue = Brightness.light; expect(t.isDark, isFalse); expect(notified, 1); // the binding tells its observers t.toggle(); // bright showing: hold dark expect((t.mode, t.isDark), (VariantMode.dark, true)); dispatcher.platformBrightnessTestValue = Brightness.dark; dispatcher.platformBrightnessTestValue = Brightness.light; expect(notified, 2); // fixed: Android's changes are ignored expect(make().mode, VariantMode.dark); // kept for the next start }, ); test('the light switch has a gap and a hold', () { final s = LightSwitch(); final t0 = DateTime(2026); DateTime at(int sec) => t0.add(Duration(seconds: sec)); expect(s.feed(30, at(0)), isNull); // in the gap: no verdict yet expect(s.feed(500, at(1)), isFalse); // the first clear reading decides expect(s.feed(30, at(2)), isFalse); // the gap keeps it expect(s.feed(5, at(3)), isFalse); // dark, but not for long enough expect(s.pending, isTrue); expect(s.feed(200, at(4)), isFalse); // back: the hold restarts expect(s.pending, isFalse); s.feed(5, at(10)); expect(s.feed(5, at(14)), isFalse); expect(s.feed(5, at(15)), isTrue); }); testWidgets('auto takes the light sensor over Android', (tester) async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; final t = Themes('t', Palette.fallbackDark, Palette.fallbackBright, prefs); final levels = StreamController(); t.followLight(levels.stream); expect(t.isDark, isTrue); // no reading yet: Android's levels.add(800); await tester.pump(); expect((t.isDark, t.lux.value), (false, 800)); levels.add(2); await tester.pump( const Duration(seconds: 6), ); // no new reading: the timer ends the hold expect(t.isDark, isTrue); t.dispose(); }); test( 'the sun: high at noon, below at midnight, sets near the almanac time', () { // Tokyo, 2026-06-21: noon (03:00 UTC) about 77°, midnight well below. expect( sunElevation(35.68, 139.77, DateTime.utc(2026, 6, 21, 2, 43)), closeTo(77.7, 1), ); expect( sunElevation(35.68, 139.77, DateTime.utc(2026, 6, 21, 15)), lessThan(-20), ); // Helsinki, 2026-10-15: sunset 18:14 EEST (15:14 UTC): -0.8° there. expect( sunElevation(60.17, 24.94, DateTime.utc(2026, 10, 15, 15, 14)), closeTo(-0.8, 0.6), ); }, ); test( 'auto: the sun before Android, the light sensor before the sun', () async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); dispatcher.platformBrightnessTestValue = Brightness.light; var now = DateTime.utc(2026, 6, 21, 15); // Tokyo midnight final t = Themes( 't', Palette.fallbackDark, Palette.fallbackBright, prefs, () => now, ); expect((t.isDark, t.autoSource), (false, "Android's dark theme")); t.setPosition(35.68, 139.77); expect((t.isDark, t.autoSource), (true, 'the sun')); now = DateTime.utc(2026, 6, 21, 3); final again = Themes( 't', Palette.fallbackDark, Palette.fallbackBright, prefs, () => now, ); expect(again.isDark, isFalse); // position kept; Tokyo noon t.dispose(); again.dispose(); }, ); }