diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 619f583af3..e43f6675e8 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -176,7 +176,7 @@ class GamePlayer extends StatelessWidget { Expanded( flex: 7, child: Padding( - padding: const EdgeInsets.only(right: 20), + padding: const EdgeInsets.only(right: 16.0), child: ConfirmMove( onConfirm: confirmMoveCallbacks!.confirm, onCancel: confirmMoveCallbacks!.cancel, @@ -187,7 +187,7 @@ class GamePlayer extends StatelessWidget { Expanded( flex: 7, child: Padding( - padding: const EdgeInsets.only(right: 20), + padding: const EdgeInsets.only(right: 16.0), child: shouldLinkToUserProfile ? GestureDetector( onTap: player.user != null diff --git a/lib/src/view/puzzle/puzzle_feedback_widget.dart b/lib/src/view/puzzle/puzzle_feedback_widget.dart index 51d24b4351..0b0cf7b568 100644 --- a/lib/src/view/puzzle/puzzle_feedback_widget.dart +++ b/lib/src/view/puzzle/puzzle_feedback_widget.dart @@ -52,17 +52,27 @@ class PuzzleFeedbackWidget extends ConsumerWidget { letterSpacing: 2.0, ), textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, ) : Text( state.result == PuzzleResult.win ? context.l10n.puzzlePuzzleSuccess : context.l10n.puzzlePuzzleComplete, + overflow: TextOverflow.ellipsis, ), subtitle: onStreak && state.result == PuzzleResult.lose ? null : RatingPrefAware( - orElse: Text('$playedXTimes.'), - child: Text('$puzzleRating. $playedXTimes.'), + orElse: Text( + '$playedXTimes.', + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + child: Text( + '$puzzleRating. $playedXTimes.', + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), ), ); case PuzzleMode.load: @@ -74,8 +84,15 @@ class PuzzleFeedbackWidget extends ConsumerWidget { size: 36, color: context.lichessColors.error, ), - title: Text(context.l10n.puzzleNotTheMove), - subtitle: Text(context.l10n.puzzleTrySomethingElse), + title: Text( + context.l10n.puzzleNotTheMove, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + context.l10n.puzzleTrySomethingElse, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), ); } else if (state.feedback == PuzzleFeedback.good) { return _FeedbackTile( @@ -104,11 +121,16 @@ class PuzzleFeedbackWidget extends ConsumerWidget { ), ), ), - title: Text(context.l10n.yourTurn), + title: Text( + context.l10n.yourTurn, + overflow: TextOverflow.ellipsis, + ), subtitle: Text( state.pov == Side.white ? context.l10n.puzzleFindTheBestMoveForWhite : context.l10n.puzzleFindTheBestMoveForBlack, + overflow: TextOverflow.ellipsis, + maxLines: 2, ), ); } @@ -135,23 +157,25 @@ class _FeedbackTile extends StatelessWidget { children: [ if (leading != null) ...[ leading!, - const SizedBox(width: 18), + const SizedBox(width: 16.0), ], - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - DefaultTextStyle.merge( - style: TextStyle( - fontSize: - defaultFontSize != null ? defaultFontSize * 1.2 : null, - fontWeight: FontWeight.bold, + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + DefaultTextStyle.merge( + style: TextStyle( + fontSize: + defaultFontSize != null ? defaultFontSize * 1.2 : null, + fontWeight: FontWeight.bold, + ), + child: title, ), - child: title, - ), - if (subtitle != null) subtitle!, - ], + if (subtitle != null) subtitle!, + ], + ), ), ], ); diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 1de8bc6f19..c9cba27a51 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -237,11 +237,13 @@ class _BoardTableState extends ConsumerState { mainAxisSize: MainAxisSize.max, children: [ Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), + padding: isTablet + ? const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, child: Row( children: [ boardWidget, @@ -258,8 +260,9 @@ class _BoardTableState extends ConsumerState { Flexible( fit: FlexFit.loose, child: Padding( - padding: - const EdgeInsets.all(kTabletBoardTableSidePadding), + padding: isTablet + ? const EdgeInsets.all(kTabletBoardTableSidePadding) + : EdgeInsets.zero, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.spaceAround, diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index e562e0ce7a..cfae7ca1ce 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -154,6 +154,10 @@ class _CountdownClockState extends ConsumerState { } } +const _kClockFontSize = 26.0; +const _kClockTenthFontSize = 20.0; +const _kClockHundredsFontSize = 18.0; + /// A stateless widget that displays the time left on the clock. /// /// For a clock widget that automatically counts down, see [CountdownClock]. @@ -204,56 +208,68 @@ class Clock extends StatelessWidget { ? ClockStyle.darkThemeStyle : ClockStyle.lightThemeStyle); - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(5.0)), - color: active - ? isEmergency - ? activeClockStyle.emergencyBackgroundColor - : activeClockStyle.activeBackgroundColor - : activeClockStyle.backgroundColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), - child: MediaQuery.withClampedTextScaling( - maxScaleFactor: kMaxClockTextScaleFactor, - child: RichText( - text: TextSpan( - text: hours > 0 - ? '$hoursDisplay:${mins.toString().padLeft(2, '0')}:$secs' - : '$minsDisplay:$secs', - style: TextStyle( - color: active - ? isEmergency - ? activeClockStyle.emergencyTextColor - : activeClockStyle.activeTextColor - : activeClockStyle.textColor, - fontSize: 26, - height: - remainingHeight < kSmallRemainingHeightLeftBoardThreshold + return LayoutBuilder( + builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + // TODO improve this + final fontScaleFactor = maxWidth < 90 ? 0.8 : 1.0; + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(5.0)), + color: active + ? isEmergency + ? activeClockStyle.emergencyBackgroundColor + : activeClockStyle.activeBackgroundColor + : activeClockStyle.backgroundColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), + child: MediaQuery.withClampedTextScaling( + maxScaleFactor: kMaxClockTextScaleFactor, + child: RichText( + text: TextSpan( + text: hours > 0 + ? '$hoursDisplay:${mins.toString().padLeft(2, '0')}:$secs' + : '$minsDisplay:$secs', + style: TextStyle( + color: active + ? isEmergency + ? activeClockStyle.emergencyTextColor + : activeClockStyle.activeTextColor + : activeClockStyle.textColor, + fontSize: _kClockFontSize * fontScaleFactor, + height: remainingHeight < + kSmallRemainingHeightLeftBoardThreshold ? 1.0 : null, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - children: [ - if (showTenths) - TextSpan( - text: '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', - style: const TextStyle(fontSize: 20), + fontFeatures: const [ + FontFeature.tabularFigures(), + ], ), - if (!active && timeLeft < const Duration(seconds: 1)) - TextSpan( - text: - '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', - style: const TextStyle(fontSize: 18), - ), - ], + children: [ + if (showTenths) + TextSpan( + text: + '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', + style: TextStyle( + fontSize: _kClockTenthFontSize * fontScaleFactor, + ), + ), + if (!active && timeLeft < const Duration(seconds: 1)) + TextSpan( + text: + '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', + style: TextStyle( + fontSize: _kClockHundredsFontSize * fontScaleFactor, + ), + ), + ], + ), + ), ), ), - ), - ), + ); + }, ); } } diff --git a/test/test_helpers.dart b/test/test_helpers.dart index 867f114d3c..448d85578c 100644 --- a/test/test_helpers.dart +++ b/test/test_helpers.dart @@ -10,7 +10,7 @@ import 'package:http/http.dart' as http; const double _kTestScreenWidth = 390.0; const double _kTestScreenHeight = 844.0; -/// iPhone 14 screen size as default test surface size +/// iPhone 14 screen size. const kTestSurfaceSize = Size(_kTestScreenWidth, _kTestScreenHeight); const kPlatformVariant = @@ -19,6 +19,30 @@ const kPlatformVariant = Matcher sameRequest(http.BaseRequest request) => _SameRequest(request); Matcher sameHeaders(Map headers) => _SameHeaders(headers); +/// Mocks a surface with a given size. +class TestSurface extends StatelessWidget { + const TestSurface({ + required this.child, + required this.size, + super.key, + }); + + final Size size; + final Widget child; + + @override + Widget build(BuildContext context) { + return MediaQuery( + data: MediaQueryData(size: size), + child: SizedBox( + width: size.width, + height: size.height, + child: child, + ), + ); + } +} + /// Mocks an http response Future mockResponse( String body, diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index c56d49ea5b..8410a60edc 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -107,7 +107,7 @@ Future makeOfflineTestProviderScope( /// /// The [child] widget is the widget we want to test. It will be wrapped in a /// [MediaQuery.new] widget, to simulate a device with a specific size, controlled -/// by [kTestSurfaceSize]. +/// by [surfaceSize] (which default to [kTestSurfaceSize]). /// /// The [overrides] parameter can be used to override any provider in the app. /// The [userSession] parameter can be used to set the initial user session state. @@ -118,12 +118,17 @@ Future makeTestProviderScope( List? overrides, AuthSessionState? userSession, Map? defaultPreferences, + Size surfaceSize = kTestSurfaceSize, + Key? key, }) async { final binding = TestLichessBinding.ensureInitialized(); addTearDown(binding.reset); - await tester.binding.setSurfaceSize(kTestSurfaceSize); + await tester.binding.setSurfaceSize(surfaceSize); + addTearDown(() { + tester.binding.setSurfaceSize(null); + }); VisibilityDetectorController.instance.updateInterval = Duration.zero; @@ -157,6 +162,7 @@ Future makeTestProviderScope( FlutterError.onError = _ignoreOverflowErrors; return ProviderScope( + key: key, overrides: [ // ignore: scoped_providers_should_specify_dependencies notificationDisplayProvider.overrideWith((ref) { @@ -220,15 +226,9 @@ Future makeTestProviderScope( }), ...overrides ?? [], ], - child: MediaQuery( - data: const MediaQueryData(size: kTestSurfaceSize), - child: Center( - child: SizedBox( - width: kTestSurfaceSize.width, - height: kTestSurfaceSize.height, - child: child, - ), - ), + child: TestSurface( + size: surfaceSize, + child: child, ), ); } diff --git a/test/widgets/board_table_test.dart b/test/widgets/board_table_test.dart new file mode 100644 index 0000000000..afbc518529 --- /dev/null +++ b/test/widgets/board_table_test.dart @@ -0,0 +1,99 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/widgets/board_table.dart'; + +import '../test_helpers.dart'; +import '../test_provider_scope.dart'; + +const surfaces = [ + // https://www.browserstack.com/guide/common-screen-resolutions + // phones + Size(360, 800), + Size(390, 844), + Size(393, 873), + Size(412, 915), + Size(414, 896), + Size(360, 780), + // tablets + Size(600, 1024), + Size(810, 1080), + Size(820, 1180), + Size(1280, 800), + Size(800, 1280), + Size(601, 962), + // folded motorola + Size(564.7, 482.6), +]; + +void main() { + testWidgets( + 'board background size should match board size on all surfaces', + (WidgetTester tester) async { + for (final surface in surfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: const MaterialApp( + home: BoardTable( + orientation: Side.white, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + topTable: Padding( + padding: EdgeInsets.all(8.0), + child: Text('Top table'), + ), + bottomTable: Padding( + padding: EdgeInsets.all(8.0), + child: Text('Bottom table'), + ), + ), + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); + + final backgroundSize = tester.getSize( + find.byType(SolidColorChessboardBackground), + ); + + expect( + backgroundSize.width, + backgroundSize.height, + reason: 'Board background size is square', + ); + + final boardSize = tester.getSize(find.byType(Chessboard)); + + expect( + boardSize.width, + boardSize.height, + reason: 'Board size is square', + ); + + expect( + boardSize, + backgroundSize, + reason: 'Board size should match background size', + ); + + final isLandscape = surface.aspectRatio > 1.0; + final isTablet = surface.shortestSide > 600; + + final expectedBoardSize = isLandscape + ? isTablet + ? surface.height - 32.0 + : surface.height + : isTablet + ? surface.width - 32.0 + : surface.width; + + expect( + boardSize, + Size(expectedBoardSize, expectedBoardSize), + ); + } + }, + variant: kPlatformVariant, + ); +}