initial commit
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/models/models.dart';
|
||||
|
||||
Map<String, dynamic> _loadFixture(String name) {
|
||||
final raw = File('test/fixtures/$name').readAsStringSync();
|
||||
return jsonDecode(raw) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Service.fromJson', () {
|
||||
test('parses a fully-populated service including nested backup config', () {
|
||||
final service = Service.fromJson(_loadFixture('service_full.json'));
|
||||
|
||||
expect(service.id, 'svc_123');
|
||||
expect(service.name, 'postgres');
|
||||
expect(service.composeFile, '/opt/compose/postgres/docker-compose.yml');
|
||||
expect(service.composeType, 'docker compose');
|
||||
expect(service.backupFolder, '/opt/data/containers/postgres');
|
||||
expect(service.status, 'up');
|
||||
expect(service.stopOnBackup, isTrue);
|
||||
expect(service.externalBackupStopMinutes, 5);
|
||||
expect(service.externalBackupStartMinutes, 2);
|
||||
|
||||
final backup = service.backup;
|
||||
expect(backup, isNotNull);
|
||||
expect(backup!.sourcePath, '/opt/data/containers/postgres');
|
||||
expect(backup.nextRun, DateTime.parse('2026-07-27T02:00:00Z'));
|
||||
expect(backup.lastRun, DateTime.parse('2026-07-26T02:00:00Z'));
|
||||
|
||||
expect(backup.targets, hasLength(1));
|
||||
final target = backup.targets.single;
|
||||
expect(target.id, 't1');
|
||||
expect(target.method, BackupMethod.restic);
|
||||
expect(target.remote, 'sftp:nas-cold:/backups/edge-01');
|
||||
expect(target.params, {'transfers': '4'});
|
||||
expect(target.schedule, '0 2 * * *');
|
||||
expect(target.lastRun, DateTime.parse('2026-07-26T02:00:00Z'));
|
||||
expect(target.nextRun, DateTime.parse('2026-07-27T02:00:00Z'));
|
||||
|
||||
expect(backup.history, hasLength(1));
|
||||
final execution = backup.history.single;
|
||||
expect(execution.success, isTrue);
|
||||
expect(execution.durationSeconds, 12);
|
||||
expect(execution.targetId, 't1');
|
||||
});
|
||||
|
||||
test('fills in defaults for every omitted/nullable field', () {
|
||||
final service = Service.fromJson(_loadFixture('service_minimal.json'));
|
||||
|
||||
expect(service.id, 'svc_456');
|
||||
expect(service.name, 'n8n');
|
||||
expect(service.status, 'down');
|
||||
expect(service.composeType, isNull);
|
||||
expect(service.backupFolder, isNull);
|
||||
expect(service.stopOnBackup, isFalse);
|
||||
expect(service.externalBackupStopMinutes, 0);
|
||||
expect(service.externalBackupStartMinutes, 0);
|
||||
expect(service.backup, isNull);
|
||||
});
|
||||
|
||||
test('round-trips through toJson/fromJson without losing data', () {
|
||||
final original = Service.fromJson(_loadFixture('service_full.json'));
|
||||
final roundTripped = Service.fromJson(original.toJson());
|
||||
expect(roundTripped, original);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/models/severity_mapping.dart';
|
||||
import 'package:manager/core/theme/status_colors.dart';
|
||||
|
||||
void main() {
|
||||
group('severityForServiceStatus', () {
|
||||
test('maps up/down to good/critical and anything else to warning', () {
|
||||
expect(severityForServiceStatus('up'), Severity.good);
|
||||
expect(severityForServiceStatus('down'), Severity.critical);
|
||||
expect(severityForServiceStatus('unknown'), Severity.warning);
|
||||
expect(severityForServiceStatus('anything-else'), Severity.warning);
|
||||
});
|
||||
});
|
||||
|
||||
group('severityForSuccess', () {
|
||||
test('maps true/false to good/critical', () {
|
||||
expect(severityForSuccess(true), Severity.good);
|
||||
expect(severityForSuccess(false), Severity.critical);
|
||||
});
|
||||
});
|
||||
|
||||
group('severityForNodeReachable', () {
|
||||
test('maps true/false to good/critical', () {
|
||||
expect(severityForNodeReachable(true), Severity.good);
|
||||
expect(severityForNodeReachable(false), Severity.critical);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/network/api_exception.dart';
|
||||
import 'package:manager/core/network/dio_error_mapper.dart';
|
||||
|
||||
RequestOptions _options() => RequestOptions(path: '/v1/services/');
|
||||
|
||||
void main() {
|
||||
group('mapDioException', () {
|
||||
test('maps timeout variants to ApiException.timeout', () {
|
||||
for (final type in [
|
||||
DioExceptionType.connectionTimeout,
|
||||
DioExceptionType.sendTimeout,
|
||||
DioExceptionType.receiveTimeout,
|
||||
]) {
|
||||
final result = mapDioException(DioException(requestOptions: _options(), type: type));
|
||||
expect(result, isA<ApiTimeoutException>());
|
||||
}
|
||||
});
|
||||
|
||||
test('maps connectionError to ApiException.network', () {
|
||||
final result = mapDioException(
|
||||
DioException(
|
||||
requestOptions: _options(),
|
||||
type: DioExceptionType.connectionError,
|
||||
message: 'Failed host lookup',
|
||||
),
|
||||
);
|
||||
expect(result, isA<ApiNetworkException>());
|
||||
expect((result as ApiNetworkException).message, contains('Failed host lookup'));
|
||||
});
|
||||
|
||||
test('maps cancel to ApiException.cancelled', () {
|
||||
final result = mapDioException(
|
||||
DioException(requestOptions: _options(), type: DioExceptionType.cancel),
|
||||
);
|
||||
expect(result, isA<ApiCancelledException>());
|
||||
});
|
||||
|
||||
test('maps badResponse with a {"error": msg} body to ApiException.server, extracting the message', () {
|
||||
final response = Response<dynamic>(
|
||||
requestOptions: _options(),
|
||||
statusCode: 404,
|
||||
data: {'error': 'not found'},
|
||||
);
|
||||
final result = mapDioException(
|
||||
DioException(requestOptions: _options(), type: DioExceptionType.badResponse, response: response),
|
||||
);
|
||||
expect(result, isA<ApiServerException>());
|
||||
final server = result as ApiServerException;
|
||||
expect(server.statusCode, 404);
|
||||
expect(server.message, 'not found');
|
||||
});
|
||||
|
||||
test('maps badResponse without a parseable body to the status message', () {
|
||||
final response = Response<dynamic>(
|
||||
requestOptions: _options(),
|
||||
statusCode: 500,
|
||||
statusMessage: 'Internal Server Error',
|
||||
data: null,
|
||||
);
|
||||
final result = mapDioException(
|
||||
DioException(requestOptions: _options(), type: DioExceptionType.badResponse, response: response),
|
||||
);
|
||||
expect(result, isA<ApiServerException>());
|
||||
expect((result as ApiServerException).message, 'Internal Server Error');
|
||||
});
|
||||
|
||||
test('passes an already-typed ApiException through unchanged', () {
|
||||
const original = ApiException.timeout();
|
||||
expect(mapDioException(original), same(original));
|
||||
});
|
||||
|
||||
test('maps a non-Dio, non-ApiException error to ApiException.unknown', () {
|
||||
final result = mapDioException(FormatException('bad json'));
|
||||
expect(result, isA<ApiUnknownException>());
|
||||
});
|
||||
});
|
||||
|
||||
group('ApiExceptionUserMessage', () {
|
||||
test('server(404) gets a generic "not found" message regardless of body text', () {
|
||||
const exception = ApiException.server(404, 'service not found');
|
||||
expect(exception.userMessage, 'Not found.');
|
||||
});
|
||||
|
||||
test('server(non-404) includes the status code and body message', () {
|
||||
const exception = ApiException.server(400, 'no backup targets configured');
|
||||
expect(exception.userMessage, contains('400'));
|
||||
expect(exception.userMessage, contains('no backup targets configured'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/theme/app_theme.dart';
|
||||
import 'package:manager/core/widgets/data_table_scaffold.dart';
|
||||
import 'package:manager/core/widgets/empty_state.dart';
|
||||
|
||||
void main() {
|
||||
const columns = [DataColumn(label: Text('Name')), DataColumn(label: Text('Status'))];
|
||||
|
||||
testWidgets('renders the given rows', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: DataTableScaffold(
|
||||
columns: columns,
|
||||
rows: const [
|
||||
DataRow(cells: [DataCell(Text('postgres')), DataCell(Text('up'))]),
|
||||
DataRow(cells: [DataCell(Text('grafana')), DataCell(Text('up'))]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('postgres'), findsOneWidget);
|
||||
expect(find.text('grafana'), findsOneWidget);
|
||||
expect(find.byType(DataTable), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows the empty-state fallback instead of an empty table', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: DataTableScaffold(
|
||||
columns: columns,
|
||||
rows: const [],
|
||||
empty: const EmptyState(icon: Icons.layers_outlined, title: 'No services yet'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('No services yet'), findsOneWidget);
|
||||
expect(find.byType(DataTable), findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/theme/app_theme.dart';
|
||||
import 'package:manager/core/widgets/empty_state.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('renders title/message and invokes the action callback', (tester) async {
|
||||
var tapped = false;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: EmptyState(
|
||||
icon: Icons.layers_outlined,
|
||||
title: 'No services yet',
|
||||
message: 'Scan a compose folder to discover services.',
|
||||
actionLabel: 'Scan now',
|
||||
onAction: () => tapped = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('No services yet'), findsOneWidget);
|
||||
expect(find.text('Scan a compose folder to discover services.'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithText(OutlinedButton, 'Scan now'));
|
||||
await tester.pump();
|
||||
expect(tapped, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('omits the action button when none is given', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: const Scaffold(
|
||||
body: EmptyState(icon: Icons.inbox_outlined, title: 'Nothing here'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(OutlinedButton), findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/theme/app_theme.dart';
|
||||
import 'package:manager/core/widgets/error_banner.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('renders the message and invokes onRetry', (tester) async {
|
||||
var retried = false;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: ErrorBanner(
|
||||
message: "Can't reach this node.",
|
||||
onRetry: () => retried = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text("Can't reach this node."), findsOneWidget);
|
||||
await tester.tap(find.text('Retry'));
|
||||
await tester.pump();
|
||||
expect(retried, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('omits the retry button when onRetry is null', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: const Scaffold(body: ErrorBanner(message: 'Something broke.')),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Retry'), findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/theme/app_theme.dart';
|
||||
import 'package:manager/core/widgets/mono_text.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('renders in the JetBrains Mono family, small variant uses a smaller size', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: const Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
MonoText('/opt/compose/postgres/docker-compose.yml'),
|
||||
MonoText('svc_123', small: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final regular = tester.widget<Text>(find.text('/opt/compose/postgres/docker-compose.yml'));
|
||||
final small = tester.widget<Text>(find.text('svc_123'));
|
||||
|
||||
expect(regular.style?.fontFamily, 'JetBrains Mono');
|
||||
expect(small.style?.fontFamily, 'JetBrains Mono');
|
||||
expect(small.style!.fontSize!, lessThan(regular.style!.fontSize!));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/theme/app_theme.dart';
|
||||
import 'package:manager/core/widgets/slide_over_panel.dart';
|
||||
|
||||
void main() {
|
||||
Widget wrap({required bool open, required VoidCallback onClose}) {
|
||||
return MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
const Center(child: Text('List content')),
|
||||
SlideOverPanel(
|
||||
open: open,
|
||||
onClose: onClose,
|
||||
title: 'vaultwarden',
|
||||
body: const Text('Detail body'),
|
||||
actions: [OutlinedButton(onPressed: () {}, child: const Text('Stop'))],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('closed panel ignores taps on its (invisible) close button', (tester) async {
|
||||
var closed = false;
|
||||
await tester.pumpWidget(wrap(open: false, onClose: () => closed = true));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// IgnorePointer(ignoring: true) means the close icon exists in the tree
|
||||
// but isn't hit-testable — tapping it must not fire onClose.
|
||||
await tester.tap(find.byIcon(Icons.close), warnIfMissed: false);
|
||||
await tester.pump();
|
||||
expect(closed, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('open panel responds to the close button and the scrim tap', (tester) async {
|
||||
var closed = false;
|
||||
await tester.pumpWidget(wrap(open: true, onClose: () => closed = true));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('vaultwarden'), findsOneWidget);
|
||||
expect(find.text('Detail body'), findsOneWidget);
|
||||
expect(find.text('Stop'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.close));
|
||||
expect(closed, isTrue);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/theme/app_theme.dart';
|
||||
import 'package:manager/core/widgets/sparkline.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('paints without error for multiple points, one point, and empty data', (tester) async {
|
||||
for (final values in [
|
||||
[12.4, 16.0, 4.1, 19.8, 9.2],
|
||||
[12.4],
|
||||
<double>[],
|
||||
]) {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(body: Center(child: Sparkline(values: values))),
|
||||
),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byType(CustomPaint), findsWidgets);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/theme/app_theme.dart';
|
||||
import 'package:manager/core/theme/status_colors.dart';
|
||||
import 'package:manager/core/widgets/stat_tile.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('renders label, value, qualifier and caption', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: const Scaffold(
|
||||
body: StatTile(
|
||||
label: 'Services',
|
||||
value: '7',
|
||||
valueQualifier: '/ 9 up',
|
||||
caption: '1 down · 1 unknown',
|
||||
severity: Severity.critical,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Services'), findsOneWidget);
|
||||
expect(find.textContaining('7'), findsOneWidget);
|
||||
expect(find.textContaining('/ 9 up'), findsOneWidget);
|
||||
expect(find.text('1 down · 1 unknown'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('severity stripe color matches the resolved status color', (tester) async {
|
||||
final key = GlobalKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: StatTile(key: key, label: 'X', value: '1', severity: Severity.critical),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final context = key.currentContext!;
|
||||
final statusColors = Theme.of(context).extension<StatusColors>()!;
|
||||
|
||||
final stripe = tester.widgetList<Container>(find.byType(Container)).firstWhere(
|
||||
(c) => c.color == statusColors.critical,
|
||||
orElse: () => throw StateError('No stripe container with critical color found'),
|
||||
);
|
||||
expect(stripe.color, statusColors.critical);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/core/theme/app_theme.dart';
|
||||
import 'package:manager/core/theme/status_colors.dart';
|
||||
import 'package:manager/core/widgets/status_pill.dart';
|
||||
|
||||
Widget _wrap(Widget child) => MaterialApp(theme: AppTheme.light(), home: Scaffold(body: Center(child: child)));
|
||||
|
||||
void main() {
|
||||
testWidgets('renders its label', (tester) async {
|
||||
await tester.pumpWidget(_wrap(const StatusPill(label: 'up', severity: Severity.good)));
|
||||
expect(find.text('up'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('good/critical/neutral each resolve to a distinct pill color', (tester) async {
|
||||
Future<Color> pillColorFor(Severity severity) async {
|
||||
await tester.pumpWidget(_wrap(StatusPill(label: severity.name, severity: severity)));
|
||||
final container = tester.widget<Container>(
|
||||
find.descendant(of: find.byType(StatusPill), matching: find.byType(Container)).first,
|
||||
);
|
||||
final decoration = container.decoration as BoxDecoration;
|
||||
return decoration.color!;
|
||||
}
|
||||
|
||||
final good = await pillColorFor(Severity.good);
|
||||
final critical = await pillColorFor(Severity.critical);
|
||||
final neutral = await pillColorFor(Severity.neutral);
|
||||
|
||||
expect(good, isNot(equals(critical)));
|
||||
expect(good, isNot(equals(neutral)));
|
||||
expect(critical, isNot(equals(neutral)));
|
||||
});
|
||||
}
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "svc_123",
|
||||
"name": "postgres",
|
||||
"compose_file": "/opt/compose/postgres/docker-compose.yml",
|
||||
"compose_type": "docker compose",
|
||||
"backup_folder": "/opt/data/containers/postgres",
|
||||
"status": "up",
|
||||
"stop_on_backup": true,
|
||||
"external_backup_stop_minutes": 5,
|
||||
"external_backup_start_minutes": 2,
|
||||
"backup": {
|
||||
"source_path": "/opt/data/containers/postgres",
|
||||
"targets": [
|
||||
{
|
||||
"id": "t1",
|
||||
"name": "nas-cold",
|
||||
"method": "restic",
|
||||
"remote": "sftp:nas-cold:/backups/edge-01",
|
||||
"params": { "transfers": "4" },
|
||||
"schedule": "0 2 * * *",
|
||||
"last_run": "2026-07-26T02:00:00Z",
|
||||
"next_run": "2026-07-27T02:00:00Z"
|
||||
}
|
||||
],
|
||||
"next_run": "2026-07-27T02:00:00Z",
|
||||
"last_run": "2026-07-26T02:00:00Z",
|
||||
"history": [
|
||||
{
|
||||
"id": "bex_1",
|
||||
"timestamp": "2026-07-26T02:00:00Z",
|
||||
"success": true,
|
||||
"duration_seconds": 12,
|
||||
"message": "snapshot 4f2a9c1e created",
|
||||
"target_id": "t1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "svc_456",
|
||||
"name": "n8n",
|
||||
"compose_file": "/opt/compose/n8n/docker-compose.yml",
|
||||
"status": "down"
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:manager/app.dart';
|
||||
import 'package:manager/core/utils/shared_preferences_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
Future<void> _pumpApp(WidgetTester tester, Map<String, Object> prefsValues) async {
|
||||
// Wide enough that NavRailShell renders its expanded (non-narrow) rail,
|
||||
// which is what shows the "NodeMaster" wordmark asserted on below.
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
SharedPreferences.setMockInitialValues(prefsValues);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
// Matches main.dart: no container-wide auto-retry, so a deliberately
|
||||
// unreachable test connection settles into AsyncError deterministically
|
||||
// instead of retrying with backoff mid-test.
|
||||
retry: (retryCount, error) => null,
|
||||
overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
|
||||
child: const App(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('redirects to Settings when no connection is configured', (tester) async {
|
||||
await _pumpApp(tester, {});
|
||||
|
||||
expect(find.text('NodeMaster'), findsOneWidget);
|
||||
// The router redirect lands on Settings — its "Connections" block
|
||||
// header (unique to that screen) proves we're actually there, not
|
||||
// just that the nav rail lists the label.
|
||||
expect(find.text('Connections'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('launches on the Dashboard when a connection is configured', (tester) async {
|
||||
await _pumpApp(tester, {
|
||||
// Port 1 refuses immediately rather than hanging until a timeout,
|
||||
// so the test doesn't depend on real network access or reachability
|
||||
// — only on routing/shell behavior once a connection exists.
|
||||
'connections': '[{"id":"conn_test","name":"Test","baseUrl":"http://127.0.0.1:1"}]',
|
||||
'active_connection_id': 'conn_test',
|
||||
});
|
||||
|
||||
expect(find.text('NodeMaster'), findsOneWidget);
|
||||
expect(find.text('Dashboard'), findsWidgets);
|
||||
expect(find.text('Services'), findsOneWidget);
|
||||
expect(find.text('Settings'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user