Optimize more details

This commit is contained in:
chen08209
2026-08-05 10:57:40 +08:00
parent 5f2c12642b
commit 1cffeaa29e
112 changed files with 6270 additions and 2437 deletions
+74
View File
@@ -18,6 +18,60 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('initial desktop layout does not animate mobile navigation out', (
tester,
) async {
tester.view.physicalSize = const Size(1200, 800);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final container = ProviderContainer(
overrides: [
navigationItemsStateProvider.overrideWithValue(
NavigationItemsState(
value: [
NavigationItem(
icon: const Icon(Icons.space_dashboard),
label: PageLabel.dashboard,
builder: (_) => const SizedBox.shrink(),
),
NavigationItem(
icon: const Icon(Icons.construction),
label: PageLabel.tools,
builder: (_) => const SizedBox.shrink(),
),
],
),
),
],
);
addTearDown(container.dispose);
globalState.container = container;
expect(container.read(viewSizeProvider), Size.zero);
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: const _ThemeManagedTestApp(),
),
);
expect(globalState.navigatorKey.currentContext, isNotNull);
expect(container.read(viewSizeProvider), const Size(1200, 800));
expect(find.byType(NavigationBar), findsNothing);
await tester.pump();
expect(find.byType(NavigationRail), findsOneWidget);
expect(find.byType(NavigationBar), findsNothing);
await tester.pump(const Duration(milliseconds: 150));
expect(find.byType(NavigationBar), findsNothing);
expect(tester.takeException(), isNull);
});
testWidgets(
'screen-size transition preserves current content and animates navigation',
(tester) async {
@@ -610,6 +664,26 @@ class _TestApp extends StatelessWidget {
}
}
class _ThemeManagedTestApp extends StatelessWidget {
const _ThemeManagedTestApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.delegate.supportedLocales,
builder: (_, child) => ThemeManager(child: child!),
home: const HomePage(),
);
}
}
class _StatefulContent extends StatefulWidget {
const _StatefulContent({super.key});
+14
View File
@@ -11,6 +11,20 @@ void main() {
expect(results.rest, ['android']);
});
test('accepts dev application environment', () {
final results = setup.createSetupArgParser().parse([
'android',
'--env',
'dev',
]);
expect(results['env'], 'dev');
});
test('Flutter build environment does not depend on Core SHA256', () {
expect(setup.createBuildEnvironment('dev'), {'APP_ENV': 'dev'});
});
test('omits verbose from flutter build args by default', () {
final args = setup.createFlutterBuildArgs(
platform: 'android',
+10
View File
@@ -0,0 +1,10 @@
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('GlobalState exposes a fallback accent color before plugin startup', () {
expect(GlobalState().accentColor, const Color(defaultPrimaryColor));
});
}
+4 -4
View File
@@ -12,7 +12,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('dashboard limits a wide grid to 12 centered columns', (
testWidgets('dashboard limits a wide grid to 16 centered columns', (
tester,
) async {
tester.view.physicalSize = const Size(1600, 1000);
@@ -39,9 +39,9 @@ void main() {
await tester.pump();
final grid = find.byType(Grid);
expect(tester.widget<Grid>(grid).crossAxisCount, 12);
expect(tester.getSize(grid).width, 840);
expect(tester.getTopLeft(grid).dx, 380);
expect(tester.widget<Grid>(grid).crossAxisCount, 16);
expect(tester.getSize(grid).width, 1120);
expect(tester.getTopLeft(grid).dx, 240);
expect(tester.takeException(), null);
});
}
+31
View File
@@ -0,0 +1,31 @@
import 'package:fl_clash/state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('crash test availability', () {
test('is enabled for debug builds', () {
expect(
GlobalState.canCrashCoreFor(isDebug: true, appEnv: 'stable'),
isTrue,
);
});
test('is enabled for dev builds', () {
expect(
GlobalState.canCrashCoreFor(isDebug: false, appEnv: 'dev'),
isTrue,
);
});
test('is disabled for pre and stable release builds', () {
expect(
GlobalState.canCrashCoreFor(isDebug: false, appEnv: 'pre'),
isFalse,
);
expect(
GlobalState.canCrashCoreFor(isDebug: false, appEnv: 'stable'),
isFalse,
);
});
});
}
+353 -21
View File
@@ -1,5 +1,9 @@
import 'dart:ui' as ui;
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/theme.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -9,6 +13,68 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('ListItem.toggle toggles when tapping the row', (tester) async {
bool? changedValue;
await tester.pumpWidget(
_TestApp(
child: Scaffold(
body: ListItem.toggle(
title: const Text('Enabled'),
value: false,
onChanged: (value) {
changedValue = value;
},
),
),
),
);
await tester.tap(find.text('Enabled'));
expect(changedValue, isTrue);
});
testWidgets('ListItem.toggle is disabled without onChanged', (tester) async {
await tester.pumpWidget(
_TestApp(
child: Scaffold(
body: ListItem.toggle(title: const Text('Disabled'), value: false),
),
),
);
final tile = tester.widget<ListTile>(find.byType(ListTile));
final control = tester.widget<Switch>(find.byType(Switch));
expect(tile.onTap, isNull);
expect(control.onChanged, isNull);
await tester.tap(find.text('Disabled'));
await tester.pump();
});
testWidgets('ListItem.checkbox toggles when tapping the row', (tester) async {
bool? changedValue;
await tester.pumpWidget(
_TestApp(
child: Scaffold(
body: ListItem.checkbox(
title: const Text('Selected'),
onChanged: (value) {
changedValue = value;
},
),
),
),
);
await tester.tap(find.text('Selected'));
expect(changedValue, isTrue);
});
testWidgets('ListItem.input limits dialog text by maxLength', (tester) async {
String? changedValue;
@@ -21,14 +87,12 @@ void main() {
child: Scaffold(
body: ListItem.input(
title: const Text('Port'),
delegate: InputDelegate(
title: 'Port',
value: '',
maxLength: 5,
onChanged: (value) {
changedValue = value;
},
),
dialogTitle: 'Port',
value: '',
maxLength: 5,
onChanged: (value) {
changedValue = value;
},
),
),
),
@@ -75,12 +139,274 @@ void main() {
expect(_top(tester, 'b'), lessThan(_top(tester, 'c')));
expect(_top(tester, 'c'), lessThan(_top(tester, 'a')));
});
testWidgets('OptionsDialog returns the tapped option', (tester) async {
String? selected;
await tester.pumpWidget(
_TestApp(
child: Builder(
builder: (context) {
return FilledButton(
onPressed: () async {
selected = await showDialog<String>(
context: context,
builder: (_) => const OptionsDialog<String>(
title: 'Options',
options: ['One', 'Two'],
value: 'One',
textBuilder: _optionText,
),
);
},
child: const Text('Open'),
);
},
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
await tester.tap(find.text('Two'));
await tester.pumpAndSettle();
expect(selected, 'Two');
});
testWidgets('InputDialog validates, submits, and resets values', (
tester,
) async {
String? result;
await tester.pumpWidget(
_TestApp(
child: Builder(
builder: (context) {
return FilledButton(
onPressed: () async {
result = await showDialog<String>(
context: context,
builder: (_) => InputDialog(
title: 'Value',
value: 'changed',
resetValue: 'default',
labelText: 'Value',
suffixText: 'unit',
hintText: 'hint',
validator: (value) => value == 'valid' ? null : 'Invalid',
),
);
},
child: const Text('Open'),
);
},
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
await tester.tap(find.text('Submit'));
await tester.pump();
expect(find.text('Invalid'), findsOneWidget);
await tester.enterText(find.byType(TextFormField), 'valid');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(result, 'valid');
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
await tester.tap(find.text('Reset'));
await tester.pumpAndSettle();
expect(result, 'default');
});
testWidgets('AddDialog returns scalar and map values', (tester) async {
Object? result;
await tester.pumpWidget(
_TestApp(
child: Builder(
builder: (context) {
return Column(
children: [
FilledButton(
onPressed: () async {
result = await showDialog<String>(
context: context,
builder: (_) => const AddDialog(
title: 'Scalar',
valueField: Field(label: 'Value', value: ''),
valueMaxLength: 4,
),
);
},
child: const Text('Scalar'),
),
FilledButton(
onPressed: () async {
result = await showDialog<MapEntry<String, String>>(
context: context,
builder: (_) => const AddDialog(
title: 'Pair',
keyField: Field(label: 'Key', value: ''),
valueField: Field(label: 'Value', value: ''),
keyMaxLength: 3,
valueMaxLength: 4,
),
);
},
child: const Text('Pair'),
),
],
);
},
),
),
);
await tester.tap(find.text('Scalar'));
await tester.pumpAndSettle();
await tester.tap(find.text('Confirm'));
await tester.pump();
expect(find.byType(AddDialog), findsOneWidget);
await tester.enterText(find.byType(TextFormField), 'value');
await tester.tap(find.text('Confirm'));
await tester.pumpAndSettle();
expect(result, 'valu');
await tester.tap(find.text('Pair'));
await tester.pumpAndSettle();
final fields = find.byType(TextFormField);
await tester.enterText(fields.first, 'key1');
await tester.enterText(fields.last, 'value');
await tester.tap(find.text('Confirm'));
await tester.pumpAndSettle();
expect((result! as MapEntry<String, String>).key, 'key');
expect((result! as MapEntry<String, String>).value, 'valu');
});
testWidgets('ListInputPage adds, edits, selects, and deletes items', (
tester,
) async {
await tester.pumpWidget(
const ProviderScope(
child: _TestApp(
child: ListInputPage(
title: 'Items',
items: ['a', 'b'],
titleBuilder: _textBuilder,
subtitleBuilder: _textBuilder,
leadingBuilder: _textBuilder,
itemMaxLength: 4,
),
),
),
);
await tester.tap(find.text('Add'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextFormField), 'c');
await tester.tap(find.text('Confirm'));
await tester.pumpAndSettle();
expect(find.text('c'), findsNWidgets(3));
await tester.tap(find.text('c').first);
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextFormField), 'd');
await tester.tap(find.text('Confirm'));
await tester.pumpAndSettle();
expect(find.text('d'), findsNWidgets(3));
await tester.tap(find.byType(Checkbox).first);
await tester.pump();
expect(find.byIcon(Icons.delete), findsOneWidget);
await tester.tap(find.text('Select all'));
await tester.pump();
await tester.tap(find.byIcon(Icons.delete));
await tester.pump();
expect(find.text('No data'), findsOneWidget);
});
testWidgets('MapInputPage adds, reorders, selects, and deletes entries', (
tester,
) async {
await tester.pumpWidget(
const ProviderScope(
child: _TestApp(
child: MapInputPage(
title: 'Map',
map: {'a': '1', 'b': '2'},
titleBuilder: _entryTitle,
subtitleBuilder: _entrySubtitle,
leadingBuilder: _entryTitle,
keyMaxLength: 4,
valueMaxLength: 4,
),
),
),
);
final listView = tester.widget<ReorderableListView>(
find.byType(ReorderableListView),
);
listView.onReorderItem!(0, 1);
await tester.pump();
expect(
tester.getTopLeft(find.text('b').first).dy,
lessThan(tester.getTopLeft(find.text('a').first).dy),
);
await tester.tap(find.text('Add'));
await tester.pumpAndSettle();
final fields = find.byType(TextFormField);
await tester.enterText(fields.first, 'c');
await tester.enterText(fields.last, '3');
await tester.tap(find.text('Confirm'));
await tester.pumpAndSettle();
expect(find.text('c'), findsNWidgets(2));
await tester.tap(find.byType(Checkbox).first);
await tester.pump();
await tester.tap(find.text('Select all'));
await tester.pump();
await tester.tap(find.byIcon(Icons.delete));
await tester.pump();
expect(find.text('No data'), findsOneWidget);
});
test('NoInputBorder implements border geometry and interior painting', () {
const border = NoInputBorder();
const rect = Rect.fromLTWH(1, 2, 30, 40);
expect(border.copyWith(), isA<NoInputBorder>());
expect(border.scale(2), isA<NoInputBorder>());
expect(border.isOutline, isFalse);
expect(border.dimensions, EdgeInsets.zero);
expect(border.preferPaintInterior, isTrue);
expect(border.getInnerPath(rect).getBounds(), rect);
expect(border.getOuterPath(rect).getBounds(), rect);
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
border.paintInterior(canvas, rect, Paint()..color = Colors.red);
border.paint(canvas, rect);
recorder.endRecording();
});
}
String _optionText(String value) => value;
Widget _textBuilder(String value) {
return Text(value);
}
Widget _entryTitle(MapEntry<String, String> value) => Text(value.key);
Widget _entrySubtitle(MapEntry<String, String> value) => Text(value.value);
double _top(WidgetTester tester, String text) {
return tester.getTopLeft(find.text(text)).dy;
}
@@ -92,20 +418,26 @@ class _TestApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
return ProviderScope(
overrides: [
viewSizeProvider.overrideWithBuild((_, _) => const Size(1200, 1000)),
],
supportedLocales: AppLocalizations.delegate.supportedLocales,
builder: (context, child) {
globalState.theme = CommonTheme.of(context, 1);
return child!;
},
home: child,
child: MaterialApp(
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.delegate.supportedLocales,
builder: (context, child) {
globalState.measure = Measure.of(context, 1);
globalState.theme = CommonTheme.of(context, 1);
return child!;
},
home: child,
),
);
}
}
+105
View File
@@ -3,6 +3,19 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('CommonCircleLoading uses the M3E default size', (tester) async {
await tester.pumpWidget(
const MaterialApp(home: Center(child: CommonCircleLoading())),
);
final customPaint = find.descendant(
of: find.byType(CommonCircleLoading),
matching: find.byType(CustomPaint),
);
expect(tester.getSize(customPaint), const Size.square(48));
});
testWidgets('CommonCircleLoading shrink-wraps when constraints are loose', (
tester,
) async {
@@ -44,4 +57,96 @@ void main() {
expect(tester.getSize(customPaint), const Size.square(32));
});
testWidgets('CommonCircleLoading continuously rotates and morphs', (
tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: SizedBox.square(dimension: 48, child: CommonCircleLoading()),
),
),
);
final loading = find.byType(CommonCircleLoading);
final transform = find.descendant(
of: loading,
matching: find.byType(Transform),
);
final customPaint = find.descendant(
of: loading,
matching: find.byType(CustomPaint),
);
final initialTransform = tester.widget<Transform>(transform).transform;
final initialPainter = tester.widget<CustomPaint>(customPaint).painter!;
await tester.pump(const Duration(milliseconds: 325));
final animatedTransform = tester.widget<Transform>(transform).transform;
final animatedPainter = tester.widget<CustomPaint>(customPaint).painter!;
expect(animatedTransform.storage, isNot(equals(initialTransform.storage)));
expect(animatedPainter.shouldRepaint(initialPainter), isTrue);
for (var i = 0; i < 7; i++) {
await tester.pump(const Duration(milliseconds: 650));
}
expect(tester.takeException(), isNull);
});
testWidgets('CommonCircleLoading exposes optional loading semantics', (
tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: CommonCircleLoading(
semanticLabel: 'Loading profiles',
semanticValue: 'In progress',
),
),
);
expect(find.bySemanticsLabel('Loading profiles'), findsOneWidget);
});
testWidgets('CommonCircleLoading supports the contained M3E API variant', (
tester,
) async {
final colorScheme = ColorScheme.fromSeed(seedColor: Colors.blue);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(colorScheme: colorScheme),
home: Center(
child: CommonCircleLoading(
variant: LoadingIndicatorM3EVariant.contained,
constraints: const BoxConstraints.tightFor(width: 32, height: 32),
padding: const EdgeInsets.all(4),
polygons: const [StarBorder(points: 6), StarBorder(points: 8)],
),
),
),
);
final decoratedBox = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(CommonCircleLoading),
matching: find.byType(DecoratedBox),
),
);
final decoration = decoratedBox.decoration as BoxDecoration;
final customPaint = find.descendant(
of: find.byType(CommonCircleLoading),
matching: find.byType(CustomPaint),
);
expect(decoration.color, colorScheme.primaryContainer);
expect(decoration.borderRadius, BorderRadius.circular(999));
expect(tester.getSize(customPaint), const Size.square(32));
expect(
tester.getSize(find.byType(CommonCircleLoading)),
const Size.square(40),
);
});
}
+115
View File
@@ -0,0 +1,115 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/theme.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/views/dashboard/widgets/memory_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('MemoryInfo refreshes only while the app is resumed', (
tester,
) async {
var readCount = 0;
Future<num> readMemory() async {
readCount++;
return readCount;
}
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused);
await tester.pumpWidget(
_TestApp(child: MemoryInfo(memoryReader: readMemory)),
);
await tester.pump();
expect(readCount, 0);
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pump();
expect(readCount, 1);
await tester.pump(const Duration(seconds: 2));
await tester.pump();
expect(readCount, 2);
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused);
await tester.pump(const Duration(seconds: 4));
expect(readCount, 2);
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pump();
expect(readCount, 3);
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('MemoryInfo ignores a request completed in the background', (
tester,
) async {
final requests = <Completer<num>>[];
Future<num> readMemory() {
final request = Completer<num>();
requests.add(request);
return request.future;
}
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pumpWidget(
_TestApp(child: MemoryInfo(memoryReader: readMemory)),
);
await tester.pump();
expect(requests, hasLength(1));
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused);
requests.first.complete(1);
await tester.pump();
await tester.pump(const Duration(seconds: 2));
expect(requests, hasLength(1));
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pump();
expect(requests, hasLength(2));
requests.last.complete(2);
await tester.pump();
await tester.pumpWidget(const SizedBox.shrink());
});
}
class _TestApp extends StatelessWidget {
final Widget child;
const _TestApp({required this.child});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.delegate.supportedLocales,
builder: (context, child) {
globalState.measure = Measure.of(context, 1);
globalState.theme = CommonTheme.of(context, 1);
return child!;
},
home: Scaffold(body: child),
);
}
}
+98
View File
@@ -0,0 +1,98 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/theme.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/palette.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Palette updates color from hue, chroma, and tone controls', (
tester,
) async {
final controller = ValueNotifier<Color>(Colors.blue);
addTearDown(controller.dispose);
await tester.pumpWidget(
_TestApp(
child: Scaffold(
body: SingleChildScrollView(
child: SizedBox(width: 700, child: Palette(controller: controller)),
),
),
),
);
await tester.pump();
expect(find.byType(Slider), findsNWidgets(2));
expect(find.text('Preview'), findsOneWidget);
expect(find.text('Primary'), findsOneWidget);
expect(find.text('100'), findsOneWidget);
final initial = controller.value;
tester.widget<Slider>(find.byType(Slider).first).onChanged!(180);
await tester.pump();
expect(controller.value, isNot(initial));
final hueColor = controller.value;
tester.widget<Slider>(find.byType(Slider).last).onChanged!(8);
await tester.pump();
expect(controller.value, isNot(hueColor));
await tester.tap(find.text('0'));
await tester.pump();
expect(controller.value.computeLuminance(), lessThan(0.01));
expect(tester.takeException(), isNull);
});
testWidgets('Palette lays out narrow tone and preview grids', (tester) async {
final controller = ValueNotifier<Color>(Colors.white);
addTearDown(controller.dispose);
await tester.pumpWidget(
_TestApp(
child: Scaffold(
body: SingleChildScrollView(
child: SizedBox(width: 32, child: Palette(controller: controller)),
),
),
),
);
await tester.pump();
expect(find.text('0'), findsOneWidget);
expect(find.text('100'), findsOneWidget);
expect(find.text('Primary'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
class _TestApp extends StatelessWidget {
const _TestApp({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.delegate.supportedLocales,
builder: (context, child) {
globalState.theme = CommonTheme.of(context, 1);
globalState.measure = Measure.of(context, 1);
return child!;
},
home: child,
),
);
}
}
+155
View File
@@ -0,0 +1,155 @@
import 'package:fl_clash/widgets/side_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('SideSheet applies defaults and custom presentation values', (
tester,
) async {
final controller = SideSheet.createAnimationController(tester);
addTearDown(controller.dispose);
var closed = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SideSheet(
animationController: controller,
onClosing: () {
closed = true;
},
enableDrag: false,
showDragHandle: true,
backgroundColor: Colors.red,
shadowColor: Colors.blue,
elevation: 4,
shape: const RoundedRectangleBorder(),
clipBehavior: Clip.hardEdge,
constraints: const BoxConstraints(minWidth: 240, maxWidth: 240),
builder: (_) => const Text('Content'),
),
),
),
);
final material = tester.widget<Material>(
find
.ancestor(of: find.text('Content'), matching: find.byType(Material))
.first,
);
expect(material.color, Colors.red);
expect(material.shadowColor, Colors.blue);
expect(material.elevation, 4);
expect(tester.getSize(find.byType(SideSheet)).width, 240);
expect(controller.duration, const Duration(milliseconds: 300));
expect(controller.reverseDuration, const Duration(milliseconds: 200));
expect(closed, isFalse);
});
testWidgets(
'modal side sheet animates, returns a value, and uses a barrier',
(tester) async {
String? result;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
return FilledButton(
onPressed: () async {
result = await showModalSideSheet<String>(
context: context,
useSafeArea: true,
isScrollControlled: true,
barrierColor: Colors.black45,
backgroundColor: Colors.green,
elevation: 3,
shape: const RoundedRectangleBorder(),
clipBehavior: Clip.antiAlias,
constraints: const BoxConstraints(maxWidth: 280),
routeSettings: const RouteSettings(name: 'side-sheet'),
anchorPoint: Offset.zero,
builder: (context) {
return SizedBox(
width: 280,
child: TextButton(
onPressed: () => Navigator.pop(context, 'done'),
child: const Text('Close sheet'),
),
);
},
);
},
child: const Text('Open sheet'),
);
},
),
),
),
);
await tester.tap(find.text('Open sheet'));
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(SideSheet), findsOneWidget);
expect(find.byType(AnimatedModalBarrier), findsOneWidget);
await tester.pumpAndSettle();
await tester.tap(find.text('Close sheet'));
await tester.pumpAndSettle();
expect(result, 'done');
expect(find.byType(SideSheet), findsNothing);
expect(tester.takeException(), isNull);
},
);
testWidgets(
'transparent modal side sheet uses supplied controller and dismisses',
(tester) async {
final controller = AnimationController(
vsync: tester,
duration: const Duration(milliseconds: 20),
reverseDuration: const Duration(milliseconds: 20),
);
addTearDown(controller.dispose);
var completed = false;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
home: Scaffold(
body: Builder(
builder: (context) {
return FilledButton(
onPressed: () async {
await showModalSideSheet<void>(
context: context,
barrierColor: Colors.transparent,
transitionAnimationController: controller,
builder: (_) =>
const SizedBox(width: 200, child: Text('Dismiss me')),
);
completed = true;
},
child: const Text('Open transparent'),
);
},
),
),
),
);
await tester.tap(find.text('Open transparent'));
await tester.pumpAndSettle();
expect(find.byType(ModalBarrier), findsWidgets);
await tester.tapAt(const Offset(20, 300));
await tester.pumpAndSettle();
expect(completed, isTrue);
expect(find.text('Dismiss me'), findsNothing);
expect(tester.takeException(), isNull);
},
);
}
+173
View File
@@ -0,0 +1,173 @@
import 'package:fl_clash/widgets/tab.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets(
'CommonTabBar selects enabled segments and ignores disabled ones',
(tester) async {
var selected = 0;
late StateSetter setState;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: StatefulBuilder(
builder: (context, setter) {
setState = setter;
return CommonTabBar<int>(
groupValue: selected,
disabledChildren: const {2},
thumbColor: Colors.blue,
backgroundColor: Colors.grey,
children: const {
0: Text('One'),
1: Text('Two'),
2: Text('Three'),
},
onValueChanged: (value) {
setState(() => selected = value!);
},
);
},
),
),
),
),
);
expect(selected, 0);
await tester.tap(find.text('Two').first);
await tester.pump(const Duration(milliseconds: 500));
expect(selected, 1);
await tester.tap(find.text('Three').first);
await tester.pump();
expect(selected, 1);
setState(() => selected = 0);
await tester.pump();
expect(tester.takeException(), null);
},
);
testWidgets('CommonTabBar commits the pressed segment after a drag', (
tester,
) async {
var selected = 1;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: StatefulBuilder(
builder: (context, setState) {
return CommonTabBar<int>(
groupValue: selected,
thumbColor: Colors.red,
children: const {
0: Text('First'),
1: Text('Selected'),
2: Text('Last'),
},
onValueChanged: (value) {
setState(() => selected = value!);
},
);
},
),
),
),
),
);
final gesture = await tester.startGesture(
tester.getCenter(
find
.ancestor(
of: find.text('First').first,
matching: find.byKey(const ValueKey<int>(0)),
)
.first,
),
);
await tester.pump();
await gesture.moveBy(const Offset(10, 0));
await tester.pump();
await gesture.moveTo(
tester.getCenter(
find
.ancestor(
of: find.text('Last').first,
matching: find.byKey(const ValueKey<int>(2)),
)
.first,
),
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 500));
expect(selected, 0);
expect(tester.takeException(), null);
});
testWidgets('CommonTabBar lays out proportional RTL segments and updates', (
tester,
) async {
var selected = 0;
var proportional = true;
var color = Colors.green;
late StateSetter setState;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
body: Center(
child: IntrinsicWidth(
child: IntrinsicHeight(
child: StatefulBuilder(
builder: (context, setter) {
setState = setter;
return CommonTabBar<int>(
groupValue: selected,
proportionalWidth: proportional,
thumbColor: color,
children: const {
0: Text('Short'),
1: Text('A much longer segment'),
2: Text('End'),
},
onValueChanged: (value) {
setState(() => selected = value!);
},
);
},
),
),
),
),
),
),
),
);
expect(
tester.getSize(find.byType(CommonTabBar<int>)).height,
greaterThan(0),
);
await tester.tap(find.text('End').first);
await tester.pump(const Duration(milliseconds: 500));
expect(selected, 2);
setState(() {
proportional = false;
color = Colors.purple;
});
await tester.pump();
expect(tester.takeException(), null);
});
}