mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-13 03:43:41 +00:00
Merge branch 'main' into codex/fix-settings-script-path-picker
This commit is contained in:
52 files changed
+762
-144
No files matched your search
@@ -0,0 +1,139 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gopeed/api/model/downloader_config.dart';
|
||||
import 'package:gopeed/app/application/app_notification_controller.dart';
|
||||
import 'package:gopeed/app/application/app_runtime_controller.dart';
|
||||
import 'package:gopeed/core/common/api_server_state.dart';
|
||||
import 'package:gopeed/core/common/start_config.dart';
|
||||
import 'package:gopeed/core/common/task_event.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
const channel = MethodChannel('dexterous.com/flutter/local_notifications');
|
||||
final calls = <MethodCall>[];
|
||||
var permissionGranted = true;
|
||||
late StreamController<TaskEvent> events;
|
||||
|
||||
setUp(() {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
|
||||
MacOSFlutterLocalNotificationsPlugin.registerWith();
|
||||
calls.clear();
|
||||
permissionGranted = true;
|
||||
events = StreamController<TaskEvent>.broadcast();
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async {
|
||||
calls.add(call);
|
||||
return permissionGranted;
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
unawaited(events.close());
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
|
||||
Future<ProviderContainer> start({required bool enabled}) async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
appRuntimeControllerProvider.overrideWith(() => _TestRuntimeController(enabled)),
|
||||
appNotificationControllerProvider.overrideWith(() => _TestNotificationController(events.stream)),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
await container.read(appRuntimeControllerProvider.future);
|
||||
await container.read(appNotificationControllerProvider.future);
|
||||
return container;
|
||||
}
|
||||
|
||||
Map<dynamic, dynamic> initialization() => calls.lastWhere((call) => call.method == 'initialize').arguments as Map;
|
||||
|
||||
test('macOS requests alerts and sound when download notifications are enabled at startup', () async {
|
||||
await start(enabled: true);
|
||||
|
||||
expect(initialization()['requestAlertPermission'], isTrue);
|
||||
expect(initialization()['requestSoundPermission'], isTrue);
|
||||
expect(initialization()['requestBadgePermission'], isFalse);
|
||||
});
|
||||
|
||||
test('disabled notifications defer authorization until the setting is enabled', () async {
|
||||
final container = await start(enabled: false);
|
||||
expect(initialization()['requestAlertPermission'], isFalse);
|
||||
expect(initialization()['requestSoundPermission'], isFalse);
|
||||
|
||||
final runtime = container.read(appRuntimeControllerProvider.notifier) as _TestRuntimeController;
|
||||
runtime.setNotificationsEnabled(true);
|
||||
await container.pump();
|
||||
await container.read(appNotificationControllerProvider.future);
|
||||
|
||||
expect(initialization()['requestAlertPermission'], isTrue);
|
||||
expect(initialization()['requestSoundPermission'], isTrue);
|
||||
|
||||
runtime.setNotificationsEnabled(false);
|
||||
await container.pump();
|
||||
await container.read(appNotificationControllerProvider.future);
|
||||
expect(initialization()['requestAlertPermission'], isFalse);
|
||||
expect(initialization()['requestSoundPermission'], isFalse);
|
||||
});
|
||||
|
||||
test('denied macOS authorization does not fail notification controller startup', () async {
|
||||
permissionGranted = false;
|
||||
final container = await start(enabled: true);
|
||||
|
||||
expect(container.read(appNotificationControllerProvider).requireValue.started, isTrue);
|
||||
});
|
||||
|
||||
test('terminal task events notify once after enabling and stop after disabling', () async {
|
||||
final container = await start(enabled: false);
|
||||
final runtime = container.read(appRuntimeControllerProvider.notifier) as _TestRuntimeController;
|
||||
const done = TaskEvent(type: TaskEventType.done, taskId: '1', name: 'archive.zip');
|
||||
events.add(done);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(calls.where((call) => call.method == 'show'), isEmpty);
|
||||
|
||||
runtime.setNotificationsEnabled(true);
|
||||
await container.pump();
|
||||
await container.read(appNotificationControllerProvider.future);
|
||||
events.add(done);
|
||||
events.add(const TaskEvent(type: TaskEventType.error, taskId: '2', name: 'failed.zip'));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
final notifications = calls.where((call) => call.method == 'show').toList();
|
||||
expect(notifications, hasLength(2));
|
||||
expect(notifications.map((call) => (call.arguments as Map)['body']), ['archive.zip', 'failed.zip']);
|
||||
|
||||
runtime.setNotificationsEnabled(false);
|
||||
await container.pump();
|
||||
await container.read(appNotificationControllerProvider.future);
|
||||
events.add(done);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(calls.where((call) => call.method == 'show'), hasLength(2));
|
||||
});
|
||||
}
|
||||
|
||||
class _TestNotificationController extends AppNotificationController {
|
||||
_TestNotificationController(this.taskEvents);
|
||||
|
||||
@override
|
||||
final Stream<TaskEvent> taskEvents;
|
||||
}
|
||||
|
||||
class _TestRuntimeController extends AppRuntimeController {
|
||||
_TestRuntimeController(this.enabled);
|
||||
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Future<AppRuntimeState> build() async => _runtime(enabled);
|
||||
|
||||
void setNotificationsEnabled(bool enabled) => state = AsyncData(_runtime(enabled));
|
||||
|
||||
AppRuntimeState _runtime(bool enabled) => AppRuntimeState(
|
||||
startConfig: StartConfig(),
|
||||
apiServerState: ApiServerState.fromJson({}),
|
||||
downloaderConfig: DownloaderConfig()..extra.desktopNotification = enabled,
|
||||
);
|
||||
}
|
||||
@@ -9,9 +9,9 @@ void main() {
|
||||
test('create-task child window has a localized native title', () {
|
||||
expect(
|
||||
AppWindowBootstrap.subWindowTitle(AppWindowType.createTask, appLocalizationsFor('en')),
|
||||
'Create Task - Gopeed',
|
||||
'Gopeed-Create Task',
|
||||
);
|
||||
expect(AppWindowBootstrap.subWindowTitle(AppWindowType.createTask, appLocalizationsFor('zh')), '创建任务 - Gopeed');
|
||||
expect(AppWindowBootstrap.subWindowTitle(AppWindowType.createTask, appLocalizationsFor('zh')), 'Gopeed-创建任务');
|
||||
});
|
||||
|
||||
test('create-task payload only carries initial task data', () {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/widgets.dart' as flutter show Row;
|
||||
import 'package:flutter/foundation.dart' show TargetPlatform, debugDefaultTargetPlatformOverride;
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
@@ -9,7 +11,10 @@ import 'package:gopeed/api/model/create_task.dart';
|
||||
import 'package:gopeed/api/model/downloader_config.dart';
|
||||
import 'package:gopeed/api/model/options.dart';
|
||||
import 'package:gopeed/api/model/request.dart';
|
||||
import 'package:gopeed/api/model/resolve_result.dart';
|
||||
import 'package:gopeed/api/model/resource.dart';
|
||||
import 'package:gopeed/core/capabilities/app_capabilities.dart';
|
||||
import 'package:gopeed/core/capabilities/app_navigation_capability.dart';
|
||||
import 'package:gopeed/core/capabilities/capability_rpc.dart';
|
||||
import 'package:gopeed/core/capabilities/gopeed_capability.dart';
|
||||
import 'package:gopeed/core/capabilities/storage_capability.dart';
|
||||
@@ -19,10 +24,63 @@ import 'package:gopeed/shared/services/download_directory_picker.dart';
|
||||
import 'package:gopeed/shared/theme/app_component_themes.dart';
|
||||
import 'package:gopeed/shared/theme/app_design_tokens.dart';
|
||||
import 'package:gopeed/shared/theme/app_theme.dart';
|
||||
import 'package:gopeed/shared/widgets/app_loading_button.dart';
|
||||
import 'package:gopeed/shared/widgets/app_tooltip.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
void main() {
|
||||
for (final navigationFails in [false, true]) {
|
||||
testWidgets('child creation selects downloads before closing (navigation fails: $navigationFails)', (tester) async {
|
||||
tester.view.physicalSize = const Size(1024, 720);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final events = <String>[];
|
||||
const channel = MethodChannel('window_manager');
|
||||
final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
events.add(call.method);
|
||||
return null;
|
||||
});
|
||||
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
|
||||
final config = DownloaderConfig(downloadDir: '/downloads')..extra.defaultDirectDownload = true;
|
||||
final registry = CapabilityRegistry(createAppCapabilityCodecs())
|
||||
..bind(GopeedMethods.getConfig, (_) => config)
|
||||
..bind(StorageMethods.saveCreateHistory, (_) => const RpcUnit())
|
||||
..bind(GopeedMethods.createTask, (_) {
|
||||
events.add('created');
|
||||
return 'task-id';
|
||||
})
|
||||
..bind(NavigationMethods.showDownloadingTasks, (_) {
|
||||
events.add('downloads');
|
||||
if (navigationFails) throw StateError('Navigation unavailable');
|
||||
return const RpcUnit();
|
||||
});
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [appCapabilitiesProvider.overrideWithValue(AppCapabilities(LocalCapabilityInvoker(registry)))],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: AppComponentThemes(
|
||||
child: CreateTaskWindowPage(
|
||||
windowController: WindowController.fromWindowId('child'),
|
||||
initialTask: CreateTask(req: Request(url: 'https://example.com/file.apk')),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Confirm'));
|
||||
// The mocked native close deliberately leaves the widget mounted.
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(events, ['created', 'downloads', 'close']);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('repeated external tasks update the open create route and replace request metadata', (tester) async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
try {
|
||||
@@ -240,7 +298,10 @@ void main() {
|
||||
|
||||
final connectionsInput = find.byKey(const ValueKey('create-task-connections-input'));
|
||||
expect(connectionsInput, findsOneWidget);
|
||||
expect(tester.getSize(connectionsInput).width, AppDesignTokens.settingsNumberControlWidth);
|
||||
expect(
|
||||
tester.getSize(connectionsInput).width,
|
||||
tester.getSize(find.byKey(const ValueKey('create-task-rename-input'))).width,
|
||||
);
|
||||
final connectionsField = tester.widget<shad.TextField>(connectionsInput);
|
||||
expect(connectionsField.controller!.text, '12');
|
||||
expect(connectionsField.features.single, isA<shad.InputSpinnerFeature>());
|
||||
@@ -399,6 +460,57 @@ void main() {
|
||||
expect(submitted?.opts?.asDefaultPath, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('resolved task keeps rename empty and uses equal-width single-line actions', (WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(700, 700);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
final registry = CapabilityRegistry(createAppCapabilityCodecs())
|
||||
..bind(GopeedMethods.getConfig, (_) => DownloaderConfig(downloadDir: '/downloads'))
|
||||
..bind(
|
||||
GopeedMethods.resolve,
|
||||
(_) => ResolveResult(
|
||||
id: 'resolved-id',
|
||||
res: Resource(
|
||||
name: 'server-generated-name',
|
||||
files: [FileInfo(name: 'video.mp4', size: 1024)],
|
||||
),
|
||||
),
|
||||
)
|
||||
..bind(StorageMethods.saveCreateHistory, (_) => const RpcUnit());
|
||||
final capabilities = AppCapabilities(LocalCapabilityInvoker(registry));
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [appCapabilitiesProvider.overrideWithValue(capabilities)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: AppComponentThemes(child: const CreateTaskWindowPage()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byKey(const ValueKey('create-task-url-input')), 'https://example.com/video.mp4');
|
||||
await tester.tap(find.byKey(const ValueKey('create-task-confirm-button')));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(_fieldText(tester, 'create-task-rename-input'), isEmpty);
|
||||
final createButton = tester.widget<AppLoadingButton>(find.byKey(const ValueKey('resolve-create-button')));
|
||||
expect(createButton.child, isA<Text>());
|
||||
final createText = createButton.child as Text;
|
||||
expect(createText.data, 'Create');
|
||||
expect(createText.maxLines, 1);
|
||||
expect(createText.softWrap, isFalse);
|
||||
expect(
|
||||
tester.getSize(find.byKey(const ValueKey('resolve-cancel-button'))),
|
||||
tester.getSize(find.byKey(const ValueKey('resolve-create-button'))),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('create history supports filtering, deleting one entry, and clearing all entries', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gopeed/features/tasks/presentation/widgets/resolve_task_actions.dart';
|
||||
import 'package:gopeed/l10n/l10n.dart';
|
||||
import 'package:gopeed/shared/theme/app_component_themes.dart';
|
||||
import 'package:gopeed/shared/theme/app_theme.dart';
|
||||
import 'package:gopeed/shared/widgets/app_loading_button.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
void main() {
|
||||
for (final width in [390.0, 1024.0]) {
|
||||
for (final language in ['en', 'zh', 'de']) {
|
||||
testWidgets('resolve actions stay equal at $width in $language, including loading', (tester) async {
|
||||
tester.view.physicalSize = Size(width, 760);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
var submitting = false;
|
||||
var cancelled = false;
|
||||
await tester.pumpWidget(
|
||||
shad.ShadcnApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: Builder(
|
||||
builder: (context) => Localizations.override(
|
||||
context: context,
|
||||
locale: Locale(language),
|
||||
delegates: const [AppLocalizations.delegate],
|
||||
child: AppComponentThemes(
|
||||
child: Center(
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) => shad.AlertDialog(
|
||||
padding: const EdgeInsets.all(18),
|
||||
content: SizedBox(width: width < 760 ? width - 32 : 720, height: 100),
|
||||
actions: [
|
||||
Flexible(
|
||||
child: ResolveTaskActions(
|
||||
submitting: submitting,
|
||||
onCancel: () => cancelled = true,
|
||||
onCreate: () => setState(() => submitting = true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final cancel = find.byKey(const ValueKey('resolve-cancel-button'));
|
||||
final create = find.byKey(const ValueKey('resolve-create-button'));
|
||||
final cancelRect = tester.getRect(cancel);
|
||||
final createRect = tester.getRect(create);
|
||||
expect(cancelRect.size, createRect.size);
|
||||
expect(cancelRect.center.dy, createRect.center.dy);
|
||||
expect(cancelRect.left, greaterThanOrEqualTo(0));
|
||||
expect(createRect.right, lessThanOrEqualTo(width));
|
||||
expect(find.text(appLocalizationsFor(language).createAction), findsOneWidget);
|
||||
await tester.tap(cancel);
|
||||
expect(cancelled, isTrue);
|
||||
await tester.tap(create);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(tester.widget<AppLoadingButton>(create).loading, isTrue);
|
||||
expect(tester.widget<shad.SecondaryButton>(cancel).onPressed, isNull);
|
||||
expect(tester.getRect(cancel), cancelRect);
|
||||
expect(tester.getRect(create), createRect);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,30 @@ import 'package:gopeed/shared/theme/app_theme.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
void main() {
|
||||
testWidgets('provides the standard text field appearance', (tester) async {
|
||||
late shad.TextFieldTheme textFieldTheme;
|
||||
|
||||
await tester.pumpWidget(
|
||||
shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: AppComponentThemes(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
textFieldTheme = shad.ComponentTheme.of<shad.TextFieldTheme>(context);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(textFieldTheme.filled, isTrue);
|
||||
expect(textFieldTheme.border, Border.all(color: AppPalette.light.border));
|
||||
expect(textFieldTheme.borderRadius, BorderRadius.circular(AppDesignTokens.controlRadius));
|
||||
expect(textFieldTheme.padding, AppDesignTokens.textFieldPadding);
|
||||
});
|
||||
|
||||
testWidgets('provides the standard checkbox metrics', (tester) async {
|
||||
late shad.CheckboxTheme checkboxTheme;
|
||||
|
||||
|
||||
@@ -134,7 +134,10 @@ void main() {
|
||||
of: find.byKey(const ValueKey('text-only-loading-button')),
|
||||
matching: find.byType(shad.CircularProgressIndicator),
|
||||
);
|
||||
expect(find.text('Login'), findsNothing);
|
||||
final hiddenLabel = find.text('Login');
|
||||
expect(hiddenLabel, findsOneWidget);
|
||||
expect(tester.widget<Opacity>(find.ancestor(of: hiddenLabel, matching: find.byType(Opacity))).opacity, 0);
|
||||
expect(find.ancestor(of: hiddenLabel, matching: find.byType(ExcludeSemantics)), findsOneWidget);
|
||||
expect(loader, findsOneWidget);
|
||||
expect(tester.getRect(loader).center, bounds.center);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,9 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:gopeed/app/app.dart';
|
||||
import 'package:gopeed/app/router/app_router.dart';
|
||||
import 'package:gopeed/core/capabilities/app_capabilities.dart';
|
||||
import 'package:gopeed/core/capabilities/app_navigation_capability.dart';
|
||||
import 'package:gopeed/app/application/app_appearance_controller.dart';
|
||||
import 'package:gopeed/app/application/app_deep_link_controller.dart';
|
||||
import 'package:gopeed/app/application/app_notification_controller.dart';
|
||||
@@ -93,6 +96,38 @@ import 'package:gopeed/util/updater.dart';
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized().platformDispatcher.localeTestValue = const Locale('en');
|
||||
|
||||
testWidgets('child success returns to downloading from completed and another page', (tester) async {
|
||||
await _setTestSize(tester, const Size(1024, 768));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
appRuntimeControllerProvider.overrideWith(FakeRuntimeController.new),
|
||||
tasksControllerProvider.overrideWith(CompletedTasksController.new),
|
||||
settingsControllerProvider.overrideWith(FakeSettingsController.new),
|
||||
],
|
||||
child: const GopeedApp(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Completed'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('done.zip'), findsOneWidget);
|
||||
|
||||
// Exercise the same serialized operation dispatched by a child window.
|
||||
await LocalAppCapabilities.instance.registry.invoke(NavigationMethods.showDownloadingTasks.name, {});
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('done.zip'), findsNothing);
|
||||
expect(find.text('Create Task'), findsOneWidget);
|
||||
|
||||
GoRouter.of(AppRouter.rootNavigatorKey.currentContext!).go('/settings');
|
||||
await tester.pumpAndSettle();
|
||||
await LocalAppCapabilities.instance.capabilities.navigation.showDownloadingTasks();
|
||||
await tester.pumpAndSettle();
|
||||
expect(GoRouter.of(AppRouter.rootNavigatorKey.currentContext!).routeInformationProvider.value.uri.path, '/');
|
||||
expect(find.text('done.zip'), findsNothing);
|
||||
expect(find.text('Create Task'), findsOneWidget);
|
||||
});
|
||||
|
||||
test('Web MCP endpoint excludes page path, query, and hash route', () {
|
||||
expect(
|
||||
webMcpEndpoint(Uri.parse('https://gopeed.example:8443/app?source=desktop#/settings/advanced')),
|
||||
@@ -414,7 +449,7 @@ void main() {
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
home: const AppComponentThemes(child: ExtensionsPage()),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -434,6 +469,7 @@ void main() {
|
||||
final cardRect = tester.getRect(find.byKey(const ValueKey('extension-card-extension-0')));
|
||||
final rightmostCardRect = tester.getRect(find.byKey(const ValueKey('extension-card-extension-2')));
|
||||
final appMarkRect = tester.getRect(find.byKey(const ValueKey('primary-rail-app-mark')));
|
||||
expect(searchRect.height, closeTo(sortRect.height, 0.01));
|
||||
expect(searchRect.width, 240);
|
||||
expect(searchRect.width, lessThan(cardRect.width));
|
||||
expect(sortRect.left - searchRect.right, closeTo(10, 0.01));
|
||||
@@ -466,6 +502,7 @@ void main() {
|
||||
expect(find.byType(PrimaryRail), findsOneWidget);
|
||||
final compactSearchRect = tester.getRect(find.byKey(const ValueKey('extension-search-field-container')));
|
||||
expect(compactSearchRect.width, lessThan(240));
|
||||
expect(compactSearchRect.height, closeTo(sortRect.height, 0.01));
|
||||
expect(
|
||||
tester.getRect(find.byKey(const ValueKey('extension-sort-control'))).center.dy,
|
||||
closeTo(compactSearchRect.center.dy, 0.01),
|
||||
@@ -710,7 +747,7 @@ void main() {
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
home: const AppComponentThemes(child: ExtensionsPage()),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1860,6 +1897,40 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('loading button preserves its content dimensions', (WidgetTester tester) async {
|
||||
var loading = false;
|
||||
late StateSetter setButtonState;
|
||||
await tester.pumpWidget(
|
||||
shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: Center(
|
||||
child: StatefulBuilder(
|
||||
builder: (context, update) {
|
||||
setButtonState = update;
|
||||
return AppLoadingButton(
|
||||
key: const ValueKey('dimension-stable-loading-button'),
|
||||
onPressed: () {},
|
||||
loading: loading,
|
||||
variant: AppLoadingButtonVariant.primary,
|
||||
child: const Text('Confirm'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
final button = find.byKey(const ValueKey('dimension-stable-loading-button'));
|
||||
final idleSize = tester.getSize(button);
|
||||
|
||||
setButtonState(() => loading = true);
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.getSize(button), idleSize);
|
||||
expect(find.descendant(of: button, matching: find.byType(shad.CircularProgressIndicator)), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('failed tracker update does not reload or reset the settings page', (WidgetTester tester) async {
|
||||
await _setTestSize(tester, const Size(1024, 900));
|
||||
final runtimeController = FailingTrackerRuntimeController();
|
||||
@@ -2302,7 +2373,9 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
final createDirectoryInput = find.byKey(const ValueKey('create-task-directory-input'));
|
||||
final createRenameInput = find.byKey(const ValueKey('create-task-rename-input'));
|
||||
final createConnectionsInput = find.byKey(const ValueKey('create-task-connections-input'));
|
||||
expect(tester.getSize(createDirectoryInput).height, tester.getSize(createRenameInput).height);
|
||||
expect(tester.getSize(createConnectionsInput), tester.getSize(createRenameInput));
|
||||
final createDirectoryField = tester.widget<shad.TextField>(createDirectoryInput);
|
||||
final createRenameField = tester.widget<shad.TextField>(
|
||||
find.descendant(of: createRenameInput, matching: find.byType(AppTextField)),
|
||||
@@ -2404,6 +2477,27 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('create task rename stays user-controlled and resets when the URL changes', (WidgetTester tester) async {
|
||||
await _setTestSize(tester, const Size(700, 500));
|
||||
await tester.pumpWidget(const ProviderScope(child: _CreateTaskPageHarness()));
|
||||
await tester.pump();
|
||||
|
||||
final urlInput = find.byKey(const ValueKey('create-task-url-input'));
|
||||
final renameInput = find.byKey(const ValueKey('create-task-rename-input'));
|
||||
await tester.enterText(urlInput, 'https://example.com/first.zip');
|
||||
await tester.enterText(renameInput, 'custom-name.zip');
|
||||
expect(find.text('custom-name.zip'), findsOneWidget);
|
||||
|
||||
await tester.enterText(urlInput, 'https://example.com/second.zip');
|
||||
await tester.pump();
|
||||
|
||||
final renameField = tester.widget<AppTextField>(
|
||||
find.descendant(of: renameInput, matching: find.byType(AppTextField)),
|
||||
);
|
||||
expect(renameField.controller!.text, isEmpty);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
});
|
||||
|
||||
testWidgets('create task labels direct download without a mode field', (WidgetTester tester) async {
|
||||
await _setTestSize(tester, const Size(700, 500));
|
||||
await tester.pumpWidget(const ProviderScope(child: _CreateTaskPageHarness()));
|
||||
@@ -2591,6 +2685,7 @@ void main() {
|
||||
expect(treeFinder, findsOneWidget);
|
||||
final tree = tester.widget<VirtualTreeView<dynamic>>(treeFinder);
|
||||
expect(tree.branchLine, same(shad.BranchLine.path));
|
||||
expect(tree.rowHeight, 36);
|
||||
expect(
|
||||
tester.widgetList<shad.Checkbox>(find.byType(shad.Checkbox)).every((checkbox) => checkbox.size == null),
|
||||
isTrue,
|
||||
@@ -4819,7 +4914,7 @@ class _CreateTaskPageHarness extends StatelessWidget {
|
||||
return shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const CreateTaskWindowPage(),
|
||||
home: const AppComponentThemes(child: CreateTaskWindowPage()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user