mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 14:07:41 +01:00
The rest banner used to be inserted into the column only while a break ran, so its arrival took height from the exercise list, shifted every tile down and pushed the last one into scroll range. The strip is now a permanently reserved 64 dp slot (muted "Rest / 00:00" while idle, Skip keeps its space), the list is a non-scrolling column that scales down as a whole if it would not fit (0.96 on the Pixel 6a with four tiles, pinned by a widget test at the phone's logical size with the status bar simulated), and the activity is locked to portrait. Verified on the real phone: only the tapped circle's pixels change when a rest starts. Verifying that needed a workout screen that today's already-logged workout did not offer, so the app gained a `sandbox` product flavor beside `daily`: com.kuhy.workout_app.sandbox installs side by side with its own data, refuses every network connection at the socket layer, starts no LAN server for the PC to pull from, keeps its /sdcard files beside the daily build's, wears a red SANDBOX ribbon, and offers a SANDBOX section in Settings (wipe, mark today not done, rest length in seconds). MainActivity answers the flavor and carries a `WorkoutSandbox` logcat trace of every tap, break, save and navigation. On Linux the same mode is WORKOUT_SANDBOX=1, driven under Xvfb by scripts/run_sandbox_xvfb.sh. CI builds --flavor daily; .phone-deploy-flavor tells phone_deploy.sh the same. Set circles and the home icon buttons carry semantics labels so they can be addressed by element (and by screen readers). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YbBZqUaNWyhHYfzz6zGC7q
80 lines
2.8 KiB
Dart
80 lines
2.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
|
import 'package:workout_app/main.dart';
|
|
import 'package:workout_app/sandbox/sandbox.dart';
|
|
import 'package:workout_app/services/http_server_service.dart';
|
|
import 'package:workout_app/services/storage_service.dart';
|
|
|
|
import 'fake_secure_storage.dart';
|
|
|
|
void main() {
|
|
setUpAll(() {
|
|
sqfliteFfiInit();
|
|
databaseFactory = databaseFactoryFfi;
|
|
});
|
|
|
|
setUp(() async {
|
|
// These tests build the whole WorkoutApp, so they cannot inject a sync
|
|
// probe the way home_screen_test does. The home screen now syncs in the
|
|
// background on open, which reaches FlutterSecureStorage -- whose real
|
|
// platform channel throws under `flutter test`. Install the fake instead
|
|
// of letting an unrelated MissingPluginException fail these.
|
|
installFakeSecureStorage();
|
|
StorageService.resetForTesting();
|
|
await StorageService.init();
|
|
});
|
|
|
|
testWidgets('WorkoutApp renders HomeScreen', (tester) async {
|
|
await tester.runAsync(() async {
|
|
await tester.pumpWidget(const WorkoutApp());
|
|
await Future<void>.delayed(const Duration(milliseconds: 300));
|
|
});
|
|
await tester.pump();
|
|
expect(find.text('Workout Tracker'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the sandbox flavor wears its ribbon on every screen', (
|
|
tester,
|
|
) async {
|
|
Sandbox.enabled = true;
|
|
addTearDown(() => Sandbox.enabled = false);
|
|
await tester.runAsync(() async {
|
|
await tester.pumpWidget(const WorkoutApp());
|
|
await Future<void>.delayed(const Duration(milliseconds: 300));
|
|
});
|
|
await tester.pump();
|
|
expect(find.byType(Banner), findsOneWidget);
|
|
expect(tester.widget<Banner>(find.byType(Banner)).message, 'SANDBOX');
|
|
});
|
|
|
|
testWidgets('the daily build has no ribbon', (tester) async {
|
|
await tester.runAsync(() async {
|
|
await tester.pumpWidget(const WorkoutApp());
|
|
await Future<void>.delayed(const Duration(milliseconds: 300));
|
|
});
|
|
await tester.pump();
|
|
expect(find.byType(Banner), findsNothing);
|
|
});
|
|
|
|
testWidgets('app-lifecycle changes drive the HTTP server', (tester) async {
|
|
await tester.runAsync(() async {
|
|
await tester.pumpWidget(const WorkoutApp());
|
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
// A valid forward transition path that visits every handled state.
|
|
for (final state in const [
|
|
AppLifecycleState.resumed,
|
|
AppLifecycleState.inactive,
|
|
AppLifecycleState.hidden,
|
|
AppLifecycleState.paused,
|
|
AppLifecycleState.detached,
|
|
]) {
|
|
tester.binding.handleAppLifecycleStateChanged(state);
|
|
await Future<void>.delayed(const Duration(milliseconds: 5));
|
|
}
|
|
await HttpServerService.instance.stop();
|
|
});
|
|
await tester.pump();
|
|
});
|
|
}
|