Files
FlClash/test/widgets/on_demand_test.dart
T
chen08209 d3fa2252b0 feat(android): add launcher shortcuts and one-step on-demand access
Changelog: Launcher shortcuts and one-step on-demand access
2026-10-11 12:28:52 +08:00

357 lines
11 KiB
Dart

import 'dart:async';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/providers/on_demand.dart';
import 'package:fl_clash/providers/state.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/views/config/on_demand.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/services.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:wifi_ssid/wifi_ssid.dart';
import '../helpers/glyph_finders.dart';
import '../helpers/test_app.dart';
class _TestExcludeSSIDs extends ExcludeSSIDs {
_TestExcludeSSIDs(this._initial);
final List<String> _initial;
@override
List<String> build() => _initial;
}
class _TestLocationPermissions extends LocationPermissions {
_TestLocationPermissions(this._initial);
final WifiSsidPermission _initial;
@override
WifiSsidPermission build() => _initial;
}
class _TestBatteryOptimizationIgnored extends BatteryOptimizationIgnored {
_TestBatteryOptimizationIgnored(this._initial);
final bool _initial;
int requests = 0;
@override
Stream<bool> build() => Stream.value(_initial);
@override
Future<void> request() async {
requests++;
state = const AsyncData(true);
}
}
void main() {
late ProviderContainer container;
Future<void> pumpView(
WidgetTester tester, {
List<String> ssids = const [],
WifiSsidPermission permission = WifiSsidPermission.denied,
bool batteryIgnored = false,
bool isAndroid = false,
bool isMacOS = false,
bool isStart = false,
Locale? locale,
Size size = const Size(1400, 1000),
}) async {
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
container = ProviderContainer(
overrides: [
excludeSSIDsProvider.overrideWith(() => _TestExcludeSSIDs(ssids)),
locationPermissionsProvider.overrideWith(
() => _TestLocationPermissions(permission),
),
batteryOptimizationIgnoredProvider.overrideWith(
() => _TestBatteryOptimizationIgnored(batteryIgnored),
),
isStartProvider.overrideWithValue(isStart),
],
);
addTearDown(container.dispose);
globalState.container = container;
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: TestApp(
locale: locale,
child: OnDemandView(isAndroid: isAndroid, isMacOS: isMacOS),
),
),
);
await tester.pumpAndSettle();
}
testWidgets('an empty list shows the placeholder and only the add action', (
tester,
) async {
await pumpView(tester);
expect(find.text('SSIDs are empty'), findsOneWidget);
expect(find.byTooltip('Add'), findsOneWidget);
expect(find.text('Select all'), findsNothing);
expect(find.byGlyph(AppGlyphs.delete), findsNothing);
});
testWidgets('every excluded SSID is rendered', (tester) async {
await pumpView(tester, ssids: ['Home', 'Office']);
expect(find.text('Home'), findsOneWidget);
expect(find.text('Office'), findsOneWidget);
expect(find.text('SSIDs are empty'), findsNothing);
});
testWidgets('selecting an item swaps the header into selection mode', (
tester,
) async {
await pumpView(tester, ssids: ['Home', 'Office']);
expect(find.byGlyph(AppGlyphs.delete), findsNothing);
await tester.tap(find.byType(CommonCheckBox).first);
await tester.pumpAndSettle();
expect(find.byGlyph(AppGlyphs.delete), findsOneWidget);
expect(find.text('Select all'), findsOneWidget);
expect(find.byTooltip('Add'), findsNothing);
});
testWidgets('select all takes every SSID, and pressing it again clears', (
tester,
) async {
await pumpView(tester, ssids: ['Home', 'Office']);
await tester.tap(find.byType(CommonCheckBox).first);
await tester.pumpAndSettle();
await tester.tap(find.text('Select all'));
await tester.pumpAndSettle();
expect(container.read(itemsProvider(_viewKey(tester))), {'Home', 'Office'});
await tester.tap(find.text('Select all'));
await tester.pumpAndSettle();
expect(
find.byTooltip('Add'),
findsOneWidget,
reason: 'selection was cleared',
);
});
testWidgets('deleting removes the selected SSIDs and clears the selection', (
tester,
) async {
await pumpView(tester, ssids: ['Home', 'Office']);
await tester.tap(find.byType(CommonCheckBox).first);
await tester.pumpAndSettle();
await tester.tap(find.byGlyph(AppGlyphs.delete));
await tester.pumpAndSettle();
expect(container.read(excludeSSIDsProvider), ['Office']);
expect(find.text('Home'), findsNothing);
expect(
find.byTooltip('Add'),
findsOneWidget,
reason: 'selection was cleared',
);
});
testWidgets('the location prerequisite reflects a denied permission', (
tester,
) async {
await pumpView(
tester,
permission: WifiSsidPermission.denied,
isMacOS: true,
);
expect(find.bySemanticsLabel('Tap to authorize'), findsOneWidget);
expect(find.bySemanticsLabel('Authorized'), findsNothing);
});
testWidgets('the location prerequisite reflects a granted permission', (
tester,
) async {
await pumpView(
tester,
permission: WifiSsidPermission.granted,
isMacOS: true,
);
expect(find.bySemanticsLabel('Authorized'), findsOneWidget);
expect(find.bySemanticsLabel('Tap to authorize'), findsNothing);
});
testWidgets('a refused location request leaves the permission as it was', (
tester,
) async {
const channel = MethodChannel('wifi_ssid');
final messenger = tester.binding.defaultBinaryMessenger;
messenger.setMockMethodCallHandler(
channel,
(_) async => throw PlatformException(code: 'IN_PROGRESS'),
);
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
await pumpView(tester, isMacOS: true);
await tester.tap(find.text('Tap to authorize'));
await tester.pumpAndSettle();
expect(
container.read(locationPermissionsProvider),
WifiSsidPermission.denied,
);
expect(find.bySemanticsLabel('Tap to authorize'), findsOneWidget);
});
testWidgets('a second tap while the location request waits asks only once', (
tester,
) async {
const channel = MethodChannel('wifi_ssid');
final messenger = tester.binding.defaultBinaryMessenger;
final answer = Completer<Object?>();
var requests = 0;
messenger.setMockMethodCallHandler(channel, (_) {
requests++;
return answer.future;
});
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
await pumpView(tester, isMacOS: true);
container.read(viewSizeProvider.notifier).value = const Size(1400, 1000);
await tester.tap(find.text('Tap to authorize'));
await tester.pump();
await tester.tap(find.text('Tap to authorize'));
await tester.pump();
answer.complete(WifiSsidPermission.permanentlyDenied.index);
await tester.pumpAndSettle();
expect(requests, 1);
expect(find.text('Location permission required'), findsOneWidget);
});
testWidgets('Android also asks to be left out of battery optimization', (
tester,
) async {
await pumpView(tester, isAndroid: true);
expect(find.text('Ignore battery optimization'), findsOneWidget);
expect(find.text('Location permission'), findsOneWidget);
});
testWidgets('authorizing the battery item asks and shows the answer', (
tester,
) async {
await pumpView(tester, isAndroid: true);
final batteryItem = find.ancestor(
of: find.text('Ignore battery optimization'),
matching: find.byType(DecorationListItem),
);
await tester.tap(
find.descendant(of: batteryItem, matching: find.text('Tap to authorize')),
);
await tester.pumpAndSettle();
final notifier =
container.read(batteryOptimizationIgnoredProvider.notifier)
as _TestBatteryOptimizationIgnored;
expect(notifier.requests, 1);
expect(
find.descendant(
of: batteryItem,
matching: find.bySemanticsLabel('Authorized'),
),
findsOneWidget,
);
});
testWidgets('an exempt app does not ask again', (tester) async {
await pumpView(tester, isAndroid: true, batteryIgnored: true);
final batteryItem = find.ancestor(
of: find.text('Ignore battery optimization'),
matching: find.byType(DecorationListItem),
);
await tester.tap(
find.descendant(of: batteryItem, matching: find.text('Authorized')),
);
await tester.pumpAndSettle();
final notifier =
container.read(batteryOptimizationIgnoredProvider.notifier)
as _TestBatteryOptimizationIgnored;
expect(notifier.requests, 0);
});
testWidgets('while running the battery item still shows and asks', (
tester,
) async {
await pumpView(tester, isAndroid: true, isStart: true);
await tester.tap(find.text('Tap to authorize').first);
await tester.pumpAndSettle();
final notifier =
container.read(batteryOptimizationIgnoredProvider.notifier)
as _TestBatteryOptimizationIgnored;
expect(notifier.requests, 1);
expect(find.byTooltip('Tip'), findsNothing);
});
testWidgets('the authorize action trails its row, the reason the group', (
tester,
) async {
await pumpView(tester, isMacOS: true, locale: const Locale('ru'));
final appLocalizations = AppLocalizations.of(
tester.element(find.byType(OnDemandView)),
);
final row = tester.getRect(find.byType(DecorationListItem).first);
final desc = find.text(appLocalizations.locationPermissionDesc);
final button = tester.getRect(
find
.ancestor(
of: find.text(appLocalizations.tapToAuthorize),
matching: find.byType(FilledButton),
)
.first,
);
expect(button.center.dy, closeTo(row.center.dy, 1));
expect(button.right, closeTo(row.right - 16, 1));
expect(tester.getTopLeft(desc).dy, greaterThanOrEqualTo(row.bottom));
});
testWidgets('a desktop that is not macOS asks for no prerequisite', (
tester,
) async {
await pumpView(tester);
expect(find.text('Ignore battery optimization'), findsNothing);
expect(find.text('Location permission'), findsNothing);
expect(find.bySemanticsLabel('Tap to authorize'), findsNothing);
});
}
/// The view keys its shared selection notifier by [UniqueKeyStateMixin.key], so
/// a test has to read the same id off the mounted state.
String _viewKey(WidgetTester tester) {
final state = tester.state(find.byType(OnDemandView)) as dynamic;
return state.key as String;
}