aaf934c2d1
Rework the application layer end to end: core IPC client, providers, models, database, managers, and shared utilities, with riverpod 3.4.2 and freezed 4.0.1. Keep a rejected profile selected, push the empty config to Core when a profile fails to build, and fall back to the default activity icon for package icons. Add opt-in proxy authentication that injects credentials into the generated profile and the runtime config, force-clears profile-provided skip-auth-prefixes so loopback stays covered, sends the credentials with the app's own proxied requests, and withholds the Android VPN system proxy while credentials are set. HelperCoreLease treats an unreachable Helper as confirmation once the OS reports the Core pid gone, and the lifecycle reconcile retries a parked stop, so the Core comes back after the Helper service dies. Launch arguments seed LinkManager so a clash:// link raises a silently started window, the access control app list is fetched on every page entry, the desktop header matches a standard title bar height, and the tray icons ship as resolution-aware assets scoped per platform. The version moves to 0.8.97+2026090101. Rule.parse mirrors mihomo's ParseRulePayload: the type is case-insensitive, MATCH rules keep no content field, a two-field rule is payload without a target except for comma-payload types whose lone field is the target, params are matched whole, and the wildcard and REMATCH-NAME types the core accepts get their own actions instead of collapsing into DOMAIN. Window routes show, hide and toggle through one serialized queue and defers the macOS accessory activation switch until a second after the last regular switch, so hotkey bursts no longer leave stray Dock icons. The hotkey manager registers through rust_api instead of hotkey_manager, the window header restyles the caption buttons after the native Windows title bar, and the proxies tab keeps its tab controller alive while the empty state animates in. Bootstrap reads the Android dynamic palette from the plugin channel without the deprecated CorePalette, dynamic_color moves to its material_ui release, navigator_resizable 3.1.0 fixes an assertion when popping mid-transition, reorderable_grid goes, and the SDK floor rises to 3.10. Changelog: Rework the app layer and window handling, and add proxy authentication
216 lines
6.1 KiB
Dart
216 lines
6.1 KiB
Dart
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/common/theme.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/l10n/l10n.dart';
|
|
import 'package:fl_clash/manager/status_manager.dart';
|
|
import 'package:fl_clash/providers/app.dart';
|
|
import 'package:fl_clash/providers/database.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'helpers/test_profiles.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late ProviderContainer container;
|
|
|
|
setUp(() {
|
|
container = ProviderContainer(
|
|
overrides: [profilesProvider.overrideWith(TestProfiles.new)],
|
|
);
|
|
globalState.container = container;
|
|
container.read(viewSizeProvider.notifier).value = const Size(1200, 1000);
|
|
});
|
|
|
|
tearDown(() => container.dispose());
|
|
|
|
Future<void> pumpHost(WidgetTester tester) async {
|
|
tester.view.physicalSize = const Size(1200, 1000);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
await tester.pumpWidget(
|
|
UncontrolledProviderScope(
|
|
container: container,
|
|
child: MaterialApp(
|
|
navigatorKey: globalState.navigatorKey,
|
|
localizationsDelegates: const [
|
|
AppLocalizations.delegate,
|
|
...GlobalMaterialLocalizations.delegates,
|
|
],
|
|
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
|
builder: (context, child) {
|
|
globalState.measure = Measure.of(context, 1);
|
|
globalState.theme = CommonTheme.of(context, 1);
|
|
return StatusManager(child: child!);
|
|
},
|
|
home: const SizedBox(key: Key('child')),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
group('safeRun', () {
|
|
testWidgets('returns the value and brackets it with start/end', (
|
|
tester,
|
|
) async {
|
|
await pumpHost(tester);
|
|
final events = <String>[];
|
|
|
|
final result = await globalState.safeRun<int>(
|
|
() async {
|
|
events.add('run');
|
|
return 42;
|
|
},
|
|
onStart: () => events.add('start'),
|
|
onEnd: () => events.add('end'),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(result, 42);
|
|
expect(events, ['start', 'run', 'end']);
|
|
});
|
|
|
|
testWidgets('swallows the error, returns null, and still calls onEnd', (
|
|
tester,
|
|
) async {
|
|
await pumpHost(tester);
|
|
final events = <String>[];
|
|
|
|
final result = await globalState.safeRun<int>(
|
|
() async => throw StateError('boom'),
|
|
onStart: () => events.add('start'),
|
|
onEnd: () => events.add('end'),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(result, isNull);
|
|
expect(events, ['start', 'end']);
|
|
});
|
|
|
|
testWidgets('surfaces a silent failure through the notifier', (
|
|
tester,
|
|
) async {
|
|
await pumpHost(tester);
|
|
|
|
await globalState.safeRun<int>(
|
|
() async => throw StateError('quiet failure'),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.textContaining('quiet failure'), findsOneWidget);
|
|
expect(find.byType(AlertDialog), findsNothing);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
});
|
|
|
|
testWidgets('surfaces a loud failure through a dialog', (tester) async {
|
|
await pumpHost(tester);
|
|
|
|
await globalState.safeRun<int>(
|
|
() async => throw StateError('loud failure'),
|
|
title: 'Backup',
|
|
silence: false,
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(AlertDialog), findsOneWidget);
|
|
expect(find.textContaining('loud failure'), findsOneWidget);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
});
|
|
|
|
testWidgets('accepts a synchronous function', (tester) async {
|
|
await pumpHost(tester);
|
|
|
|
final result = await globalState.safeRun<String>(() => 'sync');
|
|
|
|
expect(result, 'sync');
|
|
});
|
|
});
|
|
|
|
group('loadingRun', () {
|
|
testWidgets('raises and clears the loading flag around the work', (
|
|
tester,
|
|
) async {
|
|
await pumpHost(tester);
|
|
const tag = LoadingTag.backup_restore;
|
|
bool? loadingDuringRun;
|
|
|
|
final result = await globalState.loadingRun<int>(() async {
|
|
loadingDuringRun = container.read(loadingProvider(tag));
|
|
return 7;
|
|
}, tag: tag);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(result, 7);
|
|
expect(loadingDuringRun, isTrue);
|
|
expect(
|
|
container.read(loadingProvider(tag)),
|
|
isTrue,
|
|
reason: 'the flag is held for a minimum duration to avoid flicker',
|
|
);
|
|
|
|
await tester.pump(const Duration(milliseconds: 1100));
|
|
expect(container.read(loadingProvider(tag)), isFalse);
|
|
});
|
|
|
|
testWidgets('clears the loading flag when the work throws', (tester) async {
|
|
await pumpHost(tester);
|
|
const tag = LoadingTag.backup_restore;
|
|
|
|
final result = await globalState.loadingRun<int>(
|
|
() async => throw StateError('failed'),
|
|
tag: tag,
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(result, isNull);
|
|
await tester.pump(const Duration(milliseconds: 1100));
|
|
expect(container.read(loadingProvider(tag)), isFalse);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
});
|
|
|
|
testWidgets('runs without a tag', (tester) async {
|
|
await pumpHost(tester);
|
|
|
|
final result = await globalState.loadingRun<int>(
|
|
() async => 1,
|
|
tag: null,
|
|
);
|
|
|
|
expect(result, 1);
|
|
});
|
|
});
|
|
|
|
group('canCrashCoreFor', () {
|
|
test('is allowed in debug builds and on the dev channel', () {
|
|
expect(
|
|
GlobalState.canCrashCoreFor(isDebug: true, appEnv: 'stable'),
|
|
isTrue,
|
|
);
|
|
expect(
|
|
GlobalState.canCrashCoreFor(isDebug: false, appEnv: 'dev'),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('is refused for release stable and pre builds', () {
|
|
expect(
|
|
GlobalState.canCrashCoreFor(isDebug: false, appEnv: 'stable'),
|
|
isFalse,
|
|
);
|
|
expect(
|
|
GlobalState.canCrashCoreFor(isDebug: false, appEnv: 'pre'),
|
|
isFalse,
|
|
);
|
|
});
|
|
});
|
|
}
|