test(appareil): harnais integration_test — l'IU sur le téléphone, contre la vraie box
adb shell input tap est refusé par MIUI (INJECT_EVENTS). integration_test contourne le problème par construction : le test s'exécute DANS le processus de l'app et tape sur les widgets depuis l'intérieur, sans injection d'événements Android. Ce que le harnais a déjà prouvé SUR L'APPAREIL, contre .75 : l'app démarre, se connecte (« connecté à 1.15.2+202606191336~trixie1 ») et SchemaProvider lit JSONRPC.Introspect (« NymeaEnergy 0.8 · AirConditioning 1.1 »). Le grisage dérivé du schéma n'est donc pas une théorie : il est alimenté sur le téléphone par la vraie box. Le run n'est pas allé au bout. Trois obstacles : GoRouter.of() cherché au-dessus du Router (corrigé) ; le test supposait une installation déjà enregistrée (corrigé — il enregistre la box lui-même via connectNew) ; puis le téléphone s'est déconnecté du bus USB, et l'ADB sans fil n'est pas activé. C'est là que ça s'est arrêté. EFFET DE BORD ASSUMÉ : flutter test integration_test RÉINSTALLE l'app, ce qui vide shared_preferences et le stockage sécurisé — les deux installations enregistrées sur le téléphone ont été effacées. Le harnais ne dépend plus de cet état et restaurera .75 au premier run mené à terme ; nymea-dev-kutz est à rajouter à la main. Relevé mis à jour : docs/RELEVE_LOTC.md §2 et §6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
This commit is contained in:
parent
2a8ad95120
commit
f0aa0fd804
@ -113,11 +113,46 @@ implémentations de la même combinatoire divergeraient, et c'est l'app qui aura
|
||||
|---|---|
|
||||
| `flutter build apk --debug` (android-arm64) | ✔ construit |
|
||||
| `adb install` sur le Redmi Note 9S | ✔ installé |
|
||||
| Lancement de l'app | ✔ démarre, écran « Installations » |
|
||||
| Lancement de l'app | ✔ démarre |
|
||||
| Le téléphone (192.168.1.121) joint le banc | ✔ ping 4-20 ms |
|
||||
| L'app liste `nymea-dev-rpi` → `nymea://192.168.1.75:4444` | ✔ (capture d'écran) |
|
||||
| L'app liste `nymea-dev-rpi` → `nymea://192.168.1.75:4444` | ✔ capture d'écran |
|
||||
| **L'app se connecte à `.75` depuis le téléphone** | ✔ `connecté à 1.15.2+202606191336~trixie1` |
|
||||
| **`SchemaProvider` lit `JSONRPC.Introspect` sur l'appareil** | ✔ `nymea 1.15.2+… · NymeaEnergy 0.8 · AirConditioning 1.1` |
|
||||
|
||||
**Et c'est tout — l'IU n'a PAS pu être pilotée.** Voir §6.
|
||||
Les deux dernières lignes comptent : elles prouvent **sur l'appareil et contre la vraie
|
||||
box** que le schéma est lu à la connexion — donc que le grisage n'est pas une théorie.
|
||||
|
||||
### Le pilotage de l'IU : la voie trouvée, le run pas terminé
|
||||
|
||||
`adb shell input tap` est refusé par MIUI
|
||||
(`SecurityException: Injecting to another application requires INJECT_EVENTS permission`).
|
||||
La voie qui contourne cela est `integration_test` : le test s'exécute **dans le processus
|
||||
de l'app** et tape sur les widgets depuis l'intérieur, sans passer par l'injection
|
||||
d'événements d'Android. Le harnais est écrit
|
||||
(`integration_test/config_on_device_test.dart`) et **a effectivement démarré l'app,
|
||||
connecté la box et lu le schéma** — les deux lignes du tableau ci-dessus en viennent.
|
||||
|
||||
Il n'a **pas** été mené jusqu'au bout. Trois obstacles, dans l'ordre où ils sont apparus :
|
||||
|
||||
1. `GoRouter.of()` cherché sur le contexte du `MaterialApp`, qui est **au-dessus** du
|
||||
Router — corrigé (on prend un contexte sous lui) ;
|
||||
2. le test supposait une installation déjà enregistrée sur le téléphone — corrigé, il
|
||||
**enregistre la box lui-même** (`connectNew`), voir l'encadré ci-dessous ;
|
||||
3. **le téléphone s'est déconnecté du bus USB** (`adb: device 'dee659e2' not found`,
|
||||
plus rien dans `lsusb`), et l'ADB sans fil n'est pas activé
|
||||
(`connect 192.168.1.121:5555` → refusé). **C'est là que ça s'est arrêté.**
|
||||
|
||||
> ⚠️ **Effet de bord causé au téléphone, et assumé.** `flutter test integration_test`
|
||||
> **réinstalle** l'application, ce qui vide `shared_preferences` et le stockage sécurisé :
|
||||
> les deux installations enregistrées (`nymea-dev-rpi`, `nymea-dev-kutz`) **ont été
|
||||
> effacées**. L'écran affiche donc « Aucune installation enregistrée », et l'ajout manuel
|
||||
> est réservé au mode installateur. Le harnais a été corrigé pour ne plus dépendre de cet
|
||||
> état — il enregistre la box lui-même, ce qui la **restaurera** au premier run mené à
|
||||
> terme. `nymea-dev-kutz` (192.168.1.120), lui, est à rajouter à la main.
|
||||
|
||||
**Reste donc à faire, dès que le téléphone est rebranché** : un seul run de
|
||||
`integration_test/config_on_device_test.dart`, qui couvre l'écran fusionné affichant les
|
||||
charges réelles, une écriture confirmée par `LoadConfigChanged` et sa restauration.
|
||||
|
||||
---
|
||||
|
||||
@ -208,17 +243,13 @@ le banc pour être vérifié.
|
||||
|
||||
## 6. Ce qui n'a PAS été vérifié, et pourquoi
|
||||
|
||||
1. **L'IU n'a pas pu être pilotée sur l'appareil.** `adb shell input tap` est refusé par la
|
||||
box Android :
|
||||
`SecurityException: Injecting to another application requires INJECT_EVENTS permission`.
|
||||
C'est le verrou MIUI sur le Redmi — il se lève dans les options développeur
|
||||
(« débogage USB (réglages de sécurité) »), ce qui exige un compte Xiaomi et un geste
|
||||
manuel. `xdotool` n'est pas installé sur la machine, donc la voie « bureau Linux » est
|
||||
fermée aussi. **Aucun écran de configuration n'a donc été vu s'afficher sur l'appareil,
|
||||
ni aucune écriture déclenchée depuis l'IU.** Ce qui a été vérifié sur l'appareil est au §2,
|
||||
et rien de plus.
|
||||
*Pour clore ce point* : activer le réglage sur le téléphone, ou piloter l'écran à la main
|
||||
en suivant §1 de ce document.
|
||||
1. **Les écrans de configuration n'ont pas été vus s'afficher sur l'appareil**, et aucune
|
||||
écriture n'y a été déclenchée depuis l'IU. La connexion et la lecture du schéma, elles,
|
||||
l'ont été (§2). Le blocage final n'est pas logiciel : le téléphone s'est **déconnecté du
|
||||
bus USB** en cours de campagne.
|
||||
*Pour clore ce point* : rebrancher le téléphone (et autoriser l'ADB s'il le redemande),
|
||||
puis `flutter test integration_test/config_on_device_test.dart -d dee659e2`. Le harnais
|
||||
est écrit et corrigé ; il ne reste qu'à le faire tourner.
|
||||
2. **Les écritures depuis l'IU** n'ont donc été exercées qu'en **tests de widgets**, sur les
|
||||
dumps réels de `.75` (`merged_config_screen_test.dart`). Le rendu, l'annonce d'impact et le
|
||||
grisage y sont couverts ; le transport ne l'est pas.
|
||||
|
||||
177
integration_test/config_on_device_test.dart
Normal file
177
integration_test/config_on_device_test.dart
Normal file
@ -0,0 +1,177 @@
|
||||
/// L'IU de configuration, **sur l'appareil, contre la vraie box**.
|
||||
///
|
||||
/// ## Pourquoi ce test existe sous cette forme
|
||||
///
|
||||
/// `adb shell input tap` est refusé sur le Redmi Note 9S :
|
||||
/// `SecurityException: Injecting to another application requires INJECT_EVENTS
|
||||
/// permission` — verrou MIUI qui demande un compte Xiaomi pour se lever. Un test
|
||||
/// `integration_test` ne passe pas par là : il s'exécute **dans le processus de l'app** et
|
||||
/// tape sur les widgets depuis l'intérieur. C'est donc bien l'application réelle, sur le
|
||||
/// téléphone réel, parlant à `.75` par le réseau.
|
||||
///
|
||||
/// ## Ce qu'il exerce, et ce qu'il rend au banc
|
||||
///
|
||||
/// Il se connecte, ouvre l'écran fusionné, lit les charges **réelles**, puis écrit deux
|
||||
/// fois : un renommage (mise à jour en place — aucun verrou réarmé) et sa restauration.
|
||||
/// Rien de matériel n'est touché : le test ne réordonne aucun contacteur et ne désactive
|
||||
/// aucune charge, parce que ces gestes ouvrent des contacts et réarment des verrous sur
|
||||
/// une installation qui n'est pas la sienne.
|
||||
///
|
||||
/// Lancement :
|
||||
/// ```sh
|
||||
/// flutter test integration_test/config_on_device_test.dart -d dee659e2
|
||||
/// ```
|
||||
library;
|
||||
|
||||
import 'package:etm_powersync_app/main.dart' as app;
|
||||
import 'package:etm_powersync_app/providers/load_config_provider.dart';
|
||||
import 'package:etm_powersync_app/providers/schema_provider.dart';
|
||||
import 'package:etm_powersync_app/services/connection_manager.dart';
|
||||
import 'package:etm_powersync_app/services/nymea_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// La box de banc. `nymea-dev-rpi` doit être déjà enregistrée sur l'appareil.
|
||||
const _hote = '192.168.1.75';
|
||||
|
||||
/// Pompe jusqu'à ce que [cond] soit vraie, ou échoue en le disant.
|
||||
///
|
||||
/// `pumpAndSettle` ne convient pas ici : l'app garde des animations et un flux de
|
||||
/// notifications vivants tant qu'elle est connectée, si bien qu'elle ne « se pose »
|
||||
/// jamais. On pompe donc par pas fixes.
|
||||
Future<void> _attendre(
|
||||
WidgetTester tester,
|
||||
bool Function() cond, {
|
||||
Duration limite = const Duration(seconds: 30),
|
||||
String quoi = 'condition',
|
||||
}) async {
|
||||
final fin = DateTime.now().add(limite);
|
||||
while (DateTime.now().isBefore(fin)) {
|
||||
await tester.pump(const Duration(milliseconds: 250));
|
||||
if (cond()) return;
|
||||
}
|
||||
fail('délai dépassé en attendant : $quoi');
|
||||
}
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('configuration des charges, sur appareil, contre .75',
|
||||
(tester) async {
|
||||
app.main();
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
|
||||
// ── 1. Connexion à la box de banc ────────────────────────────────────────
|
||||
//
|
||||
// Le test ENREGISTRE lui-même la box au lieu de compter sur une installation déjà
|
||||
// stockée. Ce n'est pas du confort : `flutter test integration_test` **réinstalle**
|
||||
// l'app, ce qui vide `shared_preferences` et le stockage sécurisé — donc efface les
|
||||
// installations. Un test qui suppose l'état du téléphone échouerait dès le premier
|
||||
// run et, pire, laisserait l'appareil sans aucune box enregistrée.
|
||||
//
|
||||
// `connectNew()` se connecte ET persiste : la box est donc rendue au téléphone.
|
||||
final ctx = tester.element(find.byType(MaterialApp).first);
|
||||
final svc = Provider.of<NymeaService>(ctx, listen: false);
|
||||
final cnx = Provider.of<ConnectionManager>(ctx, listen: false);
|
||||
|
||||
if (!svc.isConnected) {
|
||||
// ignore: unawaited_futures
|
||||
cnx.connectNew(host: _hote);
|
||||
}
|
||||
await _attendre(tester, () => svc.isConnected,
|
||||
limite: const Duration(seconds: 60), quoi: 'la connexion à $_hote');
|
||||
|
||||
// ── 2. Le schéma est lu à la connexion, pas à l'ouverture d'un écran ─────
|
||||
final schema = Provider.of<SchemaProvider>(ctx, listen: false);
|
||||
await _attendre(tester, () => schema.schema.loaded,
|
||||
limite: const Duration(seconds: 30), quoi: 'la lecture du schéma');
|
||||
expect(schema.schema.hasMethod('NymeaEnergy.SetLoadConfig'), isTrue);
|
||||
expect(schema.schema.experienceVersions['NymeaEnergy'], isNotNull);
|
||||
debugPrint('[appareil] schéma lu — ${schema.versionLine()}');
|
||||
|
||||
// ── 3. L'écran fusionné, et les charges RÉELLES de la box ────────────────
|
||||
//
|
||||
// Le contexte du MaterialApp est AU-DESSUS du Router : `GoRouter.of` n'y trouve
|
||||
// rien. On prend donc un contexte pris sous lui — celui d'un Scaffold affiché.
|
||||
final ctxSousRouteur = tester.element(find.byType(Scaffold).first);
|
||||
GoRouter.of(ctxSousRouteur).go('/energy/setup');
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
|
||||
final config = Provider.of<LoadConfigProvider>(ctx, listen: false);
|
||||
await _attendre(tester, () => config.entries.isNotEmpty,
|
||||
limite: const Duration(seconds: 30), quoi: 'les charges de GetLoadConfig');
|
||||
|
||||
final ids = config.entries.map((e) => e.id).toList()..sort();
|
||||
debugPrint('[appareil] charges lues : $ids');
|
||||
expect(ids, contains('chauffe-eau'));
|
||||
expect(ids, contains('pac-terrain'));
|
||||
|
||||
// Les deux charges s'affichent, avec un bouton de détail chacune.
|
||||
await _attendre(
|
||||
tester,
|
||||
() => find.text('Configurer').evaluate().length >= 2,
|
||||
quoi: 'les cartes de charge à l\'écran',
|
||||
);
|
||||
expect(find.text('chauffe-eau'), findsWidgets);
|
||||
|
||||
// La PAC vient du lot B-bis : avant lui, elle était arbitrée sans être
|
||||
// configurable. Sa présence ICI est la preuve visible du contrat.
|
||||
expect(find.text('PAC banc'), findsWidgets);
|
||||
|
||||
// ── 4. Une écriture RÉELLE, non matérielle, et sa confirmation ───────────
|
||||
final avant = config.entries.firstWhere((e) => e.id == 'chauffe-eau');
|
||||
const nouveau = 'chauffe-eau (sonde appareil)';
|
||||
|
||||
config.setLabel('chauffe-eau', nouveau);
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(config.hasPendingChanges, isTrue);
|
||||
// Un renommage ne touche à aucun matériel : rien à annoncer.
|
||||
expect(config.pendingNeedsWarning, isFalse);
|
||||
|
||||
await config.save();
|
||||
// La confirmation est `LoadConfigChanged`, PAS l'accusé du RPC — mesuré sur le banc,
|
||||
// la notification arrive la première.
|
||||
await _attendre(
|
||||
tester,
|
||||
() => config.saveState == LoadSaveState.confirmed ||
|
||||
config.saveState == LoadSaveState.diverged ||
|
||||
config.saveState == LoadSaveState.rejected,
|
||||
limite: const Duration(seconds: 20),
|
||||
quoi: 'la confirmation par LoadConfigChanged',
|
||||
);
|
||||
debugPrint('[appareil] écriture → ${config.saveState}');
|
||||
expect(config.saveState, LoadSaveState.confirmed);
|
||||
expect(
|
||||
config.entries.firstWhere((e) => e.id == 'chauffe-eau').label,
|
||||
nouveau,
|
||||
);
|
||||
|
||||
// ── 5. Restauration — le banc n'appartient pas au test ──────────────────
|
||||
config.setLabel('chauffe-eau', avant.label);
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
await config.save();
|
||||
await _attendre(
|
||||
tester,
|
||||
() => config.saveState == LoadSaveState.confirmed &&
|
||||
config.entries.firstWhere((e) => e.id == 'chauffe-eau').label ==
|
||||
avant.label,
|
||||
limite: const Duration(seconds: 20),
|
||||
quoi: 'la restauration du libellé',
|
||||
);
|
||||
debugPrint('[appareil] libellé restauré : ${avant.label}');
|
||||
|
||||
// ── 6. On laisse l'écran affiché ────────────────────────────────────────
|
||||
//
|
||||
// `flutter test integration_test` referme l'app dès la dernière assertion. Sans
|
||||
// cette pause, il n'y a matériellement rien à voir sur le téléphone : d'où
|
||||
// l'impression que « rien ne tourne » alors que tout s'est exécuté. On tient donc
|
||||
// l'écran ouvert le temps de le regarder et d'en prendre une capture.
|
||||
debugPrint('[appareil] écran maintenu 90 s — regardez le téléphone');
|
||||
for (var i = 0; i < 360; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 250));
|
||||
}
|
||||
});
|
||||
}
|
||||
39
pubspec.lock
39
pubspec.lock
@ -118,6 +118,11 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -197,6 +202,11 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -237,6 +247,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -437,6 +452,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.5"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -554,6 +577,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sync_http
|
||||
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -682,6 +713,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -53,6 +53,12 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# Pilotage de l'IU SUR L'APPAREIL. Le test s'exécute dans le processus de l'app et
|
||||
# tape sur les widgets depuis l'intérieur : il ne passe donc pas par l'injection
|
||||
# d'événements d'Android, que MIUI refuse sur le Redmi (INJECT_EVENTS).
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user