screen-locker/stronglift_replacement/workout_app/test/sandbox/sandbox_test.dart
Krzysztof kuhy Rudnicki 83d77884a0 Workout screen never moves when a rest starts; a sandbox flavor to prove it on the phone
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
2026-09-20 18:55:08 +02:00

72 lines
2.2 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:workout_app/sandbox/sandbox.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
void answer(Object? Function(MethodCall call)? handler) {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
Sandbox.channel,
handler == null ? null : (call) async => handler(call),
);
}
final printed = <String>[];
setUp(() {
printed.clear();
debugPrint = (String? message, {int? wrapWidth}) => printed.add(message ?? '');
Sandbox.enabled = false;
Sandbox.restSecs = Sandbox.defaultRestSecs;
});
tearDown(() {
debugPrint = debugPrintThrottled;
answer(null);
Sandbox.enabled = false;
});
test('init takes the flavor from MainActivity', () async {
answer((call) => call.method == 'isSandbox');
await Sandbox.init();
expect(Sandbox.enabled, isTrue);
});
test('init treats a null answer as the daily build', () async {
answer((_) => null);
await Sandbox.init();
expect(Sandbox.enabled, isFalse);
});
test('init falls back to the environment where there is no channel', () async {
answer(null);
await Sandbox.init({Sandbox.envVar: '1'});
expect(Sandbox.enabled, isTrue);
expect(printed.single, contains('WORKOUT_SANDBOX=1: sandbox'));
await Sandbox.init({});
expect(Sandbox.enabled, isFalse);
expect(printed.last, contains('unset: daily build'));
});
test('init reads the real environment by default', () async {
answer(null);
await Sandbox.init();
// Whatever the host env says, it must not throw and must report it.
expect(printed.single, contains('no sandbox channel'));
});
test('rest and label pass through for the daily build', () {
expect(Sandbox.rest(180), 180);
expect(Sandbox.restLabel('Rest (3 min — well done!)'), 'Rest (3 min — well done!)');
});
test('rest and label are overridden in the sandbox', () {
Sandbox.enabled = true;
Sandbox.restSecs = 9;
expect(Sandbox.rest(180), 9);
expect(Sandbox.restLabel('Rest (3 min — well done!)'), 'Rest (9 s — sandbox)');
});
}