mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-08 01:42:59 +02:00
app: add persistance (#501)
* app: add persistance * app: address review comments * app: do not fail on http error or timeout * app: only load instances if none are cached
This commit is contained in:
+1201
-77
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -288,6 +296,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -296,6 +328,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -312,6 +352,62 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.3"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.17"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -501,6 +597,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -13,6 +13,7 @@ dependencies:
|
||||
package_info_plus: ^8.1.0
|
||||
flutter_svg: ^2.0.10+1
|
||||
url_launcher: ^6.3.1
|
||||
shared_preferences: ^2.3.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -16,10 +16,16 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:potato_mesh_reader/main.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
NodeShortNameCache.instance.clear();
|
||||
});
|
||||
|
||||
test('NodeShortNameCache fetches and memoizes short names', () async {
|
||||
var calls = 0;
|
||||
final client = MockClient((request) async {
|
||||
|
||||
@@ -18,9 +18,17 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:potato_mesh_reader/main.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Unit tests for [MeshMessage] parsing and the sorting helper.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
NodeShortNameCache.instance.clear();
|
||||
});
|
||||
|
||||
group('MeshMessage.fromJson', () {
|
||||
test('parses fields and strips leading bang from sender', () {
|
||||
final msg = MeshMessage.fromJson({
|
||||
@@ -222,13 +230,11 @@ void main() {
|
||||
instances.map((i) => i.domain), ['alpha.example', 'bravo.example']);
|
||||
});
|
||||
|
||||
test('throws on failed fetch', () async {
|
||||
test('returns empty list on failed fetch', () async {
|
||||
final client = MockClient((request) async => http.Response('oops', 500));
|
||||
|
||||
expect(
|
||||
() => fetchInstances(client: client),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
final instances = await fetchInstances(client: client);
|
||||
expect(instances, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
// Copyright © 2025-26 l5yth & contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:potato_mesh_reader/main.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
NodeShortNameCache.instance.clear();
|
||||
});
|
||||
|
||||
test('bootstrap filters private and inactive instances', () async {
|
||||
final nowSeconds = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000;
|
||||
final client = MockClient((request) async {
|
||||
if (request.url.path == '/api/instances' &&
|
||||
request.url.host == 'potatomesh.net') {
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{'name': 'Public Mesh', 'domain': 'public.mesh'},
|
||||
{
|
||||
'name': 'Hidden Mesh',
|
||||
'domain': 'private.mesh',
|
||||
'isPrivate': true
|
||||
},
|
||||
{'name': 'Stale Mesh', 'domain': 'stale.mesh'},
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
if (request.url.path == '/api/instances' &&
|
||||
request.url.host == 'public.mesh') {
|
||||
return http.Response('[]', 200);
|
||||
}
|
||||
if (request.url.path == '/api/instances' &&
|
||||
request.url.host == 'stale.mesh') {
|
||||
return http.Response('[]', 200);
|
||||
}
|
||||
if (request.url.path == '/api/nodes' &&
|
||||
request.url.host == 'public.mesh') {
|
||||
final nodes = List.generate(12, (i) {
|
||||
return {
|
||||
'node_id': '!pub$i',
|
||||
'short_name': 'P$i',
|
||||
'last_heard': nowSeconds,
|
||||
};
|
||||
});
|
||||
return http.Response(jsonEncode(nodes), 200);
|
||||
}
|
||||
if (request.url.path == '/api/nodes' &&
|
||||
request.url.host == 'stale.mesh') {
|
||||
final stale = nowSeconds - (60 * 60 * 48);
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{'node_id': '!old', 'short_name': 'OLD', 'last_heard': stale},
|
||||
{'node_id': '!older', 'short_name': 'OLD2', 'last_heard': stale},
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
if (request.url.path == '/api/messages') {
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{
|
||||
'id': 1,
|
||||
'rx_iso': '2024-01-01T00:00:00Z',
|
||||
'from_id': '!pub1',
|
||||
'to_id': '^',
|
||||
'channel': 1,
|
||||
'portnum': 'TEXT',
|
||||
'text': 'hello'
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'rx_iso': '2024-01-01T00:10:00Z',
|
||||
'from_id': '!pub2',
|
||||
'to_id': '^',
|
||||
'channel': 1,
|
||||
'portnum': 'TEXT',
|
||||
'text': 'world'
|
||||
},
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
if (request.url.path.startsWith('/api/nodes/')) {
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'node_id': '!fallback',
|
||||
'short_name': 'FLBK',
|
||||
'last_heard': nowSeconds
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response('[]', 200);
|
||||
});
|
||||
|
||||
final repository = MeshRepository(client: client, random: Random(1));
|
||||
final result = await repository.bootstrap(initialDomain: 'public.mesh');
|
||||
|
||||
expect(result.instances.map((i) => i.domain), ['public.mesh']);
|
||||
expect(result.selectedDomain, 'public.mesh');
|
||||
expect(result.nodes.length, 12);
|
||||
expect(result.messages.length, 2);
|
||||
});
|
||||
|
||||
test('loadMessages performs incremental refresh after initial sync',
|
||||
() async {
|
||||
final nowSeconds = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000;
|
||||
final limits = <String>[];
|
||||
final client = MockClient((request) async {
|
||||
if (request.url.path == '/api/nodes') {
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{'node_id': '!a', 'short_name': 'A', 'last_heard': nowSeconds},
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
if (request.url.path.startsWith('/api/nodes/')) {
|
||||
return http.Response(
|
||||
jsonEncode(
|
||||
{'node_id': '!a', 'short_name': 'A', 'last_heard': nowSeconds}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
if (request.url.path == '/api/messages') {
|
||||
limits.add(request.url.queryParameters['limit'] ?? '');
|
||||
if (limits.length == 1) {
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{
|
||||
'id': 1,
|
||||
'rx_iso': '2024-01-01T00:00:00Z',
|
||||
'from_id': '!a',
|
||||
'to_id': '^',
|
||||
'channel': 1,
|
||||
'portnum': 'TEXT',
|
||||
'text': 'first'
|
||||
}
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{
|
||||
'id': 2,
|
||||
'rx_iso': '2024-01-01T00:10:00Z',
|
||||
'from_id': '!a',
|
||||
'to_id': '^',
|
||||
'channel': 1,
|
||||
'portnum': 'TEXT',
|
||||
'text': 'new'
|
||||
}
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response('[]', 200);
|
||||
});
|
||||
|
||||
final repository = MeshRepository(client: client);
|
||||
final domainResult = await repository.loadDomainData(
|
||||
domain: 'potatomesh.net',
|
||||
forceFull: true,
|
||||
);
|
||||
expect(domainResult.messages.length, 1);
|
||||
|
||||
final refreshed = await repository.loadMessages(domain: 'potatomesh.net');
|
||||
|
||||
expect(limits.first, '1000');
|
||||
expect(limits.last, '100');
|
||||
expect(refreshed.length, 2);
|
||||
expect(refreshed.last.text, 'new');
|
||||
});
|
||||
|
||||
test('bootstrap falls back to next responsive instance when first fails',
|
||||
() async {
|
||||
final nowSeconds = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000;
|
||||
final client = MockClient((request) async {
|
||||
if (request.url.host == 'potatomesh.net' &&
|
||||
request.url.path == '/api/instances') {
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{'name': 'Broken', 'domain': 'broken.mesh'},
|
||||
{'name': 'Healthy', 'domain': 'healthy.mesh'},
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
if (request.url.host == 'broken.mesh' &&
|
||||
request.url.path == '/api/nodes') {
|
||||
return http.Response('Not found', 404);
|
||||
}
|
||||
if (request.url.host == 'healthy.mesh' &&
|
||||
request.url.path == '/api/nodes') {
|
||||
final nodes = List.generate(10, (i) {
|
||||
return {
|
||||
'node_id': '!ok$i',
|
||||
'short_name': 'OK$i',
|
||||
'last_heard': nowSeconds,
|
||||
};
|
||||
});
|
||||
return http.Response(jsonEncode(nodes), 200);
|
||||
}
|
||||
if (request.url.host == 'healthy.mesh' &&
|
||||
request.url.path == '/api/messages') {
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{
|
||||
'id': 10,
|
||||
'rx_iso': '2024-01-01T00:00:00Z',
|
||||
'from_id': '!ok1',
|
||||
'to_id': '^',
|
||||
'channel': 1,
|
||||
'portnum': 'TEXT',
|
||||
'text': 'hi'
|
||||
}
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
if (request.url.path.startsWith('/api/nodes/')) {
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'node_id': '!ok1',
|
||||
'short_name': 'OK1',
|
||||
'last_heard': nowSeconds
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response('[]', 200);
|
||||
});
|
||||
|
||||
final repository = MeshRepository(client: client);
|
||||
final result = await repository.bootstrap(initialDomain: 'broken.mesh');
|
||||
|
||||
expect(result.selectedDomain, 'healthy.mesh');
|
||||
expect(result.messages.single.text, 'hi');
|
||||
});
|
||||
|
||||
test('bootstrap skips instance discovery when cache is populated', () async {
|
||||
final nowSeconds = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000;
|
||||
final cachedInstances = jsonEncode([
|
||||
{
|
||||
'id': '1',
|
||||
'name': 'Cached Mesh',
|
||||
'domain': 'cached.mesh',
|
||||
'isPrivate': false,
|
||||
'lastUpdateTime': nowSeconds,
|
||||
}
|
||||
]);
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'mesh.instances': cachedInstances,
|
||||
'mesh.selectedDomain': 'cached.mesh',
|
||||
});
|
||||
|
||||
var instancesCalls = 0;
|
||||
final client = MockClient((request) async {
|
||||
if (request.url.path == '/api/instances') {
|
||||
instancesCalls += 1;
|
||||
return http.Response('[]', 200);
|
||||
}
|
||||
if (request.url.path == '/api/nodes') {
|
||||
final nodes = List.generate(10, (i) {
|
||||
return {
|
||||
'node_id': '!cached$i',
|
||||
'short_name': 'C$i',
|
||||
'last_heard': nowSeconds,
|
||||
};
|
||||
});
|
||||
return http.Response(jsonEncode(nodes), 200);
|
||||
}
|
||||
if (request.url.path == '/api/messages') {
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{
|
||||
'id': 20,
|
||||
'rx_iso': '2024-01-01T00:00:00Z',
|
||||
'from_id': '!cached1',
|
||||
'to_id': '^',
|
||||
'channel': 1,
|
||||
'portnum': 'TEXT',
|
||||
'text': 'cached'
|
||||
}
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
if (request.url.path.startsWith('/api/nodes/')) {
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'node_id': '!cached1',
|
||||
'short_name': 'C1',
|
||||
'last_heard': nowSeconds
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response('[]', 200);
|
||||
});
|
||||
|
||||
final repository = MeshRepository(client: client);
|
||||
final result = await repository.bootstrap(initialDomain: 'cached.mesh');
|
||||
|
||||
expect(instancesCalls, 0, reason: 'should not refetch instance list');
|
||||
expect(result.selectedDomain, 'cached.mesh');
|
||||
expect(result.messages.single.text, 'cached');
|
||||
});
|
||||
}
|
||||
@@ -17,10 +17,19 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:potato_mesh_reader/main.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Widget-level tests that exercise UI states and rendering branches.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
NodeShortNameCache.instance.clear();
|
||||
});
|
||||
|
||||
testWidgets('PotatoMeshReaderApp wires theming and home screen',
|
||||
(tester) async {
|
||||
final fetchCalls = <int>[];
|
||||
@@ -47,12 +56,25 @@ void main() {
|
||||
];
|
||||
}
|
||||
|
||||
await tester.pumpWidget(PotatoMeshReaderApp(fetcher: fakeFetch));
|
||||
Future<BootstrapResult> bootstrapper({ProgressCallback? onProgress}) async {
|
||||
onProgress?.call(const BootstrapProgress(stage: 'loading instances'));
|
||||
return BootstrapResult(
|
||||
instances: const [],
|
||||
nodes: const [],
|
||||
messages: await fakeFetch(domain: 'potatomesh.net'),
|
||||
selectedDomain: 'potatomesh.net',
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(PotatoMeshReaderApp(
|
||||
fetcher: fakeFetch,
|
||||
bootstrapper: bootstrapper,
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('PotatoMesh Reader'), findsOneWidget);
|
||||
expect(find.byType(MessagesScreen), findsOneWidget);
|
||||
expect(fetchCalls.length, greaterThanOrEqualTo(2));
|
||||
expect(fetchCalls.length, greaterThanOrEqualTo(1));
|
||||
});
|
||||
|
||||
testWidgets('MessagesScreen shows loading, data, refresh, and empty states',
|
||||
@@ -196,10 +218,37 @@ void main() {
|
||||
MeshInstance(name: 'Mesh Berlin', domain: 'berlin.mesh'),
|
||||
];
|
||||
|
||||
final mockClient = MockClient((request) async {
|
||||
if (request.url.path.contains('/api/nodes')) {
|
||||
return http.Response('[]', 200);
|
||||
}
|
||||
if (request.url.path.contains('/api/messages')) {
|
||||
return http.Response('[]', 200);
|
||||
}
|
||||
if (request.url.path.contains('/api/instances')) {
|
||||
return http.Response(
|
||||
'[{"name":"Mesh Berlin","domain":"berlin.mesh"}]', 200);
|
||||
}
|
||||
return http.Response('[]', 200);
|
||||
});
|
||||
|
||||
Future<BootstrapResult> bootstrapper({ProgressCallback? onProgress}) async {
|
||||
onProgress?.call(const BootstrapProgress(stage: 'loading instances'));
|
||||
final initialMessages = await fetcher(domain: 'potatomesh.net');
|
||||
return BootstrapResult(
|
||||
instances: const [],
|
||||
nodes: const [],
|
||||
messages: initialMessages,
|
||||
selectedDomain: 'potatomesh.net',
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
PotatoMeshReaderApp(
|
||||
fetcher: fetcher,
|
||||
instanceFetcher: ({http.Client? client}) => loader(),
|
||||
bootstrapper: bootstrapper,
|
||||
repository: MeshRepository(client: mockClient),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
@@ -214,9 +263,9 @@ void main() {
|
||||
await tester.tap(find.text('Mesh Berlin').last);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
expect(calls.last, 'berlin.mesh');
|
||||
expect(calls.contains('berlin.mesh'), isTrue);
|
||||
expect(find.text('berlin.mesh'), findsOneWidget);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,8 +15,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:potato_mesh_reader/main.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
testWidgets('SettingsScreen lists instances and updates selection',
|
||||
(tester) async {
|
||||
final selections = <String>[];
|
||||
|
||||
@@ -7,10 +7,16 @@
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:potato_mesh_reader/main.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
NodeShortNameCache.instance.clear();
|
||||
});
|
||||
|
||||
testWidgets('renders messages from fetcher and refreshes list',
|
||||
(WidgetTester tester) async {
|
||||
final sampleMessages = <MeshMessage>[
|
||||
@@ -56,14 +62,29 @@ void main() {
|
||||
return [sampleMessages[idx]];
|
||||
}
|
||||
|
||||
await tester.pumpWidget(PotatoMeshReaderApp(fetcher: mockFetcher));
|
||||
Future<BootstrapResult> bootstrapper({ProgressCallback? onProgress}) async {
|
||||
onProgress?.call(const BootstrapProgress(stage: 'loading instances'));
|
||||
return BootstrapResult(
|
||||
instances: const [],
|
||||
nodes: const [],
|
||||
messages: sampleMessages,
|
||||
selectedDomain: 'potatomesh.net',
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
PotatoMeshReaderApp(
|
||||
fetcher: mockFetcher,
|
||||
bootstrapper: bootstrapper,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('PotatoMesh Reader'), findsOneWidget);
|
||||
expect(find.textContaining('[--:--]'), findsWidgets);
|
||||
expect(find.byType(ChatLine), findsOneWidget);
|
||||
expect(find.textContaining('hello world'), findsNothing);
|
||||
expect(find.textContaining('#TEST'), findsOneWidget);
|
||||
expect(find.byType(ChatLine), findsNWidgets(2));
|
||||
expect(find.textContaining('hello world'), findsOneWidget);
|
||||
expect(find.textContaining('#TEST'), findsWidgets);
|
||||
expect(find.textContaining('<!nodeB>'), findsOneWidget);
|
||||
expect(find.textContaining('second message'), findsOneWidget);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user