Merge PR #1535 into 2.0.0-beta.2

This commit is contained in:
Levi committed 2026-09-25 16:23:39 +08:00
commit 8f2af5660e
30 files changed
+197 -154

No files matched your search

@@ -185,12 +185,11 @@ class ExtensionsController extends AsyncNotifier<ExtensionsState> {
}
bool canUpdateItem(ExtensionListItem item) {
final current = _current;
if (item.installed == null) return false;
if (current.listFilter == ExtensionListFilter.market && item.store != null) {
return _compareVersion(item.store!.version, item.installed!.version) > 0;
}
return current.updateFlags.containsKey(item.installed!.identity);
final installed = item.installed;
if (installed == null) return false;
// The backend checks GitHub live for every installed extension; the store
// index may lag behind and must not decide update visibility.
return _current.updateFlags.containsKey(installed.identity);
}
Future<void> installFromStore(StoreExtension extension) async {
@@ -240,6 +239,9 @@ class ExtensionsController extends AsyncNotifier<ExtensionsState> {
await _runBusy(extension.identity, () async {
await ref.read(gopeedServiceProvider).updateExtension(extension.identity);
await loadInstalled(refreshUpdates: false);
// Drop the stale flag immediately; the background re-check confirms it.
final flags = Map<String, String>.of(_current.updateFlags)..remove(extension.identity);
state = AsyncValue.data(_current.copyWith(updateFlags: flags));
unawaited(checkUpdate());
_bumpStoreInstallCount(extension.identity);
_reportInstallSafe(extension.identity);
@@ -354,24 +356,4 @@ class ExtensionsController extends AsyncNotifier<ExtensionsState> {
}());
}
static int _compareVersion(String a, String b) {
final aNums = _toVersionNumbers(a);
final bNums = _toVersionNumbers(b);
final maxLen = aNums.length > bNums.length ? aNums.length : bNums.length;
for (var i = 0; i < maxLen; i++) {
final left = i < aNums.length ? aNums[i] : 0;
final right = i < bNums.length ? bNums[i] : 0;
if (left > right) return 1;
if (left < right) return -1;
}
return 0;
}
static List<int> _toVersionNumbers(String version) {
return version
.split(RegExp(r'[^0-9]+'))
.where((part) => part.isNotEmpty)
.map((part) => int.tryParse(part) ?? 0)
.toList();
}
}
@@ -8,6 +8,8 @@ import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/detail/app_detail_surface.dart';
import '../../application/extensions_controller.dart';
import '../widgets/extension_detail_view.dart';
import '../widgets/extension_update_dialog.dart';
import '../widgets/extension_update_status.dart';
class ExtensionDetailsPage extends ConsumerWidget {
const ExtensionDetailsPage({super.key, required this.extensionId, this.initialItem});
@@ -24,6 +26,15 @@ class ExtensionDetailsPage extends ConsumerWidget {
return AppDetailPage(
title: item?.title ?? context.l10n.extensions,
onBack: () => context.canPop() ? context.pop() : context.go('/extensions'),
titleTrailing: _canUpdate(stateAsync.value, item)
? ExtensionUpdateStatus(
key: const ValueKey('extension-details-update-mobile'),
label: context.l10n.extensionCanUpdate,
onTap: item?.installed == null
? null
: () => showExtensionUpdateDialog(context, item!.installed!),
)
: null,
child: item == null
? Center(
child: stateAsync.isLoading
@@ -33,4 +44,9 @@ class ExtensionDetailsPage extends ConsumerWidget {
: ExtensionDetailView(key: ValueKey(item.id), item: item, mobile: true),
);
}
bool _canUpdate(ExtensionsState? state, ExtensionListItem? item) {
final installed = item?.installed;
return item != null && installed != null && (state?.updateFlags.containsKey(installed.identity) ?? false);
}
}
@@ -28,6 +28,7 @@ import '../widgets/extension_detail_view.dart';
import '../widgets/extension_icon.dart';
import '../widgets/extension_setting_field.dart';
import '../widgets/extension_update_dialog.dart';
import '../widgets/extension_update_status.dart';
const _extensionCardMinWidth = 290.0;
const _extensionGridSpacing = 10.0;
@@ -485,25 +486,7 @@ class _Content extends StatelessWidget {
}
bool _canUpdate(ExtensionsState state, ExtensionListItem item) {
if (item.installed == null) return false;
if (state.listFilter == ExtensionListFilter.market && item.store != null) {
return _compareVersion(item.store!.version, item.installed!.version) > 0;
}
return state.updateFlags.containsKey(item.installed!.identity);
}
int _compareVersion(String a, String b) {
final aNums = a.split(RegExp(r'[^0-9]+')).where((part) => part.isNotEmpty).map((part) => int.tryParse(part) ?? 0);
final bNums = b.split(RegExp(r'[^0-9]+')).where((part) => part.isNotEmpty).map((part) => int.tryParse(part) ?? 0);
final left = aNums.toList();
final right = bNums.toList();
final maxLen = left.length > right.length ? left.length : right.length;
for (var i = 0; i < maxLen; i++) {
final l = i < left.length ? left[i] : 0;
final r = i < right.length ? right[i] : 0;
if (l != r) return l.compareTo(r);
}
return 0;
return item.installed != null && state.updateFlags.containsKey(item.installed!.identity);
}
}
@@ -994,11 +977,27 @@ class _ExtensionCard extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: palette.textPrimary, fontSize: 14, fontWeight: FontWeight.w700),
Row(
children: [
Flexible(
child: Text(
item.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: palette.textPrimary, fontSize: 14, fontWeight: FontWeight.w700),
),
),
if (canUpdate && installed != null) ...[
const SizedBox(width: 4),
Flexible(
child: ExtensionUpdateStatus(
key: ValueKey('extension-card-update-status-${item.id}'),
label: context.l10n.extensionCanUpdate,
onTap: busy ? null : () => showExtensionUpdateDialog(context, installed),
),
),
],
],
),
const SizedBox(height: 2),
Text(
@@ -1067,13 +1066,6 @@ class _ExtensionCard extends ConsumerWidget {
key: ValueKey('extension-card-actions-${item.id}'),
mainAxisSize: MainAxisSize.min,
children: [
if (canUpdate && installed != null)
shad.GhostButton(
density: shad.ButtonDensity.icon,
key: ValueKey('update-extension-${installed.identity}'),
onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed),
child: const Icon(Icons.refresh),
),
if (installed == null && item.store != null)
shad.GhostButton(
key: ValueKey('install-store-extension-${item.store!.id}'),
@@ -1127,21 +1119,17 @@ class _ExtensionCard extends ConsumerWidget {
],
),
);
if (item.store == null) return card;
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
key: ValueKey('open-extension-details-${item.id}'),
behavior: HitTestBehavior.opaque,
onTap: () {
if (MediaQuery.sizeOf(context).width < Breakpoints.mobile) {
context.push('/extensions/${Uri.encodeComponent(item.id)}', extra: item);
return;
}
onOpenDetails(item);
},
child: card,
),
return GestureDetector(
key: ValueKey('open-extension-details-${item.id}'),
behavior: HitTestBehavior.opaque,
onTap: () {
if (MediaQuery.sizeOf(context).width < Breakpoints.mobile) {
context.push('/extensions/${Uri.encodeComponent(item.id)}', extra: item);
return;
}
onOpenDetails(item);
},
child: card,
);
}
}
@@ -21,20 +21,33 @@ import '../../../../util/util.dart';
import '../../application/extensions_controller.dart';
import 'extension_icon.dart';
import 'extension_update_dialog.dart';
import 'extension_update_status.dart';
class ExtensionDetailDrawer extends StatelessWidget {
class ExtensionDetailDrawer extends ConsumerWidget {
const ExtensionDetailDrawer({super.key, required this.item, required this.onClose});
final ExtensionListItem? item;
final VoidCallback onClose;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(extensionsControllerProvider).value;
final installed = item?.installed;
final canUpdate =
item != null && installed != null && (state?.updateFlags.containsKey(installed.identity) ?? false);
final updateChip = canUpdate
? ExtensionUpdateStatus(
key: const ValueKey('extension-details-update'),
label: context.l10n.extensionCanUpdate,
onTap: () => showExtensionUpdateDialog(context, installed),
)
: null;
return AppDetailDrawer(
open: item != null,
title: item?.title ?? '',
onClose: onClose,
drawerKey: const ValueKey('extension-details-drawer'),
titleTrailing: updateChip,
child: item == null ? const SizedBox.shrink() : ExtensionDetailView(item: item!),
);
}
@@ -104,8 +117,6 @@ class ExtensionDetailView extends ConsumerWidget {
style: TextStyle(color: palette.textMuted, fontSize: 11.5, fontWeight: FontWeight.w600),
),
if (store != null) _ExtensionStats(store: store),
if (installed != null && canUpdate)
_ExtensionStatus(label: context.l10n.extensionCanUpdate, emphasized: true),
],
),
],
@@ -130,15 +141,6 @@ class ExtensionDetailView extends ConsumerWidget {
_runAction(context, () => ref.read(extensionsControllerProvider.notifier).installFromStore(store)),
child: Text(context.l10n.extensionInstall),
),
if (installed != null && canUpdate)
AppLoadingButton(
key: const ValueKey('extension-details-update'),
loading: false,
variant: AppLoadingButtonVariant.primary,
icon: const Icon(Icons.refresh, size: 16),
onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed),
child: Text(context.l10n.newVersionUpdate),
),
if (installed != null && !canUpdate)
AppLoadingButton(
key: const ValueKey('extension-details-installed'),
@@ -276,35 +278,6 @@ class _ExtensionReadmeState extends State<_ExtensionReadme> {
}
}
class _ExtensionStatus extends StatelessWidget {
const _ExtensionStatus({required this.label, required this.emphasized});
final String label;
final bool emphasized;
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
final dotColor = emphasized ? palette.brand : palette.success;
final textColor = emphasized ? palette.textPrimary : palette.success;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 5,
height: 5,
decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle),
),
const SizedBox(width: 5),
Text(
label,
style: TextStyle(color: textColor, fontSize: 11.5, fontWeight: FontWeight.w600),
),
],
);
}
}
class _LinkButtonContent extends StatelessWidget {
const _LinkButtonContent({required this.icon, required this.label});
@@ -58,7 +58,7 @@ class _ExtensionUpdateDialogState extends ConsumerState<_ExtensionUpdateDialog>
canPop: !_updating,
child: AlertDialog(
key: const ValueKey('extension-update-dialog'),
title: Text(context.l10n.extensionCanUpdate),
title: Text(context.l10n.newVersionAvailable),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -0,0 +1,58 @@
import 'package:flutter/widgets.dart';
import '../../../../shared/theme/app_palette.dart';
/// Green status dot with an optional label, marking an available update.
/// Pass [onTap] to make it open the update prompt.
class ExtensionUpdateStatus extends StatelessWidget {
const ExtensionUpdateStatus({super.key, this.label, this.onTap});
final String? label;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
final content = Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: palette.brandSoft,
borderRadius: BorderRadius.circular(999),
border: Border.all(color: palette.brand.withValues(alpha: 0.35)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(color: palette.brand, shape: BoxShape.circle),
),
if (label != null) ...[
const SizedBox(width: 5),
Flexible(
child: Text(
label!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: palette.textPrimary, fontSize: 11.5, fontWeight: FontWeight.w600),
),
),
],
],
),
);
if (onTap == null) return content;
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
child: content,
),
),
);
}
}
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "إضافة",
"edit": "تعديل",
"newVersionTitle": "يتوفر إصدار جديد @version",
"newVersionUpdate": "تحديث الآن",
"newVersionUpdate": "تحديث",
"newVersionLater": "لاحقاً",
"extensions": "الإضافات",
"extensionInstallUrl": "عنوان التثبيت",
+1 -1
View File
@@ -106,7 +106,7 @@
"add": "Əlavə et",
"edit": "Redaktə et",
"newVersionTitle": "Yeni versiya @version tapıldı",
"newVersionUpdate": "İndi Yenilə",
"newVersionUpdate": "Yenilə",
"newVersionLater": "Sonra",
"extensions": "Uzantılar",
"extensionInstallUrl": "Quraşdırma URL-i",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Afegeix",
"edit": "Edita",
"newVersionTitle": "Descobriu la nova versió @version",
"newVersionUpdate": "Actualitza ara",
"newVersionUpdate": "Actualitza",
"newVersionLater": "Més tard",
"extensions": "Extensions",
"extensionInstallUrl": "URL d'instal·lació",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Hinzugefügt",
"edit": "Bearbeiten",
"newVersionTitle": "Entdecken Sie die neue Version @version",
"newVersionUpdate": "Jetzt aktualisieren",
"newVersionUpdate": "Aktualisieren",
"newVersionLater": "Später",
"extensions": "Erweiterungen",
"extensionInstallUrl": "Installations-URL",
+2 -2
View File
@@ -106,7 +106,7 @@
"add": "Add",
"edit": "Edit",
"newVersionTitle": "Discover new version @version",
"newVersionUpdate": "Update Now",
"newVersionUpdate": "Update",
"newVersionLater": "Later",
"extensions": "Extensions",
"extensionInstallUrl": "Install URL",
@@ -137,7 +137,7 @@
"extensionLoadMore": "Load More",
"extensionNoMore": "No more extensions",
"extensionInstalled": "Installed",
"extensionCanUpdate": "Update Available",
"extensionCanUpdate": "Update",
"extensionInstall": "Install",
"extensionLoadLocal": "Install Local Folder",
"extensionFilterAll": "All",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Añadir",
"edit": "Editar",
"newVersionTitle": "Nueva versión @version disponible",
"newVersionUpdate": "Actualizar Ahora",
"newVersionUpdate": "Actualizar",
"newVersionLater": "Más tarde",
"extensions": "Extensiones",
"extensionInstallUrl": "URL de instalación",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "افزودن",
"edit": "ویرایش",
"newVersionTitle": "@version عنوان: کشف نسخه جدید",
"newVersionUpdate": "بروزرسانی",
"newVersionUpdate": "به‌روزرسانی",
"newVersionLater": "بعداً",
"extensions": "افزونه‌ها",
"extensionInstallUrl": "نشانی نصب",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Ajouter",
"edit": "Modifier",
"newVersionTitle": "Nouvelle version @version disponible",
"newVersionUpdate": "Mettre à jour maintenant",
"newVersionUpdate": "Mettre à jour",
"newVersionLater": "Plus tard",
"extensions": "Extensions",
"extensionInstallUrl": "URL d'installation",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Hozzáad",
"edit": "Szerk",
"newVersionTitle": "Új verzió felfedezése @version",
"newVersionUpdate": "Frissítés most",
"newVersionUpdate": "Frissítés",
"newVersionLater": "Később",
"extensions": "Bővítmények",
"extensionInstallUrl": "Telepítési URL",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Tambah",
"edit": "Sunting",
"newVersionTitle": "Temukan versi batu @version",
"newVersionUpdate": "Perbarui Sekarang",
"newVersionUpdate": "Perbarui",
"newVersionLater": "Nanti",
"extensions": "Ekstensi",
"extensionInstallUrl": "URL Instal",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Aggiungi",
"edit": "Modifica",
"newVersionTitle": "Scopri la nuova versione @version",
"newVersionUpdate": "Aggiorna ora",
"newVersionUpdate": "Aggiorna",
"newVersionLater": "Dopo",
"extensions": "Estensioni",
"extensionInstallUrl": "Installa URL",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "追加",
"edit": "編集",
"newVersionTitle": "新しいバージョン @version を発見する",
"newVersionUpdate": "アップデート",
"newVersionUpdate": "更新",
"newVersionLater": "後で",
"extensions": "拡張機能",
"extensionInstallUrl": "インストールURL",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "추가",
"edit": "편집",
"newVersionTitle": "새 버전 @version 발견",
"newVersionUpdate": "지금 업데이트",
"newVersionUpdate": "업데이트",
"newVersionLater": "나중에",
"extensions": "확장 프로그램",
"extensionInstallUrl": "설치 URL",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Dodaj",
"edit": "Edycja",
"newVersionTitle": "Sprawdź aktualizację @version",
"newVersionUpdate": "Aktualizuj teraz",
"newVersionUpdate": "Aktualizuj",
"newVersionLater": "Później",
"extensions": "Wtyczki",
"extensionInstallUrl": "Instaluj z url",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Adicionar",
"edit": "Editar",
"newVersionTitle": "Nova Versão Disponível @version",
"newVersionUpdate": "Atualizar Agora",
"newVersionUpdate": "Atualizar",
"newVersionLater": "Mais Tarde",
"extensions": "Extensões",
"extensionInstallUrl": "Link de Instalação",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "சேர்",
"edit": "திருத்துக",
"newVersionTitle": "புதிய பதிப்பைக் கண்டறியவும் @version",
"newVersionUpdate": "இப்பொழுது புதுப்பி",
"newVersionUpdate": "புதுப்பிப்பு",
"newVersionLater": "பின்னர்",
"extensions": "நீட்டிப்புகள்",
"extensionInstallUrl": "நிறுவப்படும் URL",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Ekle",
"edit": "Düzenle",
"newVersionTitle": "Yeni sürüm @version keşfedildi",
"newVersionUpdate": "Şimdi Güncelle",
"newVersionUpdate": "Güncelle",
"newVersionLater": "Daha Sonra",
"extensions": "Eklentiler",
"extensionInstallUrl": "Kurulum URL'si",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Додати",
"edit": "Редагування",
"newVersionTitle": "Доступна нова версія @Version",
"newVersionUpdate": "Оновити зараз",
"newVersionUpdate": "Оновити",
"newVersionLater": "Пізніше",
"extensions": "Розширення",
"extensionInstallUrl": "URL для встановлення",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "Thêm",
"edit": "Sửa",
"newVersionTitle": "Khám phá phiên bản mới @version",
"newVersionUpdate": "Cập nhật ngay",
"newVersionUpdate": "Cập nhật",
"newVersionLater": "Sau",
"extensions": "Tiện ích mở rộng",
"extensionInstallUrl": "Liên kết cài đặt",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "添加",
"edit": "编辑",
"newVersionTitle": "发现新版本 @version",
"newVersionUpdate": "立即更新",
"newVersionUpdate": "更新",
"newVersionLater": "稍后再说",
"extensions": "扩展",
"extensionInstallUrl": "安装链接",
+1 -1
View File
@@ -99,7 +99,7 @@
"add": "新增",
"edit": "編輯",
"newVersionTitle": "發現新版本 @version",
"newVersionUpdate": "立即更新",
"newVersionUpdate": "更新",
"newVersionLater": "稍後",
"extensions": "擴充功能",
"extensionInstallUrl": "安裝 URL",
@@ -16,6 +16,7 @@ class AppDetailDrawer extends StatelessWidget {
required this.child,
this.drawerKey,
this.width,
this.titleTrailing,
});
final bool open;
@@ -24,6 +25,7 @@ class AppDetailDrawer extends StatelessWidget {
final Widget child;
final Key? drawerKey;
final double? width;
final Widget? titleTrailing;
@override
Widget build(BuildContext context) {
@@ -82,15 +84,25 @@ class AppDetailDrawer extends StatelessWidget {
child: Row(
children: [
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: palette.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
),
child: Row(
children: [
Flexible(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: palette.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
if (titleTrailing != null) ...[
const SizedBox(width: 8),
Flexible(child: titleTrailing!),
],
],
),
),
const SizedBox(width: 12),
@@ -13,12 +13,14 @@ class AppDetailPage extends StatelessWidget {
required this.onBack,
required this.child,
this.backgroundColor,
this.titleTrailing,
});
final String title;
final VoidCallback onBack;
final Widget child;
final Color? backgroundColor;
final Widget? titleTrailing;
@override
Widget build(BuildContext context) {
@@ -53,12 +55,22 @@ class AppDetailPage extends StatelessWidget {
),
),
Expanded(
child: Text(
title,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: palette.textPrimary, fontSize: 17, fontWeight: FontWeight.w700),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: palette.textPrimary, fontSize: 17, fontWeight: FontWeight.w700),
),
),
if (titleTrailing != null) ...[
const SizedBox(width: 8),
Flexible(child: titleTrailing!),
],
],
),
),
const SizedBox(width: 44),
+4 -2
View File
@@ -817,7 +817,9 @@ void main() {
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 entry = find.byKey(
ValueKey(details ? 'extension-details-update' : 'extension-card-update-status-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'));
@@ -2327,7 +2329,7 @@ void main() {
final progress = find.descendant(of: updateButton, matching: find.byType(shad.CircularProgressIndicator));
expect(progress, findsOneWidget);
expect(tester.getSize(progress), const Size.square(14));
expect(find.descendant(of: updateButton, matching: find.text('Update Now')), findsOneWidget);
expect(find.descendant(of: updateButton, matching: find.text('Update')), findsOneWidget);
runtimeController.completeTrackerUpdate();
await tester.pumpAndSettle();