diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 6d565ce..d5ab2ca 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -40,6 +40,10 @@ jobs: run: flutter pub get - name: Run Flutter tests with coverage run: flutter test --coverage + - name: Check formatting + run: dart format --set-exit-if-changed . + - name: Analyze Dart code + run: flutter analyze - name: Upload coverage to Codecov if: always() uses: codecov/codecov-action@v5 diff --git a/AGENTS.md b/AGENTS.md index 88a21e1..07e5771 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,11 @@ Use two-space indentation for Ruby and keep `# frozen_string_literal: true` at t JavaScript follows ES modules under `public/assets/js`; co-locate components with `__tests__` folders and use kebab-case filenames. Format Ruby via `bundle exec rufo .` and Python via `black`. Skip committing generated coverage artifacts. +## Flutter Mobile App (`app/`) +The Flutter client lives in `app/`. Keep only the mobile targets (`android/`, `ios/`) under version control unless you explicitly support other platforms. Do not commit Flutter build outputs or editor cruft (`.dart_tool/`, `.flutter-plugins-dependencies`, `.idea/`, `.metadata`, `*.iml`, `.fvmrc` if unused). + +Install dependencies with `cd app && flutter pub get`; format with `dart format .` and lint via `flutter analyze`. Run tests with `cd app && flutter test` and keep widget/unit coverage high—no new code without tests. Commit `pubspec.lock` and analysis options so toolchains stay consistent. + ## Testing Guidelines Ruby specs run with `cd web && bundle exec rspec`, producing SimpleCov output in `coverage/`. Front-end behaviour is verified through Node’s test runner: `cd web && npm test` writes V8 coverage and JUnit XML under `reports/`. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..8eb2449 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,18 @@ + +# FVM Version Cache +.fvm/ + +# Scaffholding +.dart_tool/ +.flutter-plugins-dependencies +.fvmrc +.idea/ +.metadata +android/ +ios/ +linux/ +macos/ +potato_mesh_reader.iml +pubspec.lock +web/ +windows/ diff --git a/app/README.md b/app/README.md index 78949a6..85e2548 100644 --- a/app/README.md +++ b/app/README.md @@ -7,9 +7,6 @@ Meshtastic Reader – read-only PotatoMesh chat client for Android and iOS. ```bash cd app flutter create . -# then replace pubspec.yaml and lib/main.dart with the versions in this repo flutter pub get flutter run ``` - -The app fetches from `https://potatomesh.net/api/messages?limit=100&encrypted=false`. diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/app/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/app/lib/main.dart b/app/lib/main.dart index ce51d7c..dabcabf 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -253,7 +253,8 @@ class SettingsScreen extends StatelessWidget { ListTile( leading: Icon(Icons.info_outline), title: Text('About'), - subtitle: Text('Meshtastic Reader MVP — read-only view of PotatoMesh messages.'), + subtitle: Text( + 'Meshtastic Reader MVP — read-only view of PotatoMesh messages.'), ), ], ), diff --git a/app/pubspec.lock b/app/pubspec.lock new file mode 100644 index 0000000..b1ce71c --- /dev/null +++ b/app/pubspec.lock @@ -0,0 +1,386 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002" + url: "https://pub.dev" + source: hosted + version: "2.4.7" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/app/test/mesh_message_test.dart b/app/test/mesh_message_test.dart index 3d3049c..55bb489 100644 --- a/app/test/mesh_message_test.dart +++ b/app/test/mesh_message_test.dart @@ -134,8 +134,24 @@ void main() { calls.add(request.url); return http.Response( jsonEncode([ - {'id': 2, 'rx_iso': '2024-01-02T00:01:00Z', 'from_id': '!b', 'to_id': '^', 'channel': 1, 'portnum': 'TEXT', 'text': 'Later'}, - {'id': 1, 'rx_iso': '2024-01-01T23:59:00Z', 'from_id': '!a', 'to_id': '^', 'channel': 1, 'portnum': 'TEXT', 'text': 'Earlier'}, + { + 'id': 2, + 'rx_iso': '2024-01-02T00:01:00Z', + 'from_id': '!b', + 'to_id': '^', + 'channel': 1, + 'portnum': 'TEXT', + 'text': 'Later' + }, + { + 'id': 1, + 'rx_iso': '2024-01-01T23:59:00Z', + 'from_id': '!a', + 'to_id': '^', + 'channel': 1, + 'portnum': 'TEXT', + 'text': 'Earlier' + }, ]), 200, ); @@ -159,7 +175,8 @@ void main() { }); test('throws on unexpected response shapes', () async { - final client = MockClient((request) async => http.Response('{"id":1}', 200)); + final client = + MockClient((request) async => http.Response('{"id":1}', 200)); expect( () => fetchMessages(client: client), diff --git a/app/test/messages_screen_test.dart b/app/test/messages_screen_test.dart index aa00e8a..6c6d0e0 100644 --- a/app/test/messages_screen_test.dart +++ b/app/test/messages_screen_test.dart @@ -20,7 +20,8 @@ import 'package:potato_mesh_reader/main.dart'; /// Widget-level tests that exercise UI states and rendering branches. void main() { - testWidgets('PotatoMeshReaderApp wires theming and home screen', (tester) async { + testWidgets('PotatoMeshReaderApp wires theming and home screen', + (tester) async { final fetchCalls = []; Future> fakeFetch() async { fetchCalls.add(1); @@ -50,7 +51,8 @@ void main() { expect(fetchCalls.length, 1); }); - testWidgets('MessagesScreen shows loading, data, refresh, and empty states', (tester) async { + testWidgets('MessagesScreen shows loading, data, refresh, and empty states', + (tester) async { var fetchCount = 0; final completer = Completer>(); Future> fetcher() { @@ -79,7 +81,8 @@ void main() { return Future.error(StateError('no new data')); } - await tester.pumpWidget(MaterialApp(home: MessagesScreen(fetcher: fetcher))); + await tester + .pumpWidget(MaterialApp(home: MessagesScreen(fetcher: fetcher))); expect(find.byType(CircularProgressIndicator), findsOneWidget); diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart new file mode 100644 index 0000000..88b628a --- /dev/null +++ b/app/test/widget_test.dart @@ -0,0 +1,71 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:potato_mesh_reader/main.dart'; + +void main() { + testWidgets('renders messages from fetcher and refreshes list', + (WidgetTester tester) async { + final sampleMessages = [ + MeshMessage( + id: 1, + rxTime: null, + rxIso: '2025-01-01T00:00:00Z', + fromId: '!nodeA', + toId: '^all', + channel: 1, + channelName: 'TEST', + portnum: 'TEXT_MESSAGE_APP', + text: 'hello world', + rssi: -100, + snr: -5.0, + hopLimit: 3, + ), + MeshMessage( + id: 2, + rxTime: null, + rxIso: '2025-01-01T01:00:00Z', + fromId: '!nodeB', + toId: '^all', + channel: 1, + channelName: 'TEST', + portnum: 'TEXT_MESSAGE_APP', + text: 'second message', + rssi: -90, + snr: -4.0, + hopLimit: 3, + ), + ]; + + var fetchCount = 0; + Future> mockFetcher() async { + final idx = fetchCount >= sampleMessages.length + ? sampleMessages.length - 1 + : fetchCount; + fetchCount += 1; + return [sampleMessages[idx]]; + } + + await tester.pumpWidget(PotatoMeshReaderApp(fetcher: mockFetcher)); + await tester.pumpAndSettle(); + + expect(find.text('Meshtastic Reader'), findsOneWidget); + expect(find.text('[--:--]'), findsOneWidget); + expect(find.text(''), findsOneWidget); + expect(find.text('hello world'), findsOneWidget); + expect(find.text('#TEST'), findsOneWidget); + + await tester.tap(find.byTooltip('Refresh')); + await tester.pumpAndSettle(); + + expect(find.text(''), findsOneWidget); + expect(find.text('second message'), findsOneWidget); + expect(find.text(''), findsNothing); + }); +}