From d3c723ae0fb9fd3d0a996a37deff3eb4e032023d Mon Sep 17 00:00:00 2001 From: uvtqt Date: Fri, 13 Nov 2026 11:09:38 +0100 Subject: [PATCH 1/4] Steps: say a WHOOP 4.0 only counts while the app gets its live data A WHOOP 4.0 has no on-chip step counter; its steps come only from the live 100 Hz stream, so a walk with the phone left behind leaves no steps at all and users report it as "not tracking steps". On a day the band counted nothing, the steps screen now says so, after the phone footnote and on the no-steps card. Only for a known gen4 band, never on a day it counted. --- lib/l10n/app_de.arb | 3 +- lib/l10n/app_en.arb | 6 +++- lib/l10n/app_es.arb | 3 +- lib/l10n/app_fr.arb | 3 +- lib/l10n/app_hi.arb | 3 +- lib/l10n/app_ru.arb | 3 +- lib/l10n/app_th.arb | 3 +- lib/l10n/app_zh.arb | 3 +- lib/ui2/screens/day_steps.dart | 58 ++++++++++++++++++++++++++++++---- test/ui2_day_steps_test.dart | 46 +++++++++++++++++++++++++++ 10 files changed, 116 insertions(+), 15 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 233bed5aa..cdafa7a4d 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -3545,5 +3545,6 @@ "stepCalDoneHousehold": "Gespeichert. Dein Smartphone hat {count} der {band} Schritte gezählt, die das Band erkannt hat. Bewegung im Haushalt wird jetzt auf {gain} skaliert.", "whoopFirmwareWarningTitle": "WHOOP-Nutzer: Verbinde dich nicht wieder mit der WHOOP-App", "whoopFirmwareWarningAction": "Wenn du OpenStrap oder eine andere Open-Source-App nutzt oder später zu einer wechseln möchtest, verbinde dein Band nicht wieder mit der offiziellen WHOOP-App. Sie kann Firmware-Updates automatisch installieren. Deinstalliere oder deaktiviere die WHOOP-App, damit sie dein Band nicht aktualisiert.", - "whoopFirmwareWarningDetails": "Berichte zur WHOOP 5.0 / MG Firmware 50.43.1.0 und 50.43.2.0 beschreiben einen blockierten Datenzugriff. Bluetooth kann sich weiterhin verbinden. OpenStrap kann das von WHOOP signierte Mitgliedschafts-Token nicht ausstellen, das betroffene Bänder entsperrt. Wir arbeiten an einer Lösung, aber es gibt noch keine Abhilfe." + "whoopFirmwareWarningDetails": "Berichte zur WHOOP 5.0 / MG Firmware 50.43.1.0 und 50.43.2.0 beschreiben einen blockierten Datenzugriff. Bluetooth kann sich weiterhin verbinden. OpenStrap kann das von WHOOP signierte Mitgliedschafts-Token nicht ausstellen, das betroffene Bänder entsperrt. Wir arbeiten an einer Lösung, aber es gibt noch keine Abhilfe.", + "dayStepsWhoop4NoCounter": "Ein WHOOP 4.0 hat keinen eigenen Schrittzähler und zählt Schritte nur, solange diese App seine Live-Daten empfängt." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ec5231ef9..9d9ee8c85 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -16398,5 +16398,9 @@ "strengthDelete": "Delete", "@strengthDelete": {"description": "Strength set editor copy."}, "strengthSaveFailed": "Failed to save changes", - "@strengthSaveFailed": {"description": "Strength set editor copy."} + "@strengthSaveFailed": {"description": "Strength set editor copy."}, + "dayStepsWhoop4NoCounter": "A WHOOP 4.0 has no step counter of its own, so it only counts steps while this app is receiving its live data.", + "@dayStepsWhoop4NoCounter": { + "description": "Steps screen: appended when the paired band is a WHOOP 4.0 and it counted nothing that day, because it has no on-chip step counter" + } } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index a236842ec..d80bc168b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -3245,5 +3245,6 @@ "stepCalDoneHousehold": "Guardado. Tu teléfono contó {count} de los {band} pasos que vio la banda. El movimiento en casa ahora se escala a {gain}.", "whoopFirmwareWarningTitle": "Usuarios de WHOOP: no vuelvas a conectarte a la app de WHOOP", "whoopFirmwareWarningAction": "Si usas OpenStrap u otra app de código abierto, o piensas cambiarte a una más adelante, no vuelvas a conectar tu banda a la app oficial de WHOOP. Puede instalar actualizaciones de firmware automáticamente. Desinstala o desactiva la app de WHOOP para evitar que actualice tu banda.", - "whoopFirmwareWarningDetails": "Hay informes de que el firmware 50.43.1.0 y 50.43.2.0 de WHOOP 5.0 / MG bloquea el acceso a los datos. Es posible que el Bluetooth siga conectándose. OpenStrap no puede emitir el token de membresía firmado por WHOOP que se necesita para desbloquear las bandas afectadas. Estamos trabajando en una solución, pero todavía no hay ningún arreglo disponible." + "whoopFirmwareWarningDetails": "Hay informes de que el firmware 50.43.1.0 y 50.43.2.0 de WHOOP 5.0 / MG bloquea el acceso a los datos. Es posible que el Bluetooth siga conectándose. OpenStrap no puede emitir el token de membresía firmado por WHOOP que se necesita para desbloquear las bandas afectadas. Estamos trabajando en una solución, pero todavía no hay ningún arreglo disponible.", + "dayStepsWhoop4NoCounter": "Una WHOOP 4.0 no tiene contador de pasos propio, así que solo cuenta pasos mientras esta app recibe sus datos en vivo." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2875f5709..46c67e1b1 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2895,5 +2895,6 @@ "pairBandNotSensor": "Cet appareil est un {label}, c’est-à-dire un bracelet, pas un simple capteur. Rien n’a été enregistré.", "whoopFirmwareWarningTitle": "Utilisateurs WHOOP : ne vous reconnectez pas à l’application WHOOP", "whoopFirmwareWarningAction": "Si vous utilisez OpenStrap ou une autre application open source, ou si vous comptez y passer plus tard, ne reconnectez pas votre bracelet à l’application WHOOP officielle. Elle peut installer des mises à jour du firmware automatiquement. Désinstallez ou désactivez l’application WHOOP pour l’empêcher de mettre à jour votre bracelet.", - "whoopFirmwareWarningDetails": "Des signalements concernant les firmwares 50.43.1.0 et 50.43.2.0 des WHOOP 5.0 / MG décrivent un accès aux données bloqué. Le Bluetooth peut encore se connecter. OpenStrap ne peut pas émettre le jeton d’abonnement signé par WHOOP nécessaire pour déverrouiller les bracelets concernés. Nous travaillons sur une solution, mais aucun correctif n’est encore disponible." + "whoopFirmwareWarningDetails": "Des signalements concernant les firmwares 50.43.1.0 et 50.43.2.0 des WHOOP 5.0 / MG décrivent un accès aux données bloqué. Le Bluetooth peut encore se connecter. OpenStrap ne peut pas émettre le jeton d’abonnement signé par WHOOP nécessaire pour déverrouiller les bracelets concernés. Nous travaillons sur une solution, mais aucun correctif n’est encore disponible.", + "dayStepsWhoop4NoCounter": "Un WHOOP 4.0 n’a pas de podomètre intégré : il ne compte les pas que lorsque cette application reçoit ses données en direct." } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index b9fea6734..2d4ae429d 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -2895,5 +2895,6 @@ "pairBandNotSensor": "यह डिवाइस {label} है, यानी एक बैंड, साधारण सेंसर नहीं। कुछ भी सहेजा नहीं गया।", "whoopFirmwareWarningTitle": "WHOOP उपयोगकर्ता: WHOOP ऐप से दोबारा कनेक्ट न करें", "whoopFirmwareWarningAction": "अगर आप OpenStrap या कोई दूसरा ओपन-सोर्स ऐप इस्तेमाल करते हैं, या बाद में किसी पर जाने की सोच रहे हैं, तो अपना बैंड आधिकारिक WHOOP ऐप से दोबारा कनेक्ट न करें। वह फ़र्मवेयर अपडेट अपने-आप इंस्टॉल कर सकता है। WHOOP ऐप को अनइंस्टॉल या अक्षम कर दें, ताकि वह आपके बैंड को अपडेट न कर सके।", - "whoopFirmwareWarningDetails": "WHOOP 5.0 / MG के फ़र्मवेयर 50.43.1.0 और 50.43.2.0 के बारे में रिपोर्टें बताती हैं कि डेटा तक पहुँच रोक दी जाती है। ब्लूटूथ फिर भी कनेक्ट हो सकता है। प्रभावित बैंड को अनलॉक करने के लिए WHOOP द्वारा साइन किया गया मेंबरशिप टोकन चाहिए, जिसे OpenStrap जारी नहीं कर सकता। हम समाधान पर काम कर रहे हैं, लेकिन अभी कोई उपाय उपलब्ध नहीं है।" + "whoopFirmwareWarningDetails": "WHOOP 5.0 / MG के फ़र्मवेयर 50.43.1.0 और 50.43.2.0 के बारे में रिपोर्टें बताती हैं कि डेटा तक पहुँच रोक दी जाती है। ब्लूटूथ फिर भी कनेक्ट हो सकता है। प्रभावित बैंड को अनलॉक करने के लिए WHOOP द्वारा साइन किया गया मेंबरशिप टोकन चाहिए, जिसे OpenStrap जारी नहीं कर सकता। हम समाधान पर काम कर रहे हैं, लेकिन अभी कोई उपाय उपलब्ध नहीं है।", + "dayStepsWhoop4NoCounter": "WHOOP 4.0 में अपना कोई स्टेप काउंटर नहीं है, इसलिए यह कदम तभी गिनता है जब यह ऐप उसका लाइव डेटा प्राप्त कर रहा हो।" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index f25901a16..c8e64b892 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -7943,5 +7943,6 @@ "type": "String" } } - } + }, + "dayStepsWhoop4NoCounter": "У WHOOP 4.0 нет собственного шагомера, поэтому он считает шаги только пока это приложение получает его данные в реальном времени." } diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index 19e7265dd..b4b7e8c46 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -16365,5 +16365,6 @@ "type": "String" } } - } + }, + "dayStepsWhoop4NoCounter": "WHOOP 4.0 ไม่มีตัวนับก้าวในตัว จึงนับก้าวได้เฉพาะตอนที่แอปนี้กำลังรับข้อมูลสดจากสายรัดเท่านั้น" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 111dc74e2..9033a52fb 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2895,5 +2895,6 @@ "pairBandNotSensor": "这台设备是 {label},是手环,不是普通传感器。没有保存任何内容。", "whoopFirmwareWarningTitle": "WHOOP 用户:请勿重新连接 WHOOP 应用", "whoopFirmwareWarningAction": "如果你在使用 OpenStrap 或其他开源应用,或打算以后改用,请不要再把手环连接到 WHOOP 官方应用。它可能会自动安装固件更新。请卸载或停用 WHOOP 应用,以免它更新你的手环。", - "whoopFirmwareWarningDetails": "有报告称,WHOOP 5.0 / MG 固件 50.43.1.0 和 50.43.2.0 会阻止访问数据。蓝牙可能仍能连接。解锁受影响的手环需要由 WHOOP 签名的会员令牌,OpenStrap 无法签发。我们正在寻找解决办法,但目前还没有可用的修复。" + "whoopFirmwareWarningDetails": "有报告称,WHOOP 5.0 / MG 固件 50.43.1.0 和 50.43.2.0 会阻止访问数据。蓝牙可能仍能连接。解锁受影响的手环需要由 WHOOP 签名的会员令牌,OpenStrap 无法签发。我们正在寻找解决办法,但目前还没有可用的修复。", + "dayStepsWhoop4NoCounter": "WHOOP 4.0 没有自带计步器,因此只有在本应用接收其实时数据时才会计步。" } diff --git a/lib/ui2/screens/day_steps.dart b/lib/ui2/screens/day_steps.dart index 90ed0e988..6f8f3ac65 100644 --- a/lib/ui2/screens/day_steps.dart +++ b/lib/ui2/screens/day_steps.dart @@ -75,6 +75,7 @@ class DayStepsData { this.daySource, this.note, this.bandLabel = _defaultBand, + this.bandHasNoCounter = false, }); /// The fallback name for the strap. Nothing is paired, or the link has not @@ -106,11 +107,18 @@ class DayStepsData { /// or [_defaultBand] when nothing has told us which band it is. final String bandLabel; + /// The paired band has no step counter of its own (a WHOOP 4.0): it only + /// counts while the app is receiving its live movement stream, so a walk + /// with the phone left behind leaves no steps at all. Said on the screen so + /// "it isn't tracking my steps" has its answer where the steps are. + final bool bandHasNoCounter; + bool get mixed => strap > 0 && phone > 0; static Future load( LocalRepository repo, { String bandLabel = _defaultBand, + bool bandHasNoCounter = false, String? want, }) async { final days = await repo.availableDays(); @@ -140,6 +148,7 @@ class DayStepsData { daySource: d['day_source'] as String?, note: d['note'] as String?, bandLabel: bandLabel, + bandHasNoCounter: bandHasNoCounter, ); } } @@ -273,7 +282,9 @@ class _DayStepsDetailState extends State { } try { final d = await DayStepsData.load(repo, - bandLabel: bandLabel(context), want: _day); + bandLabel: bandLabel(context), + bandHasNoCounter: pairedBandHasNoCounter(context), + want: _day); if (mounted && token == _loadToken) { setState(() => (_d = d, _loading = false)); } @@ -334,9 +345,13 @@ class _DayStepsDetailState extends State { 'The ${thousands(d.dayTotal)} steps $when came from the ' 'strap’s own counter, which didn’t keep times for this day, ' 'so they can’t go on a clock.') - : whyFromNote(d.note, unit: 'days') ?? - (l?.dayStepsNothingCounted(when) ?? - 'Nothing that can count steps recorded $when.'), + : _withNoCounterHint( + c, + d, + whyFromNote(d.note, unit: 'days') ?? + (l?.dayStepsNothingCounted(when) ?? + 'Nothing that can count steps recorded $when.'), + ), icon: chip ? LucideIcons.watch : LucideIcons.footprints, ); } @@ -431,9 +446,24 @@ class _DayStepsDetailState extends State { ? (l?.dayStepsHonestyStrap ?? 'Counted at your wrist, where a real walk tends to read low and ' 'rhythmic hand work can read as walking.') - : (l?.dayStepsHonestyPhone ?? - 'Counted by your phone, so only the steps you had it on you for ' - 'are here.'); + : _withNoCounterHint( + c, + d, + l?.dayStepsHonestyPhone ?? + 'Counted by your phone, so only the steps you had it on you ' + 'for are here.', + ); + } + + /// [text], followed by why the band added nothing when it has no counter of + /// its own. Only ever on a day the band did not count: a day it did count + /// already shows that it can. + String _withNoCounterHint(BuildContext c, DayStepsData d, String text) { + if (!d.bandHasNoCounter) return text; + final hint = AppLocalizations.of(c)?.dayStepsWhoop4NoCounter ?? + 'A WHOOP 4.0 has no step counter of its own, so it only counts steps ' + 'while this app is receiving its live data.'; + return '$text $hint'; } // ── the stretches themselves ─────────────────────────────────────────────── @@ -489,3 +519,17 @@ String bandLabel(BuildContext c) { return fallback; } } + +/// Whether the band paired NOW has no on-chip step counter: a WHOOP 4.0 +/// (`gen4` — "gen4 has no counter", see `derivation_engine.dart`'s v101→102 +/// note). False when nothing is paired or the generation is not known, so the +/// hint never names a band we were not told about. +bool pairedBandHasNoCounter(BuildContext c) { + try { + final app = c.read(); + if (!app.isPaired) return false; + return (app.paired?.generation ?? app.device.generation) == 'gen4'; + } catch (_) { + return false; + } +} diff --git a/test/ui2_day_steps_test.dart b/test/ui2_day_steps_test.dart index 920b0f61a..80ad304a5 100644 --- a/test/ui2_day_steps_test.dart +++ b/test/ui2_day_steps_test.dart @@ -87,6 +87,52 @@ void main() { expect(find.text('WHOOP 4'), findsNothing); }); + testWidgets('with a WHOOP 4.0 paired, a phone-only day says why the band ' + 'added nothing', (t) async { + await _pump( + t, + DayStepsData( + spans: [_span(0, 60, 1200)], + total: 1200, + phone: 1200, + bandHasNoCounter: true, + ), + ); + expect(find.textContaining('had it on you for'), findsOneWidget); + expect(find.textContaining('A WHOOP 4.0 has no step counter'), + findsOneWidget); + }); + + testWidgets('with a WHOOP 4.0 paired, a day with no steps says why', + (t) async { + await _pump(t, const DayStepsData(bandHasNoCounter: true)); + expect(find.textContaining('No steps counted'), findsOneWidget); + expect(find.textContaining('A WHOOP 4.0 has no step counter'), + findsOneWidget); + }); + + testWidgets('the WHOOP 4.0 hint never shows on a day the band counted, or ' + 'for another band', (t) async { + // The band counted: it evidently can, so the hint would be wrong. + await _pump( + t, + DayStepsData( + spans: [_span(240, 300, 3240, band: true)], + total: 3240, + strap: 3240, + bandLabel: 'WHOOP 4', + bandHasNoCounter: true, + ), + ); + expect(find.textContaining('has no step counter'), findsNothing); + // A band with its own counter (or none paired): never mentioned. + await _pump( + t, + DayStepsData(spans: [_span(0, 60, 1200)], total: 1200, phone: 1200), + ); + expect(find.textContaining('has no step counter'), findsNothing); + }); + testWidgets('a day counted only by the strap\'s on-chip counter says the ' 'TIMES are missing, not the steps', (t) async { await _pump( From 5aa0a3d2608170e9f0ef800d49e26dc48edbca5c Mon Sep 17 00:00:00 2001 From: uvtqt Date: Fri, 13 Nov 2026 11:57:44 +0100 Subject: [PATCH 2/4] Steps: no WHOOP 4.0 hint on a day a strap counter published An older day whose total came off a WHOOP 5.0 / MG's own counter was counted by a band that has one, so the hint would contradict it. --- lib/ui2/screens/day_steps.dart | 4 +++- test/ui2_day_steps_test.dart | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/ui2/screens/day_steps.dart b/lib/ui2/screens/day_steps.dart index 6f8f3ac65..72dc05ada 100644 --- a/lib/ui2/screens/day_steps.dart +++ b/lib/ui2/screens/day_steps.dart @@ -459,7 +459,9 @@ class _DayStepsDetailState extends State { /// its own. Only ever on a day the band did not count: a day it did count /// already shows that it can. String _withNoCounterHint(BuildContext c, DayStepsData d, String text) { - if (!d.bandHasNoCounter) return text; + // A day whose total came off a strap's own counter was counted by a + // band that HAS one (an earlier WHOOP 5.0 / MG): the hint would be false. + if (!d.bandHasNoCounter || d.daySource == 'strap_counter') return text; final hint = AppLocalizations.of(c)?.dayStepsWhoop4NoCounter ?? 'A WHOOP 4.0 has no step counter of its own, so it only counts steps ' 'while this app is receiving its live data.'; diff --git a/test/ui2_day_steps_test.dart b/test/ui2_day_steps_test.dart index 80ad304a5..6012dceb5 100644 --- a/test/ui2_day_steps_test.dart +++ b/test/ui2_day_steps_test.dart @@ -131,6 +131,20 @@ void main() { DayStepsData(spans: [_span(0, 60, 1200)], total: 1200, phone: 1200), ); expect(find.textContaining('has no step counter'), findsNothing); + // An older day a WHOOP 5.0's own counter published, viewed with a WHOOP + // 4.0 paired now: that band could count, so the hint would be false. + await _pump( + t, + DayStepsData( + spans: [_span(0, 60, 1200)], + total: 1200, + phone: 1200, + dayTotal: 6012, + daySource: 'strap_counter', + bandHasNoCounter: true, + ), + ); + expect(find.textContaining('has no step counter'), findsNothing); }); testWidgets('a day counted only by the strap\'s on-chip counter says the ' From 6376bcc207504ef0ddabf7b24358d0b075a9dd87 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Fri, 13 Nov 2026 17:41:54 +0530 Subject: [PATCH 3/4] steps hint: follow the live band, not the one the day loaded with --- lib/ui2/screens/day_steps.dart | 39 ++++++++++++++++++++++++++-------- test/ui2_day_steps_test.dart | 13 ++++++++++++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/lib/ui2/screens/day_steps.dart b/lib/ui2/screens/day_steps.dart index 72dc05ada..03c9ab9e6 100644 --- a/lib/ui2/screens/day_steps.dart +++ b/lib/ui2/screens/day_steps.dart @@ -461,7 +461,12 @@ class _DayStepsDetailState extends State { String _withNoCounterHint(BuildContext c, DayStepsData d, String text) { // A day whose total came off a strap's own counter was counted by a // band that HAS one (an earlier WHOOP 5.0 / MG): the hint would be false. - if (!d.bandHasNoCounter || d.daySource == 'strap_counter') return text; + // The band can be unpaired or swapped after the day was loaded, so the + // live answer wins whenever the app state is reachable. + if (!bandStillHasNoCounter(d.bandHasNoCounter, liveBandHasNoCounter(c)) || + d.daySource == 'strap_counter') { + return text; + } final hint = AppLocalizations.of(c)?.dayStepsWhoop4NoCounter ?? 'A WHOOP 4.0 has no step counter of its own, so it only counts steps ' 'while this app is receiving its live data.'; @@ -522,16 +527,32 @@ String bandLabel(BuildContext c) { } } -/// Whether the band paired NOW has no on-chip step counter: a WHOOP 4.0 -/// (`gen4` — "gen4 has no counter", see `derivation_engine.dart`'s v101→102 -/// note). False when nothing is paired or the generation is not known, so the -/// hint never names a band we were not told about. -bool pairedBandHasNoCounter(BuildContext c) { +/// Whether a band of [generation] has no on-chip step counter: a WHOOP 4.0 +/// (`gen4`, see `derivation_engine.dart`'s v101→102 note). +bool generationHasNoCounter(String? generation) => generation == 'gen4'; + +/// The answer to show: the live one when the app state could be read, else +/// what was loaded with the day. +bool bandStillHasNoCounter(bool loaded, bool? live) => live ?? loaded; + +/// Whether the band paired NOW has no on-chip step counter. False when +/// nothing is paired or the generation is not known, so the hint never names +/// a band we were not told about. +bool pairedBandHasNoCounter(BuildContext c) => + _bandHasNoCounter(c, listen: false) ?? false; + +/// [pairedBandHasNoCounter] for a build: listens, so the hint follows an +/// unpair or a swap. Null when there is no app state to ask (a golden). +bool? liveBandHasNoCounter(BuildContext c) => + _bandHasNoCounter(c, listen: true); + +bool? _bandHasNoCounter(BuildContext c, {required bool listen}) { try { - final app = c.read(); + final app = listen ? c.watch() : c.read(); if (!app.isPaired) return false; - return (app.paired?.generation ?? app.device.generation) == 'gen4'; + return generationHasNoCounter( + app.paired?.generation ?? app.device.generation); } catch (_) { - return false; + return null; } } diff --git a/test/ui2_day_steps_test.dart b/test/ui2_day_steps_test.dart index 6012dceb5..eea9ad34f 100644 --- a/test/ui2_day_steps_test.dart +++ b/test/ui2_day_steps_test.dart @@ -147,6 +147,19 @@ void main() { expect(find.textContaining('has no step counter'), findsNothing); }); + test('the no-counter hint follows the live band, not the day it loaded', () { + expect(generationHasNoCounter('gen4'), isTrue); + expect(generationHasNoCounter('gen5'), isFalse); + expect(generationHasNoCounter(null), isFalse); + // Unpaired since the day loaded: the live answer wins. + expect(bandStillHasNoCounter(true, false), isFalse); + // Paired since: likewise. + expect(bandStillHasNoCounter(false, true), isTrue); + // No app state to ask: what the day loaded with. + expect(bandStillHasNoCounter(true, null), isTrue); + expect(bandStillHasNoCounter(false, null), isFalse); + }); + testWidgets('a day counted only by the strap\'s on-chip counter says the ' 'TIMES are missing, not the steps', (t) async { await _pump( From 3c0d6bc6e6fa7a861b1b0e388161e070afd035b4 Mon Sep 17 00:00:00 2001 From: uvtqt Date: Fri, 13 Nov 2026 15:23:17 +0100 Subject: [PATCH 4/4] Steps test: the WHOOP 4.0 hint follows the paired band Pumps the screen under a real AppState and swaps the pairing (4.0, 5.0, 4.0, none), so a break in the live lookup fails a test instead of only the flag passed in by hand. Co-Authored-By: Claude Opus 5.5 --- test/ui2_day_steps_test.dart | 109 ++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 27 deletions(-) diff --git a/test/ui2_day_steps_test.dart b/test/ui2_day_steps_test.dart index eea9ad34f..4d09847e7 100644 --- a/test/ui2_day_steps_test.dart +++ b/test/ui2_day_steps_test.dart @@ -12,6 +12,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:openstrap_edge/state/app_state.dart'; +import 'package:openstrap_edge/sync/paired_device.dart'; import 'package:openstrap_edge/ui2/screens/day_steps.dart'; import 'package:openstrap_edge/ui2/ui2.dart'; @@ -32,19 +36,40 @@ DayStepSpan _span( activity: activity, ); -Future _pump(WidgetTester t, DayStepsData d, {double scale = 1}) async { +/// An app state whose pairing a test can swap, the way an unpair or a re-pair +/// does, and announce. +class _Paired extends AppState { + _Paired() : super.forTesting(); + + void pair(String? generation) { + paired = generation == null + ? null + : PairedDevice('remote', 'SN1', generation: generation); + notifyListeners(); + } +} + +Future _pump( + WidgetTester t, + DayStepsData d, { + double scale = 1, + AppState? app, +}) async { t.view.physicalSize = Size(390 * 3, 3000 * 3 * scale); t.view.devicePixelRatio = 3; addTearDown(t.view.reset); - await t.pumpWidget( - MediaQuery( - data: MediaQueryData(textScaler: TextScaler.linear(scale)), - child: MaterialApp( - theme: buildTheme(Brightness.light), - home: DayStepsDetail(data: d), - ), + final screen = MediaQuery( + data: MediaQueryData(textScaler: TextScaler.linear(scale)), + child: MaterialApp( + theme: buildTheme(Brightness.light), + home: DayStepsDetail(data: d), ), ); + await t.pumpWidget( + app == null + ? screen + : ChangeNotifierProvider.value(value: app, child: screen), + ); await t.pumpAndSettle(); } @@ -99,16 +124,49 @@ void main() { ), ); expect(find.textContaining('had it on you for'), findsOneWidget); - expect(find.textContaining('A WHOOP 4.0 has no step counter'), - findsOneWidget); + expect( + find.textContaining('A WHOOP 4.0 has no step counter'), + findsOneWidget, + ); }); - testWidgets('with a WHOOP 4.0 paired, a day with no steps says why', - (t) async { + testWidgets('with a WHOOP 4.0 paired, a day with no steps says why', ( + t, + ) async { await _pump(t, const DayStepsData(bandHasNoCounter: true)); expect(find.textContaining('No steps counted'), findsOneWidget); - expect(find.textContaining('A WHOOP 4.0 has no step counter'), - findsOneWidget); + expect( + find.textContaining('A WHOOP 4.0 has no step counter'), + findsOneWidget, + ); + }); + + testWidgets('the WHOOP 4.0 hint follows the band the app has paired', ( + t, + ) async { + // The day loaded with no hint; the live pairing decides. + SharedPreferences.setMockInitialValues({}); + final app = _Paired()..pair('gen4'); + addTearDown(app.dispose); + await _pump( + t, + DayStepsData(spans: [_span(0, 60, 1200)], total: 1200, phone: 1200), + app: app, + ); + const hint = 'A WHOOP 4.0 has no step counter'; + expect(find.textContaining(hint), findsOneWidget); + + app.pair('gen5'); + await t.pumpAndSettle(); + expect(find.textContaining(hint), findsNothing, reason: 'a 5.0 counts'); + + app.pair('gen4'); + await t.pumpAndSettle(); + expect(find.textContaining(hint), findsOneWidget); + + app.pair(null); + await t.pumpAndSettle(); + expect(find.textContaining(hint), findsNothing, reason: 'unpaired'); }); testWidgets('the WHOOP 4.0 hint never shows on a day the band counted, or ' @@ -209,19 +267,16 @@ void main() { _span(660, 720, 2074), ]; final merged = mergeAdjacent(rows); - expect( - merged.map((s) => (s.startTs - _nine, s.endTs - _nine, s.steps)), - [ - (0, 180 * 60, 1913), // three contiguous phone hours, joined - (240 * 60, 300 * 60, 228), // the gap kept it separate - (480 * 60, 540 * 60, 1649), // a band stretch starts inside the next - (532 * 60, 534 * 60, 51), - (534 * 60, 540 * 60, 737), - (540 * 60, 600 * 60, 5753), - (541 * 60, 544 * 60, 352), - (600 * 60, 720 * 60, 2288), - ], - ); + expect(merged.map((s) => (s.startTs - _nine, s.endTs - _nine, s.steps)), [ + (0, 180 * 60, 1913), // three contiguous phone hours, joined + (240 * 60, 300 * 60, 228), // the gap kept it separate + (480 * 60, 540 * 60, 1649), // a band stretch starts inside the next + (532 * 60, 534 * 60, 51), + (534 * 60, 540 * 60, 737), + (540 * 60, 600 * 60, 5753), + (541 * 60, 544 * 60, 352), + (600 * 60, 720 * 60, 2288), + ]); }); test('an hour gets one bar, in the colour of the sensor that counted most '