From a3c7a97a335008eaa3db4d5ff3bb7f2c2b382079 Mon Sep 17 00:00:00 2001 From: BB Studio <282851981+bbstudioapp@users.noreply.github.com> Date: Sat, 30 May 2026 14:59:10 +0200 Subject: [PATCH] =?UTF-8?q?feat(career):=20UI=20breaks=20sc=C3=A9naris?= =?UTF-8?q?=C3=A9s=20=E2=80=94=20overlay=20PAUSE=20+=20indicateur=20postur?= =?UTF-8?q?e=20(issue=20#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rhythm_coach/lib/l10n/app_de.arb | 7 + rhythm_coach/lib/l10n/app_en.arb | 7 + rhythm_coach/lib/l10n/app_es.arb | 7 + rhythm_coach/lib/l10n/app_fr.arb | 7 + rhythm_coach/lib/l10n/app_localizations.dart | 42 ++++++ .../lib/l10n/app_localizations_de.dart | 21 +++ .../lib/l10n/app_localizations_en.dart | 21 +++ .../lib/l10n/app_localizations_es.dart | 21 +++ .../lib/l10n/app_localizations_fr.dart | 21 +++ rhythm_coach/lib/l10n/enum_labels.dart | 17 +++ rhythm_coach/lib/screens/session_screen.dart | 78 ++++++++++ .../lib/widgets/posture_indicator.dart | 142 ++++++++++++++++++ rhythm_coach/test/posture_indicator_test.dart | 41 +++++ 13 files changed, 432 insertions(+) create mode 100644 rhythm_coach/lib/widgets/posture_indicator.dart create mode 100644 rhythm_coach/test/posture_indicator_test.dart diff --git a/rhythm_coach/lib/l10n/app_de.arb b/rhythm_coach/lib/l10n/app_de.arb index 4e6ee17..9450761 100644 --- a/rhythm_coach/lib/l10n/app_de.arb +++ b/rhythm_coach/lib/l10n/app_de.arb @@ -53,6 +53,7 @@ "sessionStatePaused": "PAUSE", "sessionStateFinished": "FERTIG", "sessionStateFailing": "FAIL", + "sessionBreakBanner": "PAUSE", "sessionFailPhasePhrase": "Fail-Satz", "sessionFailPhaseBreath": "Atmen", "sessionFailPhasePunishment": "Strafe", @@ -668,6 +669,12 @@ "positionThroat": "Kehle", "positionFull": "Ganz", "positionBalls": "Hoden", + "postureFree": "Frei", + "postureSitting": "Sitzend", + "postureStanding": "Stehend", + "postureKneeling": "Kniend", + "postureAllFours": "Auf allen vieren", + "postureOnBack": "Auf dem Rücken", "modeShortRhythm": "LUTSCH", "modeShortHold": "TIEF", diff --git a/rhythm_coach/lib/l10n/app_en.arb b/rhythm_coach/lib/l10n/app_en.arb index bba4854..2eb3e21 100644 --- a/rhythm_coach/lib/l10n/app_en.arb +++ b/rhythm_coach/lib/l10n/app_en.arb @@ -53,6 +53,7 @@ "sessionStatePaused": "PAUSED", "sessionStateFinished": "DONE", "sessionStateFailing": "FAIL", + "sessionBreakBanner": "BREAK", "sessionFailPhasePhrase": "Fail phrase", "sessionFailPhaseBreath": "Breath", "sessionFailPhasePunishment": "Punishment", @@ -668,6 +669,12 @@ "positionThroat": "Throat", "positionFull": "Full", "positionBalls": "Balls", + "postureFree": "Free", + "postureSitting": "Seated", + "postureStanding": "Standing", + "postureKneeling": "Kneeling", + "postureAllFours": "On all fours", + "postureOnBack": "On your back", "modeShortRhythm": "SUCK", "modeShortHold": "DEEP", diff --git a/rhythm_coach/lib/l10n/app_es.arb b/rhythm_coach/lib/l10n/app_es.arb index 05e316c..1079a12 100644 --- a/rhythm_coach/lib/l10n/app_es.arb +++ b/rhythm_coach/lib/l10n/app_es.arb @@ -53,6 +53,7 @@ "sessionStatePaused": "PAUSADA", "sessionStateFinished": "HECHO", "sessionStateFailing": "FALLO", + "sessionBreakBanner": "PAUSA", "sessionFailPhasePhrase": "Frase de fallo", "sessionFailPhaseBreath": "Respiración", "sessionFailPhasePunishment": "Castigo", @@ -668,6 +669,12 @@ "positionThroat": "Garganta", "positionFull": "Fondo", "positionBalls": "Huevos", + "postureFree": "Libre", + "postureSitting": "Sentada", + "postureStanding": "De pie", + "postureKneeling": "De rodillas", + "postureAllFours": "A cuatro patas", + "postureOnBack": "Boca arriba", "modeShortRhythm": "MAMAR", "modeShortHold": "PROF.", diff --git a/rhythm_coach/lib/l10n/app_fr.arb b/rhythm_coach/lib/l10n/app_fr.arb index 7138351..29a80b4 100644 --- a/rhythm_coach/lib/l10n/app_fr.arb +++ b/rhythm_coach/lib/l10n/app_fr.arb @@ -53,6 +53,7 @@ "sessionStatePaused": "PAUSE", "sessionStateFinished": "TERMINÉ", "sessionStateFailing": "FAIL", + "sessionBreakBanner": "PAUSE", "sessionFailPhasePhrase": "Phrase de fail", "sessionFailPhaseBreath": "Respiration", "sessionFailPhasePunishment": "Punition", @@ -668,6 +669,12 @@ "positionThroat": "Gorge", "positionFull": "Tout", "positionBalls": "Couilles", + "postureFree": "Libre", + "postureSitting": "Assise", + "postureStanding": "Debout", + "postureKneeling": "À genoux", + "postureAllFours": "À quatre pattes", + "postureOnBack": "Sur le dos", "modeShortRhythm": "SUCE", "modeShortHold": "AU FOND", diff --git a/rhythm_coach/lib/l10n/app_localizations.dart b/rhythm_coach/lib/l10n/app_localizations.dart index 679f958..08ebfa4 100644 --- a/rhythm_coach/lib/l10n/app_localizations.dart +++ b/rhythm_coach/lib/l10n/app_localizations.dart @@ -318,6 +318,12 @@ abstract class AppLocalizations { /// **'FAIL'** String get sessionStateFailing; + /// No description provided for @sessionBreakBanner. + /// + /// In fr, this message translates to: + /// **'PAUSE'** + String get sessionBreakBanner; + /// No description provided for @sessionFailPhasePhrase. /// /// In fr, this message translates to: @@ -2550,6 +2556,42 @@ abstract class AppLocalizations { /// **'Couilles'** String get positionBalls; + /// No description provided for @postureFree. + /// + /// In fr, this message translates to: + /// **'Libre'** + String get postureFree; + + /// No description provided for @postureSitting. + /// + /// In fr, this message translates to: + /// **'Assise'** + String get postureSitting; + + /// No description provided for @postureStanding. + /// + /// In fr, this message translates to: + /// **'Debout'** + String get postureStanding; + + /// No description provided for @postureKneeling. + /// + /// In fr, this message translates to: + /// **'À genoux'** + String get postureKneeling; + + /// No description provided for @postureAllFours. + /// + /// In fr, this message translates to: + /// **'À quatre pattes'** + String get postureAllFours; + + /// No description provided for @postureOnBack. + /// + /// In fr, this message translates to: + /// **'Sur le dos'** + String get postureOnBack; + /// No description provided for @modeShortRhythm. /// /// In fr, this message translates to: diff --git a/rhythm_coach/lib/l10n/app_localizations_de.dart b/rhythm_coach/lib/l10n/app_localizations_de.dart index 8d99dd6..6ff1c4d 100644 --- a/rhythm_coach/lib/l10n/app_localizations_de.dart +++ b/rhythm_coach/lib/l10n/app_localizations_de.dart @@ -122,6 +122,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get sessionStateFailing => 'FAIL'; + @override + String get sessionBreakBanner => 'PAUSE'; + @override String get sessionFailPhasePhrase => 'Fail-Satz'; @@ -1422,6 +1425,24 @@ class AppLocalizationsDe extends AppLocalizations { @override String get positionBalls => 'Hoden'; + @override + String get postureFree => 'Frei'; + + @override + String get postureSitting => 'Sitzend'; + + @override + String get postureStanding => 'Stehend'; + + @override + String get postureKneeling => 'Kniend'; + + @override + String get postureAllFours => 'Auf allen vieren'; + + @override + String get postureOnBack => 'Auf dem Rücken'; + @override String get modeShortRhythm => 'LUTSCH'; diff --git a/rhythm_coach/lib/l10n/app_localizations_en.dart b/rhythm_coach/lib/l10n/app_localizations_en.dart index 36832b1..d9bc43a 100644 --- a/rhythm_coach/lib/l10n/app_localizations_en.dart +++ b/rhythm_coach/lib/l10n/app_localizations_en.dart @@ -122,6 +122,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get sessionStateFailing => 'FAIL'; + @override + String get sessionBreakBanner => 'BREAK'; + @override String get sessionFailPhasePhrase => 'Fail phrase'; @@ -1417,6 +1420,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get positionBalls => 'Balls'; + @override + String get postureFree => 'Free'; + + @override + String get postureSitting => 'Seated'; + + @override + String get postureStanding => 'Standing'; + + @override + String get postureKneeling => 'Kneeling'; + + @override + String get postureAllFours => 'On all fours'; + + @override + String get postureOnBack => 'On your back'; + @override String get modeShortRhythm => 'SUCK'; diff --git a/rhythm_coach/lib/l10n/app_localizations_es.dart b/rhythm_coach/lib/l10n/app_localizations_es.dart index 2dfd2c6..40decf2 100644 --- a/rhythm_coach/lib/l10n/app_localizations_es.dart +++ b/rhythm_coach/lib/l10n/app_localizations_es.dart @@ -122,6 +122,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get sessionStateFailing => 'FALLO'; + @override + String get sessionBreakBanner => 'PAUSA'; + @override String get sessionFailPhasePhrase => 'Frase de fallo'; @@ -1422,6 +1425,24 @@ class AppLocalizationsEs extends AppLocalizations { @override String get positionBalls => 'Huevos'; + @override + String get postureFree => 'Libre'; + + @override + String get postureSitting => 'Sentada'; + + @override + String get postureStanding => 'De pie'; + + @override + String get postureKneeling => 'De rodillas'; + + @override + String get postureAllFours => 'A cuatro patas'; + + @override + String get postureOnBack => 'Boca arriba'; + @override String get modeShortRhythm => 'MAMAR'; diff --git a/rhythm_coach/lib/l10n/app_localizations_fr.dart b/rhythm_coach/lib/l10n/app_localizations_fr.dart index f298d8e..c9f07e5 100644 --- a/rhythm_coach/lib/l10n/app_localizations_fr.dart +++ b/rhythm_coach/lib/l10n/app_localizations_fr.dart @@ -122,6 +122,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get sessionStateFailing => 'FAIL'; + @override + String get sessionBreakBanner => 'PAUSE'; + @override String get sessionFailPhasePhrase => 'Phrase de fail'; @@ -1425,6 +1428,24 @@ class AppLocalizationsFr extends AppLocalizations { @override String get positionBalls => 'Couilles'; + @override + String get postureFree => 'Libre'; + + @override + String get postureSitting => 'Assise'; + + @override + String get postureStanding => 'Debout'; + + @override + String get postureKneeling => 'À genoux'; + + @override + String get postureAllFours => 'À quatre pattes'; + + @override + String get postureOnBack => 'Sur le dos'; + @override String get modeShortRhythm => 'SUCE'; diff --git a/rhythm_coach/lib/l10n/enum_labels.dart b/rhythm_coach/lib/l10n/enum_labels.dart index 41c13c7..8c208a8 100644 --- a/rhythm_coach/lib/l10n/enum_labels.dart +++ b/rhythm_coach/lib/l10n/enum_labels.dart @@ -4,6 +4,7 @@ import '../career/models/custom_session_config.dart'; import '../career/models/session_length_choice.dart'; import '../career/models/specialization.dart'; import '../models/badge.dart'; +import '../models/posture.dart'; import '../models/session.dart'; import '../models/session_step.dart'; import '../services/capability_axis.dart'; @@ -42,6 +43,22 @@ extension PositionL10n on Position { } } +extension PostureL10n on Posture { + /// Libellé court de la posture imposée (issue #77), pour l'indicateur de + /// posture en séance. [Posture.free] = « libre » (aucune contrainte). + String localizedLabel(BuildContext context) { + final t = AppLocalizations.of(context); + return switch (this) { + Posture.free => t.postureFree, + Posture.sitting => t.postureSitting, + Posture.standing => t.postureStanding, + Posture.kneeling => t.postureKneeling, + Posture.allFours => t.postureAllFours, + Posture.onBack => t.postureOnBack, + }; + } +} + extension BadgeTierL10n on BadgeTier { String localizedLabel(BuildContext context) { final t = AppLocalizations.of(context); diff --git a/rhythm_coach/lib/screens/session_screen.dart b/rhythm_coach/lib/screens/session_screen.dart index 33f36fb..ab999a3 100644 --- a/rhythm_coach/lib/screens/session_screen.dart +++ b/rhythm_coach/lib/screens/session_screen.dart @@ -40,8 +40,10 @@ import '../services/stats_service.dart'; import '../services/tts_service.dart'; import 'camera_test_screen.dart'; import '../theme/app_theme.dart'; +import '../models/posture.dart'; import '../widgets/mode_badge_row.dart'; import '../widgets/movement_animation.dart'; +import '../widgets/posture_indicator.dart'; import '../widgets/session_background.dart'; import '../widgets/session_finale_overlay.dart'; import '../widgets/timer_display.dart'; @@ -1053,6 +1055,13 @@ class _SessionScreenContentState extends State<_SessionScreenContent> { ) else const SizedBox(height: 30), + // Indicateur de posture imposée (issue #77) : permanent dès + // qu'une pose est imposée (hors `free`). Reflète + // `currentPose`, qui ne change qu'à l'intro et aux breaks. + if (ctrl.currentPose != Posture.free) ...[ + SizedBox(height: showBar ? 4 : 8), + PostureIndicator(pose: ctrl.currentPose), + ], if (showBar) ...[ const SizedBox(height: 4), StaminaBar( @@ -1148,6 +1157,10 @@ class _SessionScreenContentState extends State<_SessionScreenContent> { ), SizedBox(height: showBar ? 8 : 12), ], + if (ctrl.breakActive) ...[ + _BreakBanner(controller: ctrl), + SizedBox(height: showBar ? 8 : 12), + ], if (ctrl.isChallengeActive) ...[ _ChallengeBanner(controller: ctrl), SizedBox(height: showBar ? 6 : 10), @@ -1715,6 +1728,71 @@ class _ChallengeBanner extends StatelessWidget { } } +/// Bannière de break scénarisé (issue #77) : « PAUSE » + décompte mm:ss + +/// posture à venir. Affichée pendant `controller.breakActive` pour signaler +/// que le gel de l'effort est **voulu** (≠ freeze/bug). Pattern visuel calqué +/// sur `_ChallengeBanner` (cadre sombre, accent ambre). L'ordre courant +/// (« bois une gorgée »…) reste affiché par `_CurrentInstruction`. +class _BreakBanner extends StatelessWidget { + static const Color _bg = Color(0xFF14201A); + static const Color _border = Color(0xFF66BB6A); + + final SessionController controller; + const _BreakBanner({required this.controller}); + + @override + Widget build(BuildContext context) { + final t = AppLocalizations.of(context); + final b = controller.activeBreak; + final remaining = b == null + ? 0 + : (b.endTime - controller.elapsedSeconds).clamp(0, b.durationSeconds); + final mm = (remaining ~/ 60).toString(); + final ss = (remaining % 60).toString().padLeft(2, '0'); + final newPose = b?.newPose; + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: _bg, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: _border, width: 2), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + t.sessionBreakBanner, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w900, + letterSpacing: 3, + color: _border, + ), + ), + Text( + '$mm:$ss', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: AppTheme.textPrimary, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + ), + if (newPose != null && newPose != Posture.free) ...[ + const SizedBox(height: 10), + PostureIndicator(pose: newPose), + ], + ], + ), + ); + } +} + /// Bouton « SUPPLIER » du mode Carrière. Cliqué une seule fois par séance, /// déclenche un beg insistant + régénère la suite à un niveau supérieur. class _SupplierButton extends StatelessWidget { diff --git a/rhythm_coach/lib/widgets/posture_indicator.dart b/rhythm_coach/lib/widgets/posture_indicator.dart new file mode 100644 index 0000000..df55fd4 --- /dev/null +++ b/rhythm_coach/lib/widgets/posture_indicator.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; + +import '../l10n/enum_labels.dart'; +import '../models/posture.dart'; +import '../theme/app_theme.dart'; + +/// Indicateur de posture imposée (issue #77) : silhouette schématique + label +/// localisé, affiché en permanence pendant la séance dès qu'une posture est +/// imposée (`pose != Posture.free`). Pas d'asset binaire — la silhouette est +/// peinte par [_PostureSilhouettePainter] (picto léger in-repo, cohérent avec +/// la politique « pas de binaires lourds, ceux-là restent dans le repo »). +/// +/// Cf. spec `specs/scripted_breaks.md` § UI. La pose change uniquement à +/// l'intro et aux breaks ; cet indicateur reflète `SessionController.currentPose`. +class PostureIndicator extends StatelessWidget { + final Posture pose; + + /// Taille de la silhouette (carrée). Le label s'aligne dessous. + final double size; + + const PostureIndicator({super.key, required this.pose, this.size = 40}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppTheme.surface, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: AppTheme.accent.withValues(alpha: 0.35), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: size, + height: size, + child: CustomPaint( + painter: _PostureSilhouettePainter( + pose: pose, + color: AppTheme.textPrimary, + ), + ), + ), + const SizedBox(width: 10), + Text( + pose.localizedLabel(context).toUpperCase(), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: AppTheme.textPrimary, + ), + ), + ], + ), + ); + } +} + +/// Peint une silhouette humaine schématique selon la posture. Traits épais à +/// bouts ronds (effet silhouette) sur un canvas normalisé. Volontairement +/// minimal : le label porte le sens, la silhouette donne le repère visuel +/// immédiat (téléphone posé sur le côté, lecture d'un coup d'œil). +class _PostureSilhouettePainter extends CustomPainter { + final Posture pose; + final Color color; + + _PostureSilhouettePainter({required this.pose, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final w = size.width; + final h = size.height; + final stroke = w * 0.11; + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + final headPaint = Paint() + ..color = color + ..style = PaintingStyle.fill; + final headR = w * 0.12; + + // Helpers en coordonnées normalisées (0..1) → pixels. + Offset p(double x, double y) => Offset(x * w, y * h); + void line(double x1, double y1, double x2, double y2) => + canvas.drawLine(p(x1, y1), p(x2, y2), paint); + void head(double x, double y) => + canvas.drawCircle(p(x, y), headR, headPaint); + + switch (pose) { + case Posture.standing: + head(0.5, 0.16); + line(0.5, 0.26, 0.5, 0.62); // tronc + line(0.5, 0.36, 0.30, 0.52); // bras g + line(0.5, 0.36, 0.70, 0.52); // bras d + line(0.5, 0.62, 0.38, 0.92); // jambe g + line(0.5, 0.62, 0.62, 0.92); // jambe d + case Posture.sitting: + head(0.40, 0.18); + line(0.40, 0.28, 0.40, 0.58); // tronc + line(0.40, 0.58, 0.74, 0.58); // cuisses (horizontales) + line(0.74, 0.58, 0.74, 0.90); // tibias (verticaux) + line(0.40, 0.40, 0.60, 0.54); // bras posé sur la cuisse + case Posture.kneeling: + head(0.5, 0.22); + line(0.5, 0.31, 0.5, 0.60); // tronc + line(0.5, 0.42, 0.32, 0.56); // bras g + line(0.5, 0.42, 0.68, 0.56); // bras d + line(0.5, 0.60, 0.5, 0.82); // cuisses (verticales) + line(0.5, 0.82, 0.78, 0.82); // tibias au sol + case Posture.allFours: + head(0.18, 0.40); + line(0.26, 0.42, 0.78, 0.42); // dos horizontal + line(0.34, 0.42, 0.34, 0.84); // bras + line(0.72, 0.42, 0.72, 0.84); // jambe + line(0.50, 0.42, 0.50, 0.84); // appui central + case Posture.onBack: + head(0.16, 0.52); + line(0.24, 0.56, 0.84, 0.56); // corps allongé + line(0.50, 0.56, 0.62, 0.74); // genou replié + line(0.62, 0.74, 0.74, 0.74); // pied au sol + line(0.84, 0.56, 0.92, 0.62); // jambe tendue + case Posture.free: + // Pose libre : ligne ondulée neutre (jamais affichée par + // PostureIndicator, qui ne se monte que pour pose != free — rendu + // défensif au cas où). + head(0.5, 0.20); + line(0.5, 0.30, 0.5, 0.70); + } + } + + @override + bool shouldRepaint(_PostureSilhouettePainter old) => + old.pose != pose || old.color != color; +} diff --git a/rhythm_coach/test/posture_indicator_test.dart b/rhythm_coach/test/posture_indicator_test.dart new file mode 100644 index 0000000..0b3365f --- /dev/null +++ b/rhythm_coach/test/posture_indicator_test.dart @@ -0,0 +1,41 @@ +import 'package:beat_bitch/l10n/app_localizations.dart'; +import 'package:beat_bitch/models/posture.dart'; +import 'package:beat_bitch/widgets/posture_indicator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Smoke test de l'indicateur de posture (issue #77) : chaque pose construit +/// son silhouette + label localisé sans crash. Couvre aussi le painter +/// (`CustomPaint` peint dans le pump). +Widget _host(Posture pose, {Locale locale = const Locale('fr')}) => MaterialApp( + locale: locale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold(body: Center(child: PostureIndicator(pose: pose))), + ); + +void main() { + testWidgets('chaque posture rend son label localisé (FR)', (tester) async { + final expected = { + Posture.sitting: 'ASSISE', + Posture.standing: 'DEBOUT', + Posture.kneeling: 'À GENOUX', + Posture.allFours: 'À QUATRE PATTES', + Posture.onBack: 'SUR LE DOS', + }; + for (final entry in expected.entries) { + await tester.pumpWidget(_host(entry.key)); + expect(find.text(entry.value), findsOneWidget, + reason: 'label manquant pour ${entry.key}'); + expect(find.byType(CustomPaint), findsWidgets); + } + }); + + testWidgets('les 4 langues construisent sans crash (allFours)', + (tester) async { + for (final lang in const ['fr', 'en', 'de', 'es']) { + await tester.pumpWidget(_host(Posture.allFours, locale: Locale(lang))); + expect(tester.takeException(), isNull, reason: 'crash en $lang'); + } + }); +}