From 2e8672273c270f5e2508855645217de0266a7c60 Mon Sep 17 00:00:00 2001 From: Levi Date: Sun, 13 Sep 2026 17:25:31 +0800 Subject: [PATCH] feat: confirm extension updates with progress and retry --- .../presentation/pages/extensions_page.dart | 7 +- .../widgets/extension_detail_view.dart | 9 +- .../widgets/extension_update_dialog.dart | 90 +++++++++++++++++++ ui/flutter/lib/l10n/app_ar.arb | 1 + ui/flutter/lib/l10n/app_az.arb | 1 + ui/flutter/lib/l10n/app_ca.arb | 1 + ui/flutter/lib/l10n/app_de.arb | 1 + ui/flutter/lib/l10n/app_en.arb | 8 ++ ui/flutter/lib/l10n/app_es.arb | 1 + ui/flutter/lib/l10n/app_fa.arb | 1 + ui/flutter/lib/l10n/app_fr.arb | 1 + ui/flutter/lib/l10n/app_hu.arb | 1 + ui/flutter/lib/l10n/app_id.arb | 1 + ui/flutter/lib/l10n/app_it.arb | 1 + ui/flutter/lib/l10n/app_ja.arb | 1 + ui/flutter/lib/l10n/app_ko.arb | 1 + ui/flutter/lib/l10n/app_pl.arb | 1 + ui/flutter/lib/l10n/app_pt.arb | 1 + ui/flutter/lib/l10n/app_ru.arb | 1 + ui/flutter/lib/l10n/app_ta.arb | 1 + ui/flutter/lib/l10n/app_tr.arb | 1 + ui/flutter/lib/l10n/app_uk.arb | 1 + ui/flutter/lib/l10n/app_vi.arb | 1 + ui/flutter/lib/l10n/app_zh.arb | 1 + ui/flutter/lib/l10n/app_zh_TW.arb | 1 + ui/flutter/test/widget_test.dart | 84 +++++++++++++++++ 26 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 ui/flutter/lib/features/extensions/presentation/widgets/extension_update_dialog.dart diff --git a/ui/flutter/lib/features/extensions/presentation/pages/extensions_page.dart b/ui/flutter/lib/features/extensions/presentation/pages/extensions_page.dart index ef3d076d..48183df1 100644 --- a/ui/flutter/lib/features/extensions/presentation/pages/extensions_page.dart +++ b/ui/flutter/lib/features/extensions/presentation/pages/extensions_page.dart @@ -24,6 +24,7 @@ import '../../application/extensions_controller.dart'; import '../../application/pending_extension_install.dart'; import '../widgets/extension_detail_view.dart'; import '../widgets/extension_icon.dart'; +import '../widgets/extension_update_dialog.dart'; const _extensionCardMinWidth = 280.0; const _extensionGridSpacing = 10.0; @@ -1034,10 +1035,8 @@ class _ExtensionCard extends ConsumerWidget { if (canUpdate && installed != null) shad.GhostButton( density: shad.ButtonDensity.icon, - onPressed: busy - ? null - : () => - onAction(() => ref.read(extensionsControllerProvider.notifier).upgradeExtension(installed)), + key: ValueKey('update-extension-${installed.identity}'), + onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed), child: const Icon(Icons.refresh), ), if (installed == null && item.store != null) diff --git a/ui/flutter/lib/features/extensions/presentation/widgets/extension_detail_view.dart b/ui/flutter/lib/features/extensions/presentation/widgets/extension_detail_view.dart index 886be8ef..dc694b1a 100644 --- a/ui/flutter/lib/features/extensions/presentation/widgets/extension_detail_view.dart +++ b/ui/flutter/lib/features/extensions/presentation/widgets/extension_detail_view.dart @@ -19,6 +19,7 @@ import '../../../../shared/widgets/detail/app_detail_surface.dart'; import '../../../../util/util.dart'; import '../../application/extensions_controller.dart'; import 'extension_icon.dart'; +import 'extension_update_dialog.dart'; class ExtensionDetailDrawer extends StatelessWidget { const ExtensionDetailDrawer({super.key, required this.item, required this.onClose}); @@ -131,14 +132,10 @@ class ExtensionDetailView extends ConsumerWidget { if (installed != null && canUpdate) AppLoadingButton( key: const ValueKey('extension-details-update'), - loading: busy, + loading: false, variant: AppLoadingButtonVariant.primary, icon: const Icon(Icons.refresh, size: 16), - onPressed: () => _runAction( - context, - () => ref.read(extensionsControllerProvider.notifier).upgradeExtension(installed), - successMessage: context.l10n.extensionUpdateSuccess, - ), + onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed), child: Text(context.l10n.newVersionUpdate), ), if (installed != null && !canUpdate) diff --git a/ui/flutter/lib/features/extensions/presentation/widgets/extension_update_dialog.dart b/ui/flutter/lib/features/extensions/presentation/widgets/extension_update_dialog.dart new file mode 100644 index 00000000..107a46bc --- /dev/null +++ b/ui/flutter/lib/features/extensions/presentation/widgets/extension_update_dialog.dart @@ -0,0 +1,90 @@ +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../api/model/extension.dart' as api; +import '../../../../l10n/l10n.dart'; +import '../../../../shared/widgets/app_loading_button.dart'; +import '../../../../shared/widgets/app_toast.dart'; +import '../../application/extensions_controller.dart'; + +Future showExtensionUpdateDialog(BuildContext context, api.Extension extension) async { + final overlay = const DialogOverlayHandler().show( + context: context, + alignment: Alignment.center, + barrierDismissable: false, + builder: (_) => _ExtensionUpdateDialog(extension: extension), + ); + final updated = await overlay.future; + if (updated == true && context.mounted) { + showAppToast(context, context.l10n.extensionUpdateSuccess); + } +} + +class _ExtensionUpdateDialog extends ConsumerStatefulWidget { + const _ExtensionUpdateDialog({required this.extension}); + + final api.Extension extension; + + @override + ConsumerState<_ExtensionUpdateDialog> createState() => _ExtensionUpdateDialogState(); +} + +class _ExtensionUpdateDialogState extends ConsumerState<_ExtensionUpdateDialog> { + bool _updating = false; + bool _failed = false; + + Future _update() async { + if (_updating) return; + setState(() { + _updating = true; + _failed = false; + }); + try { + await ref.read(extensionsControllerProvider.notifier).upgradeExtension(widget.extension); + } catch (_) { + if (!mounted) return; + setState(() { + _updating = false; + _failed = true; + }); + return; + } + if (mounted) await closeOverlay(context, true); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: !_updating, + child: AlertDialog( + key: const ValueKey('extension-update-dialog'), + title: Text(context.l10n.extensionCanUpdate), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.extensionUpdateConfirm(widget.extension.title)), + if (_failed) ...[ + const SizedBox(height: 12), + Text(context.l10n.updateFailed, key: const ValueKey('extension-update-error')), + ], + ], + ), + actions: [ + SecondaryButton( + key: const ValueKey('cancel-update-extension-button'), + onPressed: _updating ? null : () => closeOverlay(context, false), + child: Text(context.l10n.cancel), + ), + AppLoadingButton( + key: const ValueKey('confirm-update-extension-button'), + variant: AppLoadingButtonVariant.primary, + loading: _updating, + onPressed: _update, + child: Text(context.l10n.newVersionUpdate), + ), + ], + ), + ); + } +} diff --git a/ui/flutter/lib/l10n/app_ar.arb b/ui/flutter/lib/l10n/app_ar.arb index 174cb2d1..372a5777 100644 --- a/ui/flutter/lib/l10n/app_ar.arb +++ b/ui/flutter/lib/l10n/app_ar.arb @@ -103,6 +103,7 @@ "extensions": "الإضافات", "extensionInstallUrl": "عنوان التثبيت", "extensionInstallSuccess": "تم التثبيت بنجاح", + "extensionUpdateConfirm": "هل تريد تحديث «{name}» إلى أحدث إصدار؟", "extensionUpdateSuccess": "تم التحديث بنجاح", "extensionDelete": "حذف الإضافة", "extensionAlreadyLatest": "هذا هو أحدث إصدار بالفعل", diff --git a/ui/flutter/lib/l10n/app_az.arb b/ui/flutter/lib/l10n/app_az.arb index 4e9ba6ec..564f9999 100644 --- a/ui/flutter/lib/l10n/app_az.arb +++ b/ui/flutter/lib/l10n/app_az.arb @@ -110,6 +110,7 @@ "extensions": "Uzantılar", "extensionInstallUrl": "Quraşdırma URL-i", "extensionInstallSuccess": "Uğurla quraşdırıldı", + "extensionUpdateConfirm": "“{name}” ən son versiyaya yenilənsin?", "extensionUpdateSuccess": "Uğurla yeniləndi", "extensionDelete": "Uzantını Sil", "extensionAlreadyLatest": "Artıq son versiyadır", diff --git a/ui/flutter/lib/l10n/app_ca.arb b/ui/flutter/lib/l10n/app_ca.arb index dc3f81f7..bee2f371 100644 --- a/ui/flutter/lib/l10n/app_ca.arb +++ b/ui/flutter/lib/l10n/app_ca.arb @@ -103,6 +103,7 @@ "extensions": "Extensions", "extensionInstallUrl": "URL d'instal·lació", "extensionInstallSuccess": "S'ha instal·lat amb èxit", + "extensionUpdateConfirm": "Vols actualitzar «{name}» a la versió més recent?", "extensionUpdateSuccess": "S'ha actualitzat amb èxit", "extensionDelete": "Suprimeix l'extensió", "extensionAlreadyLatest": "Ja està actualitzada", diff --git a/ui/flutter/lib/l10n/app_de.arb b/ui/flutter/lib/l10n/app_de.arb index a27d800c..5c95653f 100644 --- a/ui/flutter/lib/l10n/app_de.arb +++ b/ui/flutter/lib/l10n/app_de.arb @@ -103,6 +103,7 @@ "extensions": "Erweiterungen", "extensionInstallUrl": "Installations-URL", "extensionInstallSuccess": "Erfolgreich installiert", + "extensionUpdateConfirm": "„{name}“ auf die neueste Version aktualisieren?", "extensionUpdateSuccess": "Erfolgreich aktualisiert", "extensionDelete": "Erweiterung löschen", "extensionAlreadyLatest": "Es ist bereits die neueste Version", diff --git a/ui/flutter/lib/l10n/app_en.arb b/ui/flutter/lib/l10n/app_en.arb index d1897ce8..c483375b 100644 --- a/ui/flutter/lib/l10n/app_en.arb +++ b/ui/flutter/lib/l10n/app_en.arb @@ -110,6 +110,14 @@ "extensions": "Extensions", "extensionInstallUrl": "Install URL", "extensionInstallSuccess": "Installed successfully", + "extensionUpdateConfirm": "Update “{name}” to the latest version?", + "@extensionUpdateConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "extensionUpdateSuccess": "Updated successfully", "extensionDelete": "Delete Extension", "extensionAlreadyLatest": "It's already the latest version", diff --git a/ui/flutter/lib/l10n/app_es.arb b/ui/flutter/lib/l10n/app_es.arb index 34d2343c..a9588f9a 100644 --- a/ui/flutter/lib/l10n/app_es.arb +++ b/ui/flutter/lib/l10n/app_es.arb @@ -103,6 +103,7 @@ "extensions": "Extensiones", "extensionInstallUrl": "URL de instalación", "extensionInstallSuccess": "Instalado con éxito", + "extensionUpdateConfirm": "¿Actualizar «{name}» a la última versión?", "extensionUpdateSuccess": "Actualizado con éxito", "extensionDelete": "Eliminar Extensión", "extensionAlreadyLatest": "Ya es la versión más reciente", diff --git a/ui/flutter/lib/l10n/app_fa.arb b/ui/flutter/lib/l10n/app_fa.arb index bafb1bb3..66235220 100644 --- a/ui/flutter/lib/l10n/app_fa.arb +++ b/ui/flutter/lib/l10n/app_fa.arb @@ -103,6 +103,7 @@ "extensions": "افزونه‌ها", "extensionInstallUrl": "نشانی نصب", "extensionInstallSuccess": "نصب شده با موفقیت", + "extensionUpdateConfirm": "«{name}» به آخرین نسخه به‌روزرسانی شود؟", "extensionUpdateSuccess": "به روز رسانی موفقیت آمیز", "extensionDelete": "حذف افزونه", "extensionAlreadyLatest": "این نسخه در حال حاضر آخرین نسخه است", diff --git a/ui/flutter/lib/l10n/app_fr.arb b/ui/flutter/lib/l10n/app_fr.arb index e708aca4..de714190 100644 --- a/ui/flutter/lib/l10n/app_fr.arb +++ b/ui/flutter/lib/l10n/app_fr.arb @@ -103,6 +103,7 @@ "extensions": "Extensions", "extensionInstallUrl": "URL d'installation", "extensionInstallSuccess": "Installé avec succès", + "extensionUpdateConfirm": "Mettre à jour « {name} » vers la dernière version ?", "extensionUpdateSuccess": "Mis à jour avec succès", "extensionDelete": "Supprimer l'extension", "extensionAlreadyLatest": "C'est déjà la dernière version", diff --git a/ui/flutter/lib/l10n/app_hu.arb b/ui/flutter/lib/l10n/app_hu.arb index 2823ae6d..7b24e524 100644 --- a/ui/flutter/lib/l10n/app_hu.arb +++ b/ui/flutter/lib/l10n/app_hu.arb @@ -103,6 +103,7 @@ "extensions": "Bővítmények", "extensionInstallUrl": "Telepítési URL", "extensionInstallSuccess": "Sikeresen telepítve", + "extensionUpdateConfirm": "Frissíti a(z) „{name}” bővítményt a legújabb verzióra?", "extensionUpdateSuccess": "Sikeresen frissítve", "extensionDelete": "Bővítmény törlése", "extensionAlreadyLatest": "Ez már a legfrissebb verzió", diff --git a/ui/flutter/lib/l10n/app_id.arb b/ui/flutter/lib/l10n/app_id.arb index 43a57bdc..4f883708 100644 --- a/ui/flutter/lib/l10n/app_id.arb +++ b/ui/flutter/lib/l10n/app_id.arb @@ -103,6 +103,7 @@ "extensions": "Ekstensi", "extensionInstallUrl": "URL Instal", "extensionInstallSuccess": "Diinstal dengan sukses", + "extensionUpdateConfirm": "Perbarui “{name}” ke versi terbaru?", "extensionUpdateSuccess": "Diperbaruhi dengan sukses", "extensionDelete": "Hapus Ekstensi", "extensionAlreadyLatest": "Ini sudah versi terbaru", diff --git a/ui/flutter/lib/l10n/app_it.arb b/ui/flutter/lib/l10n/app_it.arb index 8d0f62b9..168e2fd6 100644 --- a/ui/flutter/lib/l10n/app_it.arb +++ b/ui/flutter/lib/l10n/app_it.arb @@ -103,6 +103,7 @@ "extensions": "Estensioni", "extensionInstallUrl": "Installa URL", "extensionInstallSuccess": "Installato con successo", + "extensionUpdateConfirm": "Aggiornare «{name}» alla versione più recente?", "extensionUpdateSuccess": "Aggiornato con successo", "extensionDelete": "Elimina estensione", "extensionAlreadyLatest": "È già l'ultima versione", diff --git a/ui/flutter/lib/l10n/app_ja.arb b/ui/flutter/lib/l10n/app_ja.arb index 097488ef..98ea7b15 100644 --- a/ui/flutter/lib/l10n/app_ja.arb +++ b/ui/flutter/lib/l10n/app_ja.arb @@ -103,6 +103,7 @@ "extensions": "拡張機能", "extensionInstallUrl": "インストールURL", "extensionInstallSuccess": "正常にインストールされました", + "extensionUpdateConfirm": "「{name}」を最新バージョンに更新しますか?", "extensionUpdateSuccess": "正常に更新されました", "extensionDelete": "拡張機能を削除", "extensionAlreadyLatest": "既に最新バージョンです", diff --git a/ui/flutter/lib/l10n/app_ko.arb b/ui/flutter/lib/l10n/app_ko.arb index b7c66da1..28d64a32 100644 --- a/ui/flutter/lib/l10n/app_ko.arb +++ b/ui/flutter/lib/l10n/app_ko.arb @@ -103,6 +103,7 @@ "extensions": "확장 프로그램", "extensionInstallUrl": "설치 URL", "extensionInstallSuccess": "설치되었습니다", + "extensionUpdateConfirm": "“{name}”을(를) 최신 버전으로 업데이트할까요?", "extensionUpdateSuccess": "업데이트되었습니다", "extensionDelete": "확장 프로그램 삭제", "extensionAlreadyLatest": "이미 최신 버전입니다", diff --git a/ui/flutter/lib/l10n/app_pl.arb b/ui/flutter/lib/l10n/app_pl.arb index ff542d50..6256937f 100644 --- a/ui/flutter/lib/l10n/app_pl.arb +++ b/ui/flutter/lib/l10n/app_pl.arb @@ -103,6 +103,7 @@ "extensions": "Wtyczki", "extensionInstallUrl": "Instaluj z url", "extensionInstallSuccess": "Instalacja zakończona", + "extensionUpdateConfirm": "Zaktualizować „{name}” do najnowszej wersji?", "extensionUpdateSuccess": "Aktualizacja zakończona", "extensionDelete": "Usuń wtyczkę", "extensionAlreadyLatest": "Wtyczka w aktualnej wersji", diff --git a/ui/flutter/lib/l10n/app_pt.arb b/ui/flutter/lib/l10n/app_pt.arb index c0d66de9..eca4a034 100644 --- a/ui/flutter/lib/l10n/app_pt.arb +++ b/ui/flutter/lib/l10n/app_pt.arb @@ -103,6 +103,7 @@ "extensions": "Extensões", "extensionInstallUrl": "Link de Instalação", "extensionInstallSuccess": "Instalado com Sucesso", + "extensionUpdateConfirm": "Atualizar “{name}” para a versão mais recente?", "extensionUpdateSuccess": "Atualizado com Sucesso", "extensionDelete": "Excluir Extensão", "extensionAlreadyLatest": "Já é a última versão", diff --git a/ui/flutter/lib/l10n/app_ru.arb b/ui/flutter/lib/l10n/app_ru.arb index 67e42710..08811c7d 100644 --- a/ui/flutter/lib/l10n/app_ru.arb +++ b/ui/flutter/lib/l10n/app_ru.arb @@ -103,6 +103,7 @@ "extensions": "Расширения", "extensionInstallUrl": "URL для установки", "extensionInstallSuccess": "Установка завершена", + "extensionUpdateConfirm": "Обновить «{name}» до последней версии?", "extensionUpdateSuccess": "Обновление завершено", "extensionDelete": "Удалить расширение", "extensionAlreadyLatest": "Это последняя версия", diff --git a/ui/flutter/lib/l10n/app_ta.arb b/ui/flutter/lib/l10n/app_ta.arb index 51ee4819..a4544b27 100644 --- a/ui/flutter/lib/l10n/app_ta.arb +++ b/ui/flutter/lib/l10n/app_ta.arb @@ -103,6 +103,7 @@ "extensions": "நீட்டிப்புகள்", "extensionInstallUrl": "நிறுவப்படும் URL", "extensionInstallSuccess": "வெற்றிகரமாக நிறுவப்பட்டது", + "extensionUpdateConfirm": "“{name}” ஐ சமீபத்திய பதிப்பிற்குப் புதுப்பிக்கவா?", "extensionUpdateSuccess": "வெற்றிகரமாக புதுப்பிக்கப்பட்டது", "extensionDelete": "நீட்டிப்பை நீக்கு", "extensionAlreadyLatest": "இது ஏற்கனவே சமீபத்திய பதிப்பாகும்", diff --git a/ui/flutter/lib/l10n/app_tr.arb b/ui/flutter/lib/l10n/app_tr.arb index 0b160d9b..e96f138a 100644 --- a/ui/flutter/lib/l10n/app_tr.arb +++ b/ui/flutter/lib/l10n/app_tr.arb @@ -103,6 +103,7 @@ "extensions": "Eklentiler", "extensionInstallUrl": "Kurulum URL'si", "extensionInstallSuccess": "Başarıyla kuruldu", + "extensionUpdateConfirm": "“{name}” en son sürüme güncellensin mi?", "extensionUpdateSuccess": "Başarıyla güncellendi", "extensionDelete": "Eklentiyi Sil", "extensionAlreadyLatest": "Zaten en son sürüm", diff --git a/ui/flutter/lib/l10n/app_uk.arb b/ui/flutter/lib/l10n/app_uk.arb index 8632668f..2c659f04 100644 --- a/ui/flutter/lib/l10n/app_uk.arb +++ b/ui/flutter/lib/l10n/app_uk.arb @@ -103,6 +103,7 @@ "extensions": "Розширення", "extensionInstallUrl": "URL для встановлення", "extensionInstallSuccess": "Встановлено успішно", + "extensionUpdateConfirm": "Оновити «{name}» до останньої версії?", "extensionUpdateSuccess": "Оновлено успішно", "extensionDelete": "Видалити розширення", "extensionAlreadyLatest": "Вже остання версія", diff --git a/ui/flutter/lib/l10n/app_vi.arb b/ui/flutter/lib/l10n/app_vi.arb index ab4171ab..8e3e36f5 100644 --- a/ui/flutter/lib/l10n/app_vi.arb +++ b/ui/flutter/lib/l10n/app_vi.arb @@ -103,6 +103,7 @@ "extensions": "Tiện ích mở rộng", "extensionInstallUrl": "Liên kết cài đặt", "extensionInstallSuccess": "Cài đặt thành công", + "extensionUpdateConfirm": "Cập nhật “{name}” lên phiên bản mới nhất?", "extensionUpdateSuccess": "Cập nhật thành công", "extensionDelete": "Xóa tiện ích mở rộng", "extensionAlreadyLatest": "Đây đã là phiên bản mới nhất", diff --git a/ui/flutter/lib/l10n/app_zh.arb b/ui/flutter/lib/l10n/app_zh.arb index 0b801d25..2395f69b 100644 --- a/ui/flutter/lib/l10n/app_zh.arb +++ b/ui/flutter/lib/l10n/app_zh.arb @@ -103,6 +103,7 @@ "extensions": "扩展", "extensionInstallUrl": "安装链接", "extensionInstallSuccess": "安装成功", + "extensionUpdateConfirm": "确定将“{name}”更新到最新版本吗?", "extensionUpdateSuccess": "更新成功", "extensionDelete": "删除扩展", "extensionAlreadyLatest": "已经是最新版本", diff --git a/ui/flutter/lib/l10n/app_zh_TW.arb b/ui/flutter/lib/l10n/app_zh_TW.arb index 21a2bc72..2692840b 100644 --- a/ui/flutter/lib/l10n/app_zh_TW.arb +++ b/ui/flutter/lib/l10n/app_zh_TW.arb @@ -103,6 +103,7 @@ "extensions": "擴充功能", "extensionInstallUrl": "安裝 URL", "extensionInstallSuccess": "安裝成功", + "extensionUpdateConfirm": "確定將「{name}」更新到最新版本嗎?", "extensionUpdateSuccess": "更新成功", "extensionDelete": "刪除擴充功能", "extensionAlreadyLatest": "已是最新版本", diff --git a/ui/flutter/test/widget_test.dart b/ui/flutter/test/widget_test.dart index 80fe6a95..7656b5ce 100644 --- a/ui/flutter/test/widget_test.dart +++ b/ui/flutter/test/widget_test.dart @@ -648,6 +648,73 @@ void main() { expect(tester.takeException(), isNull); }); + for (final (width, details) in [(390.0, false), (1100.0, false), (1100.0, true)]) { + testWidgets('extension update confirms, shows progress and retries at $width details=$details', (tester) async { + await _setTestSize(tester, Size(width, 900)); + final controller = UpdateableExtensionsController(); + await tester.pumpWidget( + ProviderScope( + overrides: [extensionsControllerProvider.overrideWith(() => controller)], + child: shad.ShadcnApp( + theme: AppTheme.light(), + materialTheme: AppTheme.materialLight(), + home: const ExtensionsPage(), + ), + ), + ); + await tester.pumpAndSettle(); + if (details) { + await tester.tap(find.byKey(const ValueKey('extension-card-extension-0'))); + await tester.pumpAndSettle(); + } + final entry = find.byKey(ValueKey(details ? 'extension-details-update' : 'update-extension-extension-0')); + final dialog = find.byKey(const ValueKey('extension-update-dialog')); + final confirm = find.byKey(const ValueKey('confirm-update-extension-button')); + final cancel = find.byKey(const ValueKey('cancel-update-extension-button')); + await tester.tap(entry); + await tester.pumpAndSettle(); + expect(dialog, findsOneWidget); + expect(find.text('Update “Extension 0” to the latest version?'), findsOneWidget); + expect(controller.updateCalls, 0); + await tester.tap(cancel); + await tester.pumpAndSettle(); + expect(dialog, findsNothing); + expect(controller.updateCalls, 0); + + await tester.tap(entry); + await tester.pumpAndSettle(); + controller.failUpdate = true; + await tester.tap(confirm); + await tester.pump(); + expect(controller.updateCalls, 1); + expect(tester.widget(confirm).loading, isTrue); + expect(tester.widget(cancel).onPressed, isNull); + await tester.tap(confirm); + await tester.tapAt(const Offset(5, 5)); + await tester.binding.handlePopRoute(); + await tester.pump(); + expect(controller.updateCalls, 1); + expect(dialog, findsOneWidget); + controller.updateGate.complete(); + await tester.pumpAndSettle(); + expect(dialog, findsOneWidget); + expect(find.byKey(const ValueKey('extension-update-error')), findsOneWidget); + expect(tester.widget(confirm).loading, isFalse); + + controller.failUpdate = false; + controller.updateGate = Completer(); + await tester.tap(confirm); + await tester.pump(); + expect(controller.updateCalls, 2); + controller.updateGate.complete(); + await tester.pumpAndSettle(); + expect(dialog, findsNothing); + await tester.pump(const Duration(seconds: 6)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + }); + } + testWidgets('installed extension details use a disabled installed button without a hero status', ( WidgetTester tester, ) async { @@ -5304,6 +5371,23 @@ class PaginatedExtensionsController extends FakeExtensionsController { } } +class UpdateableExtensionsController extends FakeExtensionsController { + int updateCalls = 0; + bool failUpdate = false; + Completer updateGate = Completer(); + + @override + Future build() async => + (await super.build()).copyWith(listFilter: ExtensionListFilter.installed, updateFlags: {'extension-0': '2.0.0'}); + + @override + Future upgradeExtension(api_extension.Extension extension) async { + updateCalls++; + await updateGate.future; + if (failUpdate) throw StateError('update failed'); + } +} + class BusyInstallExtensionsController extends FakeExtensionsController { @override Future build() async {