Optimize more details
This commit is contained in:
@@ -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});
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user