diff --git a/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart b/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart index 889cd482..bce0c31b 100644 --- a/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart +++ b/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart @@ -29,9 +29,9 @@ import '../../../../shared/theme/app_design_tokens.dart'; import '../../../../shared/theme/app_palette.dart'; import '../../../../shared/widgets/app_choice_segmented_control.dart'; import '../../../../shared/widgets/app_http_headers_editor.dart'; +import '../../../../shared/widgets/app_loading_button.dart'; import '../../../../shared/widgets/app_number_input.dart'; import '../../../../shared/widgets/app_path_picker_field.dart'; -import '../../../../shared/widgets/app_primary_button.dart'; import '../../../../shared/widgets/app_text_field.dart'; import '../../../../shared/widgets/app_tooltip.dart'; import '../../../../shared/widgets/app_toast.dart'; @@ -87,6 +87,7 @@ class _CreateTaskWindowPageState extends ConsumerState { String _configuredDownloadDirectory = ''; String _fileDataUri = ''; bool _programmaticUrlChange = false; + String _lastUrlText = ''; @override void initState() { @@ -162,10 +163,15 @@ class _CreateTaskWindowPageState extends ConsumerState { } void _handleUrlChanged() { - if (!_programmaticUrlChange && _fileDataUri.isNotEmpty) { - _fileDataUri = ''; + final urlText = _urlController.text; + if (urlText != _lastUrlText) { + _lastUrlText = urlText; + _renameController.clear(); + if (!_programmaticUrlChange && _fileDataUri.isNotEmpty) { + _fileDataUri = ''; + } } - _recognizeMagnetUri(_urlController.text.trim()); + _recognizeMagnetUri(urlText.trim()); } void _applyInitialTask(CreateTask? task) { @@ -203,9 +209,6 @@ class _CreateTaskWindowPageState extends ConsumerState { } } if (opts != null) { - if (opts.name.isNotEmpty) { - _renameController.text = opts.name; - } if (opts.path.isNotEmpty) { _directoryController.text = opts.path; } @@ -307,6 +310,7 @@ class _CreateTaskWindowPageState extends ConsumerState { SizedBox( height: 120, child: AppTextField( + key: const ValueKey('create-task-url-input'), controller: _urlController, hintText: context.l10n.pasteDownloadLinks, keyboardType: TextInputType.multiline, @@ -689,16 +693,12 @@ class _CreateTaskWindowPageState extends ConsumerState { child: SizedBox(width: 68, child: Center(child: Text(context.l10n.cancel))), ), const SizedBox(width: 12), - AppPrimaryButton( - onPressed: _creating ? null : _confirm, - child: SizedBox( - width: 68, - child: Center( - child: _creating - ? const SizedBox.square(dimension: 14, child: CircularProgressIndicator()) - : Text(context.l10n.confirm), - ), - ), + AppLoadingButton( + key: const ValueKey('create-task-confirm-button'), + onPressed: _confirm, + loading: _creating, + variant: AppLoadingButtonVariant.primary, + child: SizedBox(width: 68, child: Center(child: Text(context.l10n.confirm))), ), ], ), @@ -858,7 +858,6 @@ class _CreateTaskWindowPageState extends ConsumerState { final options = _buildOptions(); final result = await ref.read(gopeedServiceProvider).resolve(ResolveTask(req: request, opts: options)); if (!mounted) return; - _syncResolvedName(result); final created = await _showResolveDialog(request, result); if (!created) { if (mounted) { @@ -1147,16 +1146,12 @@ class _CreateTaskWindowPageState extends ConsumerState { onPressed: submitting ? null : () => closeOverlay(dialogContext, false), child: SizedBox(width: 68, child: Center(child: Text(dialogContext.l10n.cancel))), ), - AppPrimaryButton( - onPressed: submitting ? null : submit, - child: SizedBox( - width: 68, - child: Center( - child: submitting - ? const SizedBox.square(dimension: 14, child: CircularProgressIndicator()) - : Text(dialogContext.l10n.create), - ), - ), + AppLoadingButton( + key: const ValueKey('resolve-create-button'), + onPressed: submit, + loading: submitting, + variant: AppLoadingButtonVariant.primary, + child: Text(dialogContext.l10n.createAction, maxLines: 1, softWrap: false), ), ], ); @@ -1318,12 +1313,6 @@ class _CreateTaskWindowPageState extends ConsumerState { filterController.dispose(); } - void _syncResolvedName(ResolveResult result) { - if (_renameController.text.trim().isEmpty && result.res.name.trim().isNotEmpty) { - _renameController.text = result.res.name; - } - } - void _setUrlText(String value, {String fileDataUri = ''}) { _programmaticUrlChange = true; _urlController.text = value; diff --git a/ui/flutter/lib/features/tasks/presentation/widgets/resolve_file_tree.dart b/ui/flutter/lib/features/tasks/presentation/widgets/resolve_file_tree.dart index 60e88399..d5f8af8e 100644 --- a/ui/flutter/lib/features/tasks/presentation/widgets/resolve_file_tree.dart +++ b/ui/flutter/lib/features/tasks/presentation/widgets/resolve_file_tree.dart @@ -61,6 +61,8 @@ class _ResolveFileTreeState extends State { child: FileTreeView( items: _treeItems, keyPrefix: 'resolve-tree', + rowHeight: _resolveTreeRowHeight, + contentTextStyle: _resolveFileNameStyle, headerLeading: ExcludeFocus( child: Checkbox( state: allSelected @@ -336,3 +338,5 @@ extension on _FileTypeFilter { const _videoExts = {'mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v'}; const _audioExts = {'mp3', 'flac', 'wav', 'aac', 'm4a', 'ogg', 'ape'}; const _imageExts = {'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'}; +const _resolveTreeRowHeight = 36.0; +const _resolveFileNameStyle = TextStyle(fontSize: 14, height: 1.25); diff --git a/ui/flutter/lib/l10n/app_ar.arb b/ui/flutter/lib/l10n/app_ar.arb index 3569cc64..830d6eb1 100644 --- a/ui/flutter/lib/l10n/app_ar.arb +++ b/ui/flutter/lib/l10n/app_ar.arb @@ -18,6 +18,7 @@ "exit": "خروج", "pressBackAgainToExit": "اضغط زر الرجوع مرة أخرى للخروج", "create": "إنشاء مهمة", + "createAction": "إنشاء", "directDownload": "تنزيل مباشر", "advancedOptions": "خيارات متقدمة", "followSettings": "اتباع الإعدادات", diff --git a/ui/flutter/lib/l10n/app_ca.arb b/ui/flutter/lib/l10n/app_ca.arb index 7743b429..1daea2e6 100644 --- a/ui/flutter/lib/l10n/app_ca.arb +++ b/ui/flutter/lib/l10n/app_ca.arb @@ -18,6 +18,7 @@ "exit": "Surt", "pressBackAgainToExit": "Torna a prémer enrere per sortir", "create": "Crea una tasca", + "createAction": "Crea", "directDownload": "Baixada directa", "advancedOptions": "Opcions avançades", "followSettings": "Segueix la configuració", diff --git a/ui/flutter/lib/l10n/app_de.arb b/ui/flutter/lib/l10n/app_de.arb index 2a347170..fdd924ff 100644 --- a/ui/flutter/lib/l10n/app_de.arb +++ b/ui/flutter/lib/l10n/app_de.arb @@ -18,6 +18,7 @@ "exit": "Beenden", "pressBackAgainToExit": "Drücken Sie erneut Zurück, um die App zu beenden", "create": "Aufgabe Erstellen", + "createAction": "Erstellen", "directDownload": "Direkter Download", "advancedOptions": "Erweiterte Einstellungen", "followSettings": "Einstellungen Folgen", diff --git a/ui/flutter/lib/l10n/app_en.arb b/ui/flutter/lib/l10n/app_en.arb index 578e762d..1a0fcfa2 100644 --- a/ui/flutter/lib/l10n/app_en.arb +++ b/ui/flutter/lib/l10n/app_en.arb @@ -18,6 +18,7 @@ "exit": "Exit", "pressBackAgainToExit": "Press back again to exit", "create": "Create Task", + "createAction": "Create", "directDownload": "Direct Download", "advancedOptions": "Advanced Options", "followSettings": "Follow Settings", diff --git a/ui/flutter/lib/l10n/app_es.arb b/ui/flutter/lib/l10n/app_es.arb index 6aef1468..c93209d1 100644 --- a/ui/flutter/lib/l10n/app_es.arb +++ b/ui/flutter/lib/l10n/app_es.arb @@ -18,6 +18,7 @@ "exit": "Salir", "pressBackAgainToExit": "Pulsa Atrás de nuevo para salir", "create": "Crear Tarea", + "createAction": "Crear", "directDownload": "Descarga Directa", "advancedOptions": "Opciones Avanzadas", "followSettings": "Siga las opciones", diff --git a/ui/flutter/lib/l10n/app_fa.arb b/ui/flutter/lib/l10n/app_fa.arb index 7398b5c3..3943a61d 100644 --- a/ui/flutter/lib/l10n/app_fa.arb +++ b/ui/flutter/lib/l10n/app_fa.arb @@ -18,6 +18,7 @@ "exit": "خروج", "pressBackAgainToExit": "برای خروج دوباره دکمه بازگشت را فشار دهید", "create": "ایجاد کار", + "createAction": "ایجاد", "directDownload": "دانلود مستقیم", "advancedOptions": "گزینه های پیشرفته", "followSettings": "تنظیمات را دنبال کنید", diff --git a/ui/flutter/lib/l10n/app_fr.arb b/ui/flutter/lib/l10n/app_fr.arb index 43404fdc..4af83656 100644 --- a/ui/flutter/lib/l10n/app_fr.arb +++ b/ui/flutter/lib/l10n/app_fr.arb @@ -18,6 +18,7 @@ "exit": "Quitter", "pressBackAgainToExit": "Appuyez à nouveau sur Retour pour quitter", "create": "Créer une tâche", + "createAction": "Créer", "directDownload": "Téléchargement direct", "advancedOptions": "Options avancées", "followSettings": "Suivre les paramètres", diff --git a/ui/flutter/lib/l10n/app_hu.arb b/ui/flutter/lib/l10n/app_hu.arb index cd5d2b2d..9c2fd4c8 100644 --- a/ui/flutter/lib/l10n/app_hu.arb +++ b/ui/flutter/lib/l10n/app_hu.arb @@ -18,6 +18,7 @@ "exit": "Kilépés", "pressBackAgainToExit": "Nyomja meg újra a Vissza gombot a kilépéshez", "create": "Feladat létrehozása", + "createAction": "Létrehozás", "directDownload": "Közvetlen letöltés", "advancedOptions": "Haladó beállítások", "followSettings": "Beállítások követése", diff --git a/ui/flutter/lib/l10n/app_id.arb b/ui/flutter/lib/l10n/app_id.arb index 1f87a970..857ec774 100644 --- a/ui/flutter/lib/l10n/app_id.arb +++ b/ui/flutter/lib/l10n/app_id.arb @@ -18,6 +18,7 @@ "exit": "Keluar", "pressBackAgainToExit": "Tekan kembali sekali lagi untuk keluar", "create": "Membuat Tugas", + "createAction": "Buat", "directDownload": "Unduhan Langsung", "advancedOptions": "Pengaturan Lanjutan", "followSettings": "Mengikuti Pengaturan", diff --git a/ui/flutter/lib/l10n/app_it.arb b/ui/flutter/lib/l10n/app_it.arb index 7d298adb..01263794 100644 --- a/ui/flutter/lib/l10n/app_it.arb +++ b/ui/flutter/lib/l10n/app_it.arb @@ -18,6 +18,7 @@ "exit": "Uscita", "pressBackAgainToExit": "Premi di nuovo Indietro per uscire", "create": "Crea attività", + "createAction": "Crea", "directDownload": "Scaricamento diretto", "advancedOptions": "Opzioni avanzate", "followSettings": "Seguire le impostazioni", diff --git a/ui/flutter/lib/l10n/app_ja.arb b/ui/flutter/lib/l10n/app_ja.arb index 0bdbc743..d816b478 100644 --- a/ui/flutter/lib/l10n/app_ja.arb +++ b/ui/flutter/lib/l10n/app_ja.arb @@ -18,6 +18,7 @@ "exit": "終了", "pressBackAgainToExit": "もう一度戻るボタンを押すと終了します", "create": "タスクを作成", + "createAction": "作成", "directDownload": "直接ダウンロード", "advancedOptions": "詳細オプション", "followSettings": "設定に従う", diff --git a/ui/flutter/lib/l10n/app_ko.arb b/ui/flutter/lib/l10n/app_ko.arb index db8caf2d..a4d3f9da 100644 --- a/ui/flutter/lib/l10n/app_ko.arb +++ b/ui/flutter/lib/l10n/app_ko.arb @@ -18,6 +18,7 @@ "exit": "종료", "pressBackAgainToExit": "종료하려면 뒤로 버튼을 한 번 더 누르세요", "create": "작업 생성", + "createAction": "생성", "directDownload": "바로 다운로드", "advancedOptions": "고급 옵션", "followSettings": "설정값 사용", diff --git a/ui/flutter/lib/l10n/app_pl.arb b/ui/flutter/lib/l10n/app_pl.arb index c255d506..6aea60ce 100644 --- a/ui/flutter/lib/l10n/app_pl.arb +++ b/ui/flutter/lib/l10n/app_pl.arb @@ -18,6 +18,7 @@ "exit": "Wyjście", "pressBackAgainToExit": "Naciśnij ponownie Wstecz, aby wyjść", "create": "Utwórz zadanie", + "createAction": "Utwórz", "directDownload": "Pobieranie bezpośrednie", "advancedOptions": "Zaawansowane", "followSettings": "Podążaj za ustawieniami", diff --git a/ui/flutter/lib/l10n/app_pt.arb b/ui/flutter/lib/l10n/app_pt.arb index 8447f830..39b3a81a 100644 --- a/ui/flutter/lib/l10n/app_pt.arb +++ b/ui/flutter/lib/l10n/app_pt.arb @@ -18,6 +18,7 @@ "exit": "Sair", "pressBackAgainToExit": "Prima novamente Voltar para sair", "create": "Criar Tarefa", + "createAction": "Criar", "directDownload": "Download Direto", "advancedOptions": "Opções Avançadas", "followSettings": "Seguir as Configurações", diff --git a/ui/flutter/lib/l10n/app_ru.arb b/ui/flutter/lib/l10n/app_ru.arb index 1cc5fe24..782a5c9e 100644 --- a/ui/flutter/lib/l10n/app_ru.arb +++ b/ui/flutter/lib/l10n/app_ru.arb @@ -18,6 +18,7 @@ "exit": "Выход", "pressBackAgainToExit": "Нажмите «Назад» ещё раз, чтобы выйти", "create": "Создать задачу", + "createAction": "Создать", "directDownload": "Прямая загрузка", "advancedOptions": "Расширенные опции", "followSettings": "По умолчанию", diff --git a/ui/flutter/lib/l10n/app_ta.arb b/ui/flutter/lib/l10n/app_ta.arb index 82870d83..00d06c0c 100644 --- a/ui/flutter/lib/l10n/app_ta.arb +++ b/ui/flutter/lib/l10n/app_ta.arb @@ -18,6 +18,7 @@ "exit": "வெளியேறு", "pressBackAgainToExit": "வெளியேற மீண்டும் பின்செல் பொத்தானை அழுத்தவும்", "create": "பணியை உருவாக்கு", + "createAction": "உருவாக்கு", "directDownload": "நேரடி பதிவிறக்கம்", "advancedOptions": "மேம்பட்ட விருப்பங்கள்", "followSettings": "அமைப்புகளைப் பின்தொடரவும்", diff --git a/ui/flutter/lib/l10n/app_tr.arb b/ui/flutter/lib/l10n/app_tr.arb index ca979cae..a753215d 100644 --- a/ui/flutter/lib/l10n/app_tr.arb +++ b/ui/flutter/lib/l10n/app_tr.arb @@ -18,6 +18,7 @@ "exit": "Çıkış", "pressBackAgainToExit": "Çıkmak için tekrar geri tuşuna basın", "create": "İşlem Oluştur", + "createAction": "Oluştur", "directDownload": "Direkt İndir", "advancedOptions": "Gelişmiş Seçenekler", "followSettings": "Ayarları Takip Et", diff --git a/ui/flutter/lib/l10n/app_uk.arb b/ui/flutter/lib/l10n/app_uk.arb index ceae4052..82c6cc34 100644 --- a/ui/flutter/lib/l10n/app_uk.arb +++ b/ui/flutter/lib/l10n/app_uk.arb @@ -18,6 +18,7 @@ "exit": "Вихід", "pressBackAgainToExit": "Натисніть «Назад» ще раз, щоб вийти", "create": "Створити завдання", + "createAction": "Створити", "directDownload": "Пряме завантаження", "advancedOptions": "Додаткові параметри", "followSettings": "За замовчуванням", diff --git a/ui/flutter/lib/l10n/app_vi.arb b/ui/flutter/lib/l10n/app_vi.arb index 6ed35bd1..ccd43cb7 100644 --- a/ui/flutter/lib/l10n/app_vi.arb +++ b/ui/flutter/lib/l10n/app_vi.arb @@ -18,6 +18,7 @@ "exit": "Thoát", "pressBackAgainToExit": "Nhấn quay lại lần nữa để thoát", "create": "Tạo nhiệm vụ", + "createAction": "Tạo", "directDownload": "Tải trực tiếp", "advancedOptions": "Tùy chọn nâng cao", "followSettings": "Theo thiết lập", diff --git a/ui/flutter/lib/l10n/app_zh.arb b/ui/flutter/lib/l10n/app_zh.arb index 96e8a184..ddc6e16d 100644 --- a/ui/flutter/lib/l10n/app_zh.arb +++ b/ui/flutter/lib/l10n/app_zh.arb @@ -18,6 +18,7 @@ "exit": "退出", "pressBackAgainToExit": "再按一次返回键退出应用", "create": "创建任务", + "createAction": "创建", "directDownload": "直接下载", "advancedOptions": "高级选项", "followSettings": "跟随设置", diff --git a/ui/flutter/lib/l10n/app_zh_TW.arb b/ui/flutter/lib/l10n/app_zh_TW.arb index c75a84c7..db5bb6a2 100644 --- a/ui/flutter/lib/l10n/app_zh_TW.arb +++ b/ui/flutter/lib/l10n/app_zh_TW.arb @@ -18,6 +18,7 @@ "exit": "退出", "pressBackAgainToExit": "再按一次返回鍵退出應用程式", "create": "建立任務", + "createAction": "建立", "directDownload": "直接下載", "advancedOptions": "進階選項", "followSettings": "跟隨設定", diff --git a/ui/flutter/lib/shared/widgets/app_loading_button.dart b/ui/flutter/lib/shared/widgets/app_loading_button.dart index b203ed33..f886204a 100644 --- a/ui/flutter/lib/shared/widgets/app_loading_button.dart +++ b/ui/flutter/lib/shared/widgets/app_loading_button.dart @@ -30,7 +30,15 @@ class AppLoadingButton extends StatelessWidget { final palette = AppPalette.of(context); final replaceChild = loading && icon == null; final leading = loading && icon != null ? _LoadingIcon(variant: variant) : icon; - final effectiveChild = replaceChild ? _LoadingIcon(variant: variant) : child; + final effectiveChild = replaceChild + ? Stack( + alignment: Alignment.center, + children: [ + ExcludeSemantics(child: Opacity(opacity: 0, child: child)), + Positioned.fill(child: _LoadingIcon(variant: variant)), + ], + ) + : child; final content = leading == null ? effectiveChild : Row( diff --git a/ui/flutter/test/features/tasks/presentation/create_task_window_page_test.dart b/ui/flutter/test/features/tasks/presentation/create_task_window_page_test.dart index c0eeae56..59254019 100644 --- a/ui/flutter/test/features/tasks/presentation/create_task_window_page_test.dart +++ b/ui/flutter/test/features/tasks/presentation/create_task_window_page_test.dart @@ -9,6 +9,8 @@ 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/capability_rpc.dart'; import 'package:gopeed/core/capabilities/gopeed_capability.dart'; @@ -18,6 +20,7 @@ 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; @@ -312,6 +315,56 @@ void main() { expect(options.archivePassword, 'archive-password'); expect(options.deleteAfterExtract, isTrue); expect(submitted?.opts?.asDefaultPath, isTrue); + expect(submitted?.opts?.name, isEmpty); + }); + + testWidgets('resolved task keeps rename empty and uses a single-line intrinsic create action', ( + 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(find.byKey(const ValueKey('resolve-create-button'))); + expect(createButton.child, isA()); + final createText = createButton.child as Text; + expect(createText.data, 'Create'); + expect(createText.maxLines, 1); + expect(createText.softWrap, isFalse); + expect(tester.takeException(), isNull); }); testWidgets('create history supports filtering, deleting one entry, and clearing all entries', ( diff --git a/ui/flutter/test/shared/widgets/app_loading_button_test.dart b/ui/flutter/test/shared/widgets/app_loading_button_test.dart index 1726cfd6..8d3f1ded 100644 --- a/ui/flutter/test/shared/widgets/app_loading_button_test.dart +++ b/ui/flutter/test/shared/widgets/app_loading_button_test.dart @@ -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(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); }); diff --git a/ui/flutter/test/widget_test.dart b/ui/flutter/test/widget_test.dart index d7fddcbe..f11f9e50 100644 --- a/ui/flutter/test/widget_test.dart +++ b/ui/flutter/test/widget_test.dart @@ -1860,6 +1860,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(); @@ -2404,6 +2438,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( + 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 +2646,7 @@ void main() { expect(treeFinder, findsOneWidget); final tree = tester.widget>(treeFinder); expect(tree.branchLine, same(shad.BranchLine.path)); + expect(tree.rowHeight, 36); expect( tester.widgetList(find.byType(shad.Checkbox)).every((checkbox) => checkbox.size == null), isTrue,