initial commit

This commit is contained in:
2026-07-27 14:42:23 +02:00
commit 833c68a189
257 changed files with 17947 additions and 0 deletions
@@ -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);
});
}
+44
View File
@@ -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);
});
}
+37
View File
@@ -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);
});
}
+29
View File
@@ -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);
});
}
+23
View File
@@ -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);
}
});
}
+50
View File
@@ -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);
});
}
+33
View File
@@ -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)));
});
}