chore: Halkyon Learn moves into apps/learn
Its server, client, app and curator, and the tools that write its assets (maps, Anki fixtures, icons), as they are; paths are fixed in the commits that follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
11abd9afce
commit
649499be23
6 changed files with 0 additions and 0 deletions
|
|
@ -0,0 +1,223 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:fake_async/fake_async.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/bloc/pomodoro_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_run.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
|
||||
import '../../../support/fake_settings_gateway.dart';
|
||||
import '../../../support/memory_pomodoro_run_store.dart';
|
||||
|
||||
void main() {
|
||||
final start = DateTime.utc(2026, 10, 8, 9);
|
||||
|
||||
/// Runs [body] with a bloc over fresh settings and [kept] as the timer the
|
||||
/// device kept, in fake time from [start].
|
||||
void withBloc(
|
||||
void Function(
|
||||
FakeAsync async,
|
||||
PomodoroBloc bloc,
|
||||
SettingsRepository settings,
|
||||
MemoryPomodoroRunStore store,
|
||||
)
|
||||
body, {
|
||||
PomodoroRun? kept,
|
||||
}) => fakeAsync((async) {
|
||||
withClock(async.getClock(start), () {
|
||||
final settings = SettingsRepository(FakeSettingsGateway());
|
||||
final store = MemoryPomodoroRunStore(kept);
|
||||
final bloc = PomodoroBloc(settings, store)
|
||||
..add(const PomodoroEventStarted());
|
||||
async.flushMicrotasks();
|
||||
body(async, bloc, settings, store);
|
||||
bloc.close();
|
||||
settings.dispose();
|
||||
async.flushMicrotasks();
|
||||
});
|
||||
});
|
||||
|
||||
test("starts with the person's chosen profile and no timer", () {
|
||||
withBloc((async, bloc, settings, store) {
|
||||
expect(bloc.state.loaded, isTrue);
|
||||
expect(bloc.state.settings.selected, PomodoroProfile.classic);
|
||||
expect(bloc.state.run, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('start runs the focus; pause keeps what is left; resume goes on', () {
|
||||
withBloc((async, bloc, settings, store) {
|
||||
bloc.add(const PomodoroEventStartRequested());
|
||||
async.flushMicrotasks();
|
||||
expect(bloc.state.run!.isRunning, isTrue);
|
||||
expect(bloc.state.run!.phase, PomodoroPhase.focus);
|
||||
expect(store.kept, bloc.state.run, reason: 'kept on the device');
|
||||
|
||||
async.elapse(const Duration(minutes: 10));
|
||||
bloc.add(const PomodoroEventPauseRequested());
|
||||
async.flushMicrotasks();
|
||||
expect(bloc.state.run!.isRunning, isFalse);
|
||||
expect(
|
||||
bloc.state.run!.leftAt(clock.now()),
|
||||
const Duration(minutes: 15),
|
||||
);
|
||||
|
||||
async.elapse(const Duration(hours: 1));
|
||||
bloc.add(const PomodoroEventResumeRequested());
|
||||
async.flushMicrotasks();
|
||||
expect(
|
||||
bloc.state.run!.endsAt,
|
||||
clock.now().add(const Duration(minutes: 15)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when the focus ends, the break waits to be started', () {
|
||||
withBloc((async, bloc, settings, store) {
|
||||
bloc.add(const PomodoroEventStartRequested());
|
||||
async
|
||||
..flushMicrotasks()
|
||||
..elapse(const Duration(minutes: 25, seconds: 1));
|
||||
|
||||
expect(bloc.state.run!.phase, PomodoroPhase.shortBreak);
|
||||
expect(bloc.state.run!.isRunning, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
test('skip goes to the next phase, and stop forgets the timer', () {
|
||||
withBloc((async, bloc, settings, store) {
|
||||
bloc.add(const PomodoroEventStartRequested());
|
||||
async.flushMicrotasks();
|
||||
bloc.add(const PomodoroEventSkipRequested());
|
||||
async.flushMicrotasks();
|
||||
expect(bloc.state.run!.phase, PomodoroPhase.shortBreak);
|
||||
|
||||
bloc.add(const PomodoroEventStopRequested());
|
||||
async.flushMicrotasks();
|
||||
expect(bloc.state.run, isNull);
|
||||
expect(store.kept, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('a timer the device kept catches up when the app opens', () {
|
||||
withBloc(
|
||||
(async, bloc, settings, store) {
|
||||
expect(bloc.state.run!.phase, PomodoroPhase.shortBreak);
|
||||
expect(bloc.state.run!.isRunning, isFalse);
|
||||
},
|
||||
kept: PomodoroRun.ready(
|
||||
PomodoroProfile.classic,
|
||||
).started(start.subtract(const Duration(hours: 1))),
|
||||
);
|
||||
});
|
||||
|
||||
test('a change to the profile running applies from now on', () {
|
||||
withBloc((async, bloc, settings, store) {
|
||||
bloc.add(const PomodoroEventStartRequested());
|
||||
async.flushMicrotasks();
|
||||
|
||||
bloc.add(
|
||||
PomodoroEventProfileSaved(
|
||||
PomodoroProfile.classic.copyWith(shortBreakMinutes: 10),
|
||||
),
|
||||
);
|
||||
async.flushMicrotasks();
|
||||
bloc.add(const PomodoroEventSkipRequested());
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(bloc.state.run!.length, const Duration(minutes: 10));
|
||||
});
|
||||
});
|
||||
|
||||
test('a timer on a profile deleted goes on with the one chosen then', () {
|
||||
withBloc((async, bloc, settings, store) {
|
||||
const mine = PomodoroProfile(
|
||||
id: 'mine',
|
||||
name: 'Mine',
|
||||
shortBreakMinutes: 9,
|
||||
);
|
||||
bloc
|
||||
..add(const PomodoroEventProfileSaved(mine))
|
||||
..add(const PomodoroEventProfileChosen('mine'));
|
||||
async.flushMicrotasks();
|
||||
bloc.add(const PomodoroEventStartRequested());
|
||||
async.flushMicrotasks();
|
||||
expect(bloc.state.run!.profile, mine);
|
||||
|
||||
bloc.add(const PomodoroEventProfileDeleted('mine'));
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(bloc.state.settings.selected, PomodoroProfile.classic);
|
||||
expect(bloc.state.run!.profile, PomodoroProfile.classic);
|
||||
expect(bloc.state.run!.isRunning, isTrue, reason: 'it goes on');
|
||||
});
|
||||
});
|
||||
|
||||
test("adds, chooses and deletes profiles in the person's settings", () {
|
||||
withBloc((async, bloc, settings, store) {
|
||||
const mine = PomodoroProfile(id: 'mine', name: 'Mine');
|
||||
bloc
|
||||
..add(const PomodoroEventProfileSaved(mine))
|
||||
..add(const PomodoroEventProfileChosen('mine'));
|
||||
async.flushMicrotasks();
|
||||
expect(bloc.state.settings.selected, mine);
|
||||
|
||||
bloc.add(const PomodoroEventProfileDeleted('mine'));
|
||||
async.flushMicrotasks();
|
||||
expect(bloc.state.settings.selected, PomodoroProfile.classic);
|
||||
expect(bloc.state.settings.profiles, hasLength(2));
|
||||
});
|
||||
});
|
||||
|
||||
test('a timer kept while the app was closed shows caught up, never as it '
|
||||
'was', () {
|
||||
fakeAsync((async) {
|
||||
withClock(async.getClock(start), () {
|
||||
final settings = SettingsRepository(FakeSettingsGateway());
|
||||
final store = MemoryPomodoroRunStore(
|
||||
PomodoroRun.ready(
|
||||
PomodoroProfile.classic,
|
||||
).started(start.subtract(const Duration(hours: 1))),
|
||||
);
|
||||
final bloc = PomodoroBloc(settings, store);
|
||||
final states = <PomodoroState>[];
|
||||
bloc.stream.listen(states.add);
|
||||
bloc.add(const PomodoroEventStarted());
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(
|
||||
states.where((state) => state.run?.isRunning ?? false),
|
||||
isEmpty,
|
||||
);
|
||||
expect(states.last.run?.phase, PomodoroPhase.shortBreak);
|
||||
bloc.close();
|
||||
settings.dispose();
|
||||
async.flushMicrotasks();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('a timer stopped outside the app while it was gone is no more', () {
|
||||
fakeAsync((async) {
|
||||
withClock(async.getClock(start), () {
|
||||
final settings = SettingsRepository(FakeSettingsGateway());
|
||||
final store = MemoryPomodoroRunStore(
|
||||
PomodoroRun.ready(PomodoroProfile.classic).started(start),
|
||||
);
|
||||
final bloc = PomodoroBloc(
|
||||
settings,
|
||||
store,
|
||||
stoppedOutside: () async => true,
|
||||
)..add(const PomodoroEventStarted());
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(bloc.state.loaded, isTrue);
|
||||
expect(bloc.state.run, isNull);
|
||||
expect(store.kept, isNull);
|
||||
bloc.close();
|
||||
settings.dispose();
|
||||
async.flushMicrotasks();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/android_pomodoro_platform.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_run.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/widget/pomodoro_texts.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
// The app has them through Material's localizations.
|
||||
setUpAll(initializeDateFormatting);
|
||||
final now = DateTime.utc(2026, 10, 8, 9);
|
||||
final german = lookupAppLocalizations(const Locale('de'));
|
||||
|
||||
test("hands the service a notice in the app's language, the cycle as "
|
||||
'segments and its progress', () {
|
||||
const quietProfile = PomodoroProfile(id: 'classic', doNotDisturb: true);
|
||||
final run = PomodoroRun.ready(quietProfile).started(now);
|
||||
|
||||
final json = noticeJson(
|
||||
german.pomodoroNotice(run, now.add(const Duration(minutes: 5))),
|
||||
);
|
||||
|
||||
expect(json['title'], 'Fokus 1 von 4');
|
||||
expect(json['text'], 'Klassisch');
|
||||
expect(
|
||||
json['endsAt'],
|
||||
now.add(const Duration(minutes: 25)).millisecondsSinceEpoch,
|
||||
);
|
||||
expect(json['elapsedMs'], const Duration(minutes: 5).inMilliseconds);
|
||||
expect(json['index'], 0);
|
||||
expect((json['cycle']! as List).first, {
|
||||
'phase': 'focus',
|
||||
'lengthMs': const Duration(minutes: 25).inMilliseconds,
|
||||
});
|
||||
expect((json['cycle']! as List).last, {
|
||||
'phase': 'longBreak',
|
||||
'lengthMs': const Duration(minutes: 15).inMilliseconds,
|
||||
});
|
||||
expect(json['actions'], {
|
||||
'pause': 'Pausieren',
|
||||
'resume': 'Fortsetzen',
|
||||
'skip': 'Überspringen',
|
||||
'stop': 'Beenden',
|
||||
});
|
||||
expect(json['alertTitle'], 'Zeit für eine Pause');
|
||||
expect(json['alertText'], 'Kurze Pause wartet auf dich.');
|
||||
expect(json['quiet'], isTrue);
|
||||
expect(json['timerName'], 'Pomodoro-Timer');
|
||||
});
|
||||
|
||||
test('a paused timer has no end, and keeps quiet no longer', () {
|
||||
const quietProfile = PomodoroProfile(id: 'classic', doNotDisturb: true);
|
||||
final run = PomodoroRun.ready(quietProfile);
|
||||
|
||||
final json = noticeJson(german.pomodoroNotice(run, now));
|
||||
|
||||
expect(json['endsAt'], isNull);
|
||||
expect(json['leftMs'], const Duration(minutes: 25).inMilliseconds);
|
||||
expect(json['text'], 'Pausiert · Klassisch');
|
||||
expect(json['quiet'], isFalse);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,366 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:dbus/dbus.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/linux_pomodoro_platform.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_platform.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/status_notifier_item.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_run.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/widget/pomodoro_texts.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// A notification server, as GNOME's, noting what it was asked.
|
||||
class _Notifications() extends DBusObject {
|
||||
this : super(DBusObjectPath('/org/freedesktop/Notifications'));
|
||||
|
||||
final notified = <List<DBusValue>>[];
|
||||
|
||||
@override
|
||||
Future<DBusMethodResponse> handleMethodCall(DBusMethodCall call) async {
|
||||
if (call.name == 'Notify') {
|
||||
notified.add(call.values);
|
||||
return DBusMethodSuccessResponse([DBusUint32(notified.length)]);
|
||||
}
|
||||
return DBusMethodErrorResponse.unknownMethod();
|
||||
}
|
||||
}
|
||||
|
||||
/// The top bar's watcher, as GNOME's AppIndicator extension runs it.
|
||||
class _Watcher() extends DBusObject {
|
||||
this : super(DBusObjectPath('/StatusNotifierWatcher'));
|
||||
|
||||
final registered = <String>[];
|
||||
|
||||
@override
|
||||
Future<DBusMethodResponse> handleMethodCall(DBusMethodCall call) async {
|
||||
if (call.name == 'RegisterStatusNotifierItem') {
|
||||
registered.add((call.values.single as DBusString).value);
|
||||
return DBusMethodSuccessResponse();
|
||||
}
|
||||
return DBusMethodErrorResponse.unknownMethod();
|
||||
}
|
||||
}
|
||||
|
||||
/// GNOME Shell's extensions, with those [installed] (off) noted by id.
|
||||
class _Extensions(final Set<String> installed) extends DBusObject {
|
||||
this : super(DBusObjectPath('/org/gnome/Shell/Extensions'));
|
||||
|
||||
final calls = <String>[];
|
||||
|
||||
@override
|
||||
Future<DBusMethodResponse> handleMethodCall(DBusMethodCall call) async {
|
||||
final uuid = (call.values.first as DBusString).value;
|
||||
calls.add('${call.name} $uuid');
|
||||
return switch (call.name) {
|
||||
'GetExtensionInfo' => DBusMethodSuccessResponse([
|
||||
DBusDict.stringVariant({
|
||||
if (installed.contains(uuid)) 'uuid': DBusString(uuid),
|
||||
}),
|
||||
]),
|
||||
'EnableExtension' => DBusMethodSuccessResponse([
|
||||
const DBusBoolean(true),
|
||||
]),
|
||||
'InstallRemoteExtension' => DBusMethodSuccessResponse([
|
||||
const DBusString('successful'),
|
||||
]),
|
||||
_ => DBusMethodErrorResponse.unknownMethod(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
// The app has them through Material's localizations.
|
||||
setUpAll(initializeDateFormatting);
|
||||
final english = lookupAppLocalizations(const Locale('en', 'GB'));
|
||||
late DBusServer server;
|
||||
late DBusAddress address;
|
||||
late DBusClient serverSide;
|
||||
late DBusClient app;
|
||||
late _Notifications notifications;
|
||||
late Directory folder;
|
||||
late String banners;
|
||||
late List<Uri> opened;
|
||||
late LinuxPomodoroPlatform platform;
|
||||
|
||||
LinuxPomodoroPlatform newPlatform() => LinuxPomodoroPlatform(
|
||||
app,
|
||||
folder,
|
||||
run: (command, arguments) async {
|
||||
if (arguments.first == 'set') {
|
||||
banners = arguments.last;
|
||||
}
|
||||
return ProcessResult(0, 0, '$banners\n', '');
|
||||
},
|
||||
open: (page) async => opened.add(page),
|
||||
);
|
||||
|
||||
setUp(() async {
|
||||
server = DBusServer();
|
||||
address = await server.listenAddress(
|
||||
DBusAddress.unix(dir: Directory.systemTemp),
|
||||
);
|
||||
serverSide = DBusClient(address);
|
||||
notifications = _Notifications();
|
||||
await serverSide.requestName('org.freedesktop.Notifications');
|
||||
await serverSide.registerObject(notifications);
|
||||
app = DBusClient(address);
|
||||
folder = await Directory.systemTemp.createTemp('pomodoro_linux_test');
|
||||
banners = 'true';
|
||||
opened = [];
|
||||
platform = newPlatform();
|
||||
});
|
||||
tearDown(() async {
|
||||
await app.close();
|
||||
await serverSide.close();
|
||||
await server.close();
|
||||
await folder.delete(recursive: true);
|
||||
});
|
||||
|
||||
PomodoroNotice notice(PomodoroRun run) =>
|
||||
english.pomodoroNotice(run, DateTime.now().toUtc());
|
||||
|
||||
/// GNOME's AppIndicator extension, on the bus until its client closes.
|
||||
Future<(_Watcher, DBusClient)> startWatcher() async {
|
||||
final watcher = _Watcher();
|
||||
final shell = DBusClient(address);
|
||||
await shell.requestName('org.kde.StatusNotifierWatcher');
|
||||
await shell.registerObject(watcher);
|
||||
return (watcher, shell);
|
||||
}
|
||||
|
||||
test('without a top bar for it, shows no notification of its own', () async {
|
||||
final run = PomodoroRun.ready(PomodoroProfile.classic);
|
||||
|
||||
await platform.show(notice(run.started(DateTime.now().toUtc())));
|
||||
await platform.show(notice(run));
|
||||
|
||||
expect(notifications.notified, isEmpty);
|
||||
});
|
||||
|
||||
test('keeps GNOME quiet while focusing, and gives the banners back '
|
||||
'after', () async {
|
||||
const quiet = PomodoroProfile(id: 'classic', doNotDisturb: true);
|
||||
final run = PomodoroRun.ready(quiet).started(DateTime.now().toUtc());
|
||||
|
||||
expect(await platform.mayKeepQuiet(), isTrue);
|
||||
await platform.show(notice(run));
|
||||
expect(banners, 'false');
|
||||
|
||||
await platform.clear();
|
||||
expect(banners, 'true');
|
||||
});
|
||||
|
||||
test('alerts when the phase ends', () async {
|
||||
const short = PomodoroProfile(id: 'short', focusMinutes: 1);
|
||||
final run = PomodoroRun(
|
||||
profile: short,
|
||||
phase: PomodoroPhase.focus,
|
||||
endsAt: DateTime.now().toUtc().add(const Duration(milliseconds: 200)),
|
||||
);
|
||||
|
||||
await platform.show(notice(run));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
|
||||
final alert = notifications.notified.single;
|
||||
expect((alert[3] as DBusString).value, 'Time for a break');
|
||||
expect(
|
||||
(alert[6] as DBusDict).children[const DBusString('sound-name')],
|
||||
const DBusVariant(DBusString('complete')),
|
||||
);
|
||||
});
|
||||
|
||||
group('where the top bar shows icons', () {
|
||||
late _Watcher watcher;
|
||||
late DBusClient shell;
|
||||
|
||||
setUp(() async => (watcher, shell) = await startWatcher());
|
||||
tearDown(() => shell.close());
|
||||
|
||||
test('the timer counts down there, its buttons in its menu', () async {
|
||||
final run = PomodoroRun.ready(
|
||||
PomodoroProfile.classic,
|
||||
).started(DateTime.now().toUtc());
|
||||
final pressed = platform.actions.first;
|
||||
|
||||
await platform.show(notice(run));
|
||||
|
||||
// Registered by its path, as libayatana-appindicator does: the snap's
|
||||
// rules let the item bind no name, and send the label's signal only
|
||||
// from there.
|
||||
final [registered] = watcher.registered;
|
||||
expect(registered, '/org/ayatana/NotificationItem/halkyon_learn');
|
||||
final name = app.uniqueName;
|
||||
expect(notifications.notified, isEmpty);
|
||||
final item = DBusRemoteObject(
|
||||
shell,
|
||||
name: name,
|
||||
path: DBusObjectPath(registered),
|
||||
);
|
||||
expect(
|
||||
await item.getProperty('org.kde.StatusNotifierItem', 'XAyatanaLabel'),
|
||||
isA<DBusString>().having((label) => label.value, 'value', '25:00'),
|
||||
);
|
||||
final menu = DBusRemoteObject(
|
||||
shell,
|
||||
name: name,
|
||||
path: DBusObjectPath('/MenuBar'),
|
||||
);
|
||||
final layout = await menu.callMethod(
|
||||
'com.canonical.dbusmenu',
|
||||
'GetLayout',
|
||||
[const DBusInt32(0), const DBusInt32(-1), DBusArray.string(const [])],
|
||||
replySignature: DBusSignature('u(ia{sv}av)'),
|
||||
);
|
||||
final children =
|
||||
((layout.values[1] as DBusStruct).children[2] as DBusArray).children;
|
||||
String labelOf(DBusValue child) =>
|
||||
(((child as DBusVariant).value as DBusStruct).children[1] as DBusDict)
|
||||
.children[const DBusString('label')]!
|
||||
.asVariant()
|
||||
.asString();
|
||||
expect(children.map(labelOf), [
|
||||
'Focus 1 of 4 · Classic',
|
||||
'Pause',
|
||||
'Skip',
|
||||
'Stop',
|
||||
]);
|
||||
|
||||
await menu.callMethod('com.canonical.dbusmenu', 'Event', [
|
||||
const DBusInt32(3),
|
||||
const DBusString('clicked'),
|
||||
const DBusVariant(DBusString('')),
|
||||
const DBusUint32(0),
|
||||
], replySignature: DBusSignature(''));
|
||||
expect(
|
||||
await pressed.timeout(const Duration(seconds: 5)),
|
||||
PomodoroAction.skip,
|
||||
);
|
||||
|
||||
await platform.clear();
|
||||
expect(
|
||||
await item.getProperty('org.kde.StatusNotifierItem', 'Status'),
|
||||
const DBusString('Passive'),
|
||||
);
|
||||
});
|
||||
|
||||
test('changes that come at once register the item once', () async {
|
||||
final run = PomodoroRun.ready(PomodoroProfile.classic);
|
||||
|
||||
await Future.wait([
|
||||
platform.show(notice(run.started(DateTime.now().toUtc()))),
|
||||
platform.show(notice(run)),
|
||||
]);
|
||||
|
||||
expect(watcher.registered, hasLength(1));
|
||||
});
|
||||
|
||||
test('the item registers again with a watcher that restarts', () async {
|
||||
final run = PomodoroRun.ready(PomodoroProfile.classic);
|
||||
await platform.show(notice(run));
|
||||
expect(watcher.registered, hasLength(1));
|
||||
|
||||
await shell.close();
|
||||
(watcher, shell) = await startWatcher();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
expect(watcher.registered, hasLength(1));
|
||||
});
|
||||
|
||||
test('suggests nothing', () async {
|
||||
expect(await platform.suggestsTopBar.first, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('without a top bar for it', () {
|
||||
late DBusClient gnomeShell;
|
||||
|
||||
setUp(() async {
|
||||
gnomeShell = DBusClient(address);
|
||||
await gnomeShell.requestName('org.gnome.Shell');
|
||||
});
|
||||
tearDown(() => gnomeShell.close());
|
||||
|
||||
test('suggests setting one up on GNOME, until dismissed', () async {
|
||||
final suggestions = <bool>[];
|
||||
final subscription = platform.suggestsTopBar.listen(suggestions.add);
|
||||
addTearDown(subscription.cancel);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
expect(suggestions, [true]);
|
||||
|
||||
await platform.dismissTopBarSuggestion();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
expect(suggestions, [true, false]);
|
||||
expect(await newPlatform().suggestsTopBar.first, isFalse);
|
||||
});
|
||||
|
||||
test('suggests nothing elsewhere than on GNOME', () async {
|
||||
await gnomeShell.close();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
expect(await newPlatform().suggestsTopBar.first, isFalse);
|
||||
});
|
||||
|
||||
test('stops suggesting once a watcher turns up', () async {
|
||||
final suggestions = <bool>[];
|
||||
final subscription = platform.suggestsTopBar.listen(suggestions.add);
|
||||
addTearDown(subscription.cancel);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
final (_, shell) = await startWatcher();
|
||||
addTearDown(shell.close);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
expect(suggestions.last, isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'setting up switches on the extension where it is installed',
|
||||
() async {
|
||||
final extensions = _Extensions({'ubuntu-appindicators@ubuntu.com'});
|
||||
await serverSide.requestName('org.gnome.Shell.Extensions');
|
||||
await serverSide.registerObject(extensions);
|
||||
|
||||
await platform.setUpTopBar();
|
||||
|
||||
expect(extensions.calls, [
|
||||
'GetExtensionInfo ubuntu-appindicators@ubuntu.com',
|
||||
'EnableExtension ubuntu-appindicators@ubuntu.com',
|
||||
]);
|
||||
expect(opened, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('setting up has GNOME install it where it is not', () async {
|
||||
final extensions = _Extensions({});
|
||||
await serverSide.requestName('org.gnome.Shell.Extensions');
|
||||
await serverSide.registerObject(extensions);
|
||||
|
||||
await platform.setUpTopBar();
|
||||
|
||||
expect(
|
||||
extensions.calls.last,
|
||||
'InstallRemoteExtension appindicatorsupport@rgcjonas.gmail.com',
|
||||
);
|
||||
expect(opened, isEmpty);
|
||||
});
|
||||
|
||||
test("setting up opens the extension's page where GNOME can't be "
|
||||
'asked', () async {
|
||||
await platform.setUpTopBar();
|
||||
|
||||
expect(opened.single.host, 'extensions.gnome.org');
|
||||
});
|
||||
});
|
||||
|
||||
test('an icon of the timer is drawn for the top bar', () {
|
||||
final icon = timerIcon(24);
|
||||
|
||||
expect(icon, hasLength(24 * 24 * 4));
|
||||
// The ring's top and the centre: ink and none.
|
||||
expect(icon[(6 * 24 + 12) * 4], greaterThan(200));
|
||||
expect(icon[(16 * 24 + 9) * 4], 0);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_run_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_run.dart';
|
||||
|
||||
void main() {
|
||||
late Directory folder;
|
||||
late File file;
|
||||
|
||||
setUp(() async {
|
||||
folder = await Directory.systemTemp.createTemp('pomodoro_run_test');
|
||||
file = File('${folder.path}/pomodoro.json');
|
||||
});
|
||||
tearDown(() => folder.delete(recursive: true));
|
||||
|
||||
test('keeps a timer until it is forgotten', () async {
|
||||
final run = PomodoroRun.ready(
|
||||
PomodoroProfile.classic,
|
||||
).started(DateTime.utc(2026, 10, 8, 9));
|
||||
|
||||
await FilePomodoroRunStore(file).write(run);
|
||||
expect(await FilePomodoroRunStore(file).read(), run);
|
||||
|
||||
await FilePomodoroRunStore(file).write(null);
|
||||
expect(await FilePomodoroRunStore(file).read(), isNull);
|
||||
await FilePomodoroRunStore(file).write(null);
|
||||
});
|
||||
|
||||
test('a damaged file is no timer', () async {
|
||||
await file.writeAsString('{"phase": 3}');
|
||||
|
||||
expect(await FilePomodoroRunStore(file).read(), isNull);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_run.dart';
|
||||
|
||||
void main() {
|
||||
final now = DateTime.utc(2026, 10, 8, 9);
|
||||
const classic = PomodoroProfile.classic;
|
||||
|
||||
group('the built-in profiles', () {
|
||||
test('Classic: 25 minutes of focus, 5 of break, 15 after four', () {
|
||||
expect(classic.focusMinutes, 25);
|
||||
expect(classic.shortBreakMinutes, 5);
|
||||
expect(classic.longBreakMinutes, 15);
|
||||
expect(classic.longBreakAfter, 4);
|
||||
expect(classic.autoStartFocus, isFalse);
|
||||
expect(classic.autoStartBreaks, isFalse);
|
||||
expect(classic.doNotDisturb, isFalse);
|
||||
});
|
||||
|
||||
test('52/17 has no long break', () {
|
||||
expect(PomodoroProfile.fiftyTwoSeventeen.focusMinutes, 52);
|
||||
expect(PomodoroProfile.fiftyTwoSeventeen.shortBreakMinutes, 17);
|
||||
expect(PomodoroProfile.fiftyTwoSeventeen.hasLongBreak, isFalse);
|
||||
});
|
||||
|
||||
test('are what a person starts with, Classic chosen', () {
|
||||
const settings = PomodoroSettings();
|
||||
|
||||
expect(settings.profiles, [
|
||||
PomodoroProfile.classic,
|
||||
PomodoroProfile.fiftyTwoSeventeen,
|
||||
]);
|
||||
expect(settings.selected, PomodoroProfile.classic);
|
||||
});
|
||||
|
||||
test('deleting the chosen profile chooses the first one left', () {
|
||||
const mine = PomodoroProfile(id: 'mine');
|
||||
const settings = PomodoroSettings(
|
||||
profiles: [PomodoroProfile.classic, mine],
|
||||
selectedId: 'mine',
|
||||
);
|
||||
|
||||
expect(settings.withoutProfile('mine').selectedId, 'classic');
|
||||
expect(
|
||||
settings.withSelected('classic').withoutProfile('mine').selectedId,
|
||||
'classic',
|
||||
);
|
||||
});
|
||||
|
||||
test('a chosen profile that is gone falls back to the first', () {
|
||||
const settings = PomodoroSettings(selectedId: 'gone');
|
||||
|
||||
expect(settings.selected, PomodoroProfile.classic);
|
||||
});
|
||||
});
|
||||
|
||||
group('a run', () {
|
||||
test('starts ready, paused at the full focus', () {
|
||||
final run = PomodoroRun.ready(classic);
|
||||
|
||||
expect(run.phase, PomodoroPhase.focus);
|
||||
expect(run.isRunning, isFalse);
|
||||
expect(run.leftAt(now), const Duration(minutes: 25));
|
||||
});
|
||||
|
||||
test('counts down while running, and keeps what is left while paused', () {
|
||||
final running = PomodoroRun.ready(classic).started(now);
|
||||
final later = now.add(const Duration(minutes: 10));
|
||||
|
||||
expect(running.isRunning, isTrue);
|
||||
expect(running.endsAt, now.add(const Duration(minutes: 25)));
|
||||
expect(running.leftAt(later), const Duration(minutes: 15));
|
||||
|
||||
final paused = running.paused(later);
|
||||
expect(paused.isRunning, isFalse);
|
||||
expect(
|
||||
paused.leftAt(later.add(const Duration(hours: 1))),
|
||||
const Duration(minutes: 15),
|
||||
);
|
||||
expect(
|
||||
paused.started(later).endsAt,
|
||||
later.add(const Duration(minutes: 15)),
|
||||
);
|
||||
});
|
||||
|
||||
test('after a focus comes a short break, and after four a long one', () {
|
||||
var run = PomodoroRun.ready(classic);
|
||||
final phases = <PomodoroPhase>[];
|
||||
for (var i = 0; i < 9; i++) {
|
||||
run = run.next(now);
|
||||
phases.add(run.phase);
|
||||
}
|
||||
|
||||
expect(phases, [
|
||||
PomodoroPhase.shortBreak,
|
||||
PomodoroPhase.focus,
|
||||
PomodoroPhase.shortBreak,
|
||||
PomodoroPhase.focus,
|
||||
PomodoroPhase.shortBreak,
|
||||
PomodoroPhase.focus,
|
||||
PomodoroPhase.longBreak,
|
||||
PomodoroPhase.focus,
|
||||
PomodoroPhase.shortBreak,
|
||||
]);
|
||||
});
|
||||
|
||||
test('a profile without a long break only ever takes short ones', () {
|
||||
var run = PomodoroRun.ready(PomodoroProfile.fiftyTwoSeventeen);
|
||||
for (var i = 0; i < 10; i++) {
|
||||
run = run.next(now);
|
||||
expect(run.phase, isNot(PomodoroPhase.longBreak));
|
||||
}
|
||||
});
|
||||
|
||||
test('the next phase waits, paused at its full length, unless the '
|
||||
'profile starts it by itself', () {
|
||||
final waiting = PomodoroRun.ready(classic).started(now).next(now);
|
||||
expect(waiting.isRunning, isFalse);
|
||||
expect(waiting.leftAt(now), const Duration(minutes: 5));
|
||||
|
||||
const breaksGoOn = PomodoroProfile(
|
||||
id: 'auto',
|
||||
autoStartBreaks: true,
|
||||
);
|
||||
final going = PomodoroRun.ready(breaksGoOn).started(now).next(now);
|
||||
expect(going.isRunning, isTrue);
|
||||
expect(going.endsAt, now.add(const Duration(minutes: 5)));
|
||||
expect(going.next(now).isRunning, isFalse, reason: 'focus waits');
|
||||
});
|
||||
|
||||
test('catching up ends the phases that ran out meanwhile, each from '
|
||||
'when the last one ended', () {
|
||||
const allGoOn = PomodoroProfile(
|
||||
id: 'auto',
|
||||
autoStartFocus: true,
|
||||
autoStartBreaks: true,
|
||||
);
|
||||
final run = PomodoroRun.ready(allGoOn).started(now);
|
||||
|
||||
// 25 + 5 + 25 minutes later, and 2 more into the second break.
|
||||
final (caughtUp, ended) = run.caughtUpTo(
|
||||
now.add(const Duration(minutes: 57)),
|
||||
);
|
||||
|
||||
expect(ended, PomodoroPhase.focus);
|
||||
expect(caughtUp.phase, PomodoroPhase.shortBreak);
|
||||
expect(
|
||||
caughtUp.leftAt(now.add(const Duration(minutes: 57))),
|
||||
const Duration(minutes: 3),
|
||||
);
|
||||
});
|
||||
|
||||
test('catching up stops at a phase that waits', () {
|
||||
final run = PomodoroRun.ready(classic).started(now);
|
||||
|
||||
final (caughtUp, ended) = run.caughtUpTo(
|
||||
now.add(const Duration(hours: 3)),
|
||||
);
|
||||
|
||||
expect(ended, PomodoroPhase.focus);
|
||||
expect(caughtUp.phase, PomodoroPhase.shortBreak);
|
||||
expect(caughtUp.isRunning, isFalse);
|
||||
});
|
||||
|
||||
test('catching up before the end changes nothing', () {
|
||||
final run = PomodoroRun.ready(classic).started(now);
|
||||
|
||||
expect(run.caughtUpTo(now.add(const Duration(minutes: 3))), (
|
||||
run,
|
||||
null,
|
||||
));
|
||||
});
|
||||
|
||||
test("shows where it is in the profile's cycle", () {
|
||||
final run = PomodoroRun.ready(classic).next(now).next(now);
|
||||
|
||||
expect(run.cycle, [
|
||||
PomodoroPhase.focus,
|
||||
PomodoroPhase.shortBreak,
|
||||
PomodoroPhase.focus,
|
||||
PomodoroPhase.shortBreak,
|
||||
PomodoroPhase.focus,
|
||||
PomodoroPhase.shortBreak,
|
||||
PomodoroPhase.focus,
|
||||
PomodoroPhase.longBreak,
|
||||
]);
|
||||
expect(run.indexInCycle, 2);
|
||||
expect(
|
||||
run.elapsedInCycleAt(now),
|
||||
const Duration(minutes: 30),
|
||||
reason: 'a focus and a break done',
|
||||
);
|
||||
});
|
||||
|
||||
test('a profile changed under way keeps where the timer is within its '
|
||||
'cycle', () {
|
||||
var run = PomodoroRun.ready(PomodoroProfile.fiftyTwoSeventeen);
|
||||
for (var i = 0; i < 10; i++) {
|
||||
run = run.next(now);
|
||||
}
|
||||
final withLongBreaks = run.withProfile(
|
||||
PomodoroProfile.fiftyTwoSeventeen.copyWith(
|
||||
longBreakMinutes: 30,
|
||||
longBreakAfter: 4,
|
||||
),
|
||||
);
|
||||
|
||||
expect(withLongBreaks.phase, PomodoroPhase.focus);
|
||||
expect(
|
||||
withLongBreaks.indexInCycle,
|
||||
lessThan(withLongBreaks.cycle.length),
|
||||
);
|
||||
expect(withLongBreaks.focusDone, lessThan(4));
|
||||
final cycleLength = withLongBreaks.cycle.fold(
|
||||
Duration.zero,
|
||||
(sum, phase) => sum + withLongBreaks.lengthIn(phase),
|
||||
);
|
||||
expect(
|
||||
withLongBreaks.elapsedInCycleAt(now),
|
||||
lessThanOrEqualTo(cycleLength),
|
||||
);
|
||||
});
|
||||
|
||||
test('a phase waiting at its full length takes the length of the changed '
|
||||
'profile; one under way keeps its end', () {
|
||||
final waiting = PomodoroRun.ready(classic);
|
||||
expect(
|
||||
waiting.withProfile(classic.copyWith(focusMinutes: 40)).leftAt(now),
|
||||
const Duration(minutes: 40),
|
||||
);
|
||||
|
||||
final running = waiting.started(now);
|
||||
expect(
|
||||
running.withProfile(classic.copyWith(focusMinutes: 40)).endsAt,
|
||||
running.endsAt,
|
||||
);
|
||||
});
|
||||
|
||||
test('a countdown past its end shows no time left', () {
|
||||
expect(formatPomodoroLeft(const Duration(seconds: -13)), '00:00');
|
||||
expect(formatPomodoroLeft(const Duration(minutes: 61)), '1:01:00');
|
||||
});
|
||||
|
||||
test('survives being kept and read back', () {
|
||||
final run = PomodoroRun.ready(classic).next(now).started(now);
|
||||
|
||||
expect(PomodoroRun.fromJson(run.toJson()), run);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_platform.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_run.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/widget/pomodoro_chip.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../../support/fake_pomodoro_platform.dart';
|
||||
import '../../../support/fake_settings_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
import '../../../support/memory_pomodoro_run_store.dart';
|
||||
import '../../../support/pump_app.dart';
|
||||
import '../../../support/test_dependencies.dart';
|
||||
|
||||
void main() {
|
||||
late FakePomodoroPlatform platform;
|
||||
|
||||
setUp(() => platform = FakePomodoroPlatform());
|
||||
|
||||
Future<void> showHome(WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(1000, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
pomodoro: platform,
|
||||
settingsGateway: FakeSettingsGateway(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tap(WidgetTester tester, Finder finder) async {
|
||||
await tester.tap(finder);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets('Home starts the chosen profile, and the system shows the '
|
||||
'timer', (tester) async {
|
||||
await showHome(tester);
|
||||
expect(find.text('Pomodoro'), findsOneWidget);
|
||||
expect(find.text('Classic'), findsOneWidget);
|
||||
expect(find.text('25:00'), findsOneWidget);
|
||||
|
||||
await tap(tester, find.text('Start'));
|
||||
|
||||
expect(find.text('Pause'), findsOneWidget);
|
||||
expect(find.text('Skip'), findsOneWidget);
|
||||
expect(find.text('Stop'), findsOneWidget);
|
||||
expect(platform.showing?.title, 'Focus 1 of 4');
|
||||
expect(platform.showing?.text, 'Classic');
|
||||
expect(platform.showing?.isRunning, isTrue);
|
||||
expect(platform.showing?.alertTitle, 'Time for a break');
|
||||
expect(
|
||||
platform.showing?.alertText,
|
||||
'Short break is ready when you are.',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
expect(find.text('24:57'), findsOneWidget, reason: 'counting down');
|
||||
});
|
||||
|
||||
testWidgets("the system's buttons act on the timer", (tester) async {
|
||||
await showHome(tester);
|
||||
await tap(tester, find.text('Start'));
|
||||
|
||||
platform.press(PomodoroAction.pause);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.text('Resume'), findsOneWidget);
|
||||
expect(platform.showing?.isRunning, isFalse);
|
||||
expect(platform.showing?.text, 'Paused · Classic');
|
||||
|
||||
platform.press(PomodoroAction.skip);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(platform.showing?.phase, PomodoroPhase.shortBreak);
|
||||
|
||||
platform.press(PomodoroAction.stop);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.text('Start'), findsOneWidget);
|
||||
expect(platform.showing, isNull);
|
||||
});
|
||||
|
||||
testWidgets('another profile is chosen on Home', (tester) async {
|
||||
await showHome(tester);
|
||||
|
||||
await tester.tap(find.text('Classic'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('52/17').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('52:00'), findsOneWidget);
|
||||
expect(find.text('52 min focus, 17 min break'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a profile is added, kept and chosen', (tester) async {
|
||||
await showHome(tester);
|
||||
|
||||
await tester.tap(find.byTooltip('Pomodoro profiles'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Add a profile'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.widgetWithText(TextField, 'Name'), 'Exams');
|
||||
await tester.tap(find.byTooltip('Increase').first);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Exams'), findsOneWidget);
|
||||
expect(
|
||||
find.text('30 min focus, 5 min break, 15 min after 4'),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(find.text('Exams'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Close'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('30:00'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('keeping quiet while focusing asks for the permission first', (
|
||||
tester,
|
||||
) async {
|
||||
await showHome(tester);
|
||||
await tester.tap(find.byTooltip('Pomodoro profiles'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Edit profile').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final quiet = find.widgetWithText(
|
||||
SwitchListTile,
|
||||
'Do Not Disturb while focusing',
|
||||
);
|
||||
await tester.ensureVisible(quiet);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(quiet);
|
||||
await tester.pumpAndSettle();
|
||||
expect(platform.askedForQuiet, 1);
|
||||
expect(tester.widget<SwitchListTile>(quiet).value, isFalse);
|
||||
expect(
|
||||
find.textContaining('may not keep this device quiet'),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
platform.allowsQuiet = true;
|
||||
await tester.tap(quiet);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<SwitchListTile>(quiet).value, isTrue);
|
||||
});
|
||||
|
||||
testWidgets("allowed in the system's settings, keeping quiet switches on "
|
||||
'by itself on coming back', (tester) async {
|
||||
await showHome(tester);
|
||||
await tester.tap(find.byTooltip('Pomodoro profiles'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Edit profile').first);
|
||||
await tester.pumpAndSettle();
|
||||
final quiet = find.widgetWithText(
|
||||
SwitchListTile,
|
||||
'Do Not Disturb while focusing',
|
||||
);
|
||||
await tester.ensureVisible(quiet);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(quiet);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<SwitchListTile>(quiet).value, isFalse);
|
||||
|
||||
// The person allows it in the system's settings, and comes back.
|
||||
platform.allowsQuiet = true;
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.widget<SwitchListTile>(quiet).value, isTrue);
|
||||
expect(find.textContaining('may not keep this device quiet'), findsNothing);
|
||||
expect(platform.askedForQuiet, 1, reason: 'asked once, not again');
|
||||
});
|
||||
|
||||
testWidgets('coming back without allowing it leaves keeping quiet off', (
|
||||
tester,
|
||||
) async {
|
||||
await showHome(tester);
|
||||
await tester.tap(find.byTooltip('Pomodoro profiles'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Edit profile').first);
|
||||
await tester.pumpAndSettle();
|
||||
final quiet = find.widgetWithText(
|
||||
SwitchListTile,
|
||||
'Do Not Disturb while focusing',
|
||||
);
|
||||
await tester.ensureVisible(quiet);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(quiet);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.widget<SwitchListTile>(quiet).value, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('suggests setting up the top bar where it shows no icons, '
|
||||
'until dismissed', (tester) async {
|
||||
platform.suggestTopBar(suggest: true);
|
||||
await showHome(tester);
|
||||
|
||||
expect(
|
||||
find.textContaining("GNOME's top bar"),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(find.text('Set up'));
|
||||
await tester.pump();
|
||||
expect(platform.topBarSetUps, 1);
|
||||
|
||||
await tester.tap(find.text("Don't suggest again"));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining("GNOME's top bar"), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('suggests nothing where the top bar shows the timer', (
|
||||
tester,
|
||||
) async {
|
||||
await showHome(tester);
|
||||
|
||||
expect(find.textContaining("GNOME's top bar"), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the timer shows while studying, only to watch', (
|
||||
tester,
|
||||
) async {
|
||||
final now = clock.now().toUtc();
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
pomodoro: platform,
|
||||
studysetGateway: FakeStudysetGateway()
|
||||
..receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
),
|
||||
pomodoroRuns: MemoryPomodoroRunStore(
|
||||
PomodoroRun.ready(PomodoroProfile.classic).started(now),
|
||||
),
|
||||
),
|
||||
location: RoutePaths.study('japanese'),
|
||||
);
|
||||
|
||||
final chip = find.byType(PomodoroChip);
|
||||
expect(
|
||||
find.descendant(of: chip, matching: find.textContaining(':')),
|
||||
findsOneWidget,
|
||||
);
|
||||
// Only to watch: a tap leaves the timer running.
|
||||
await tester.tap(chip);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(platform.showing?.isRunning, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('the timer stays out of studying where the device says so', (
|
||||
tester,
|
||||
) async {
|
||||
final now = clock.now().toUtc();
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
pomodoro: platform,
|
||||
deviceSettings: const DeviceSettings(pomodoroWhileStudying: false),
|
||||
studysetGateway: FakeStudysetGateway()
|
||||
..receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
),
|
||||
pomodoroRuns: MemoryPomodoroRunStore(
|
||||
PomodoroRun.ready(PomodoroProfile.classic).started(now),
|
||||
),
|
||||
),
|
||||
location: RoutePaths.study('japanese'),
|
||||
);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(PomodoroChip),
|
||||
matching: find.textContaining(':'),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue