mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-09 18:10:29 +00:00
feat(ui): rework extension update detection and entry point (#1535)
This commit is contained in:
31 files changed
+434
-228
No files matched your search
@@ -0,0 +1,162 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gopeed/api/model/extension.dart';
|
||||
import 'package:gopeed/api/model/store_extension.dart';
|
||||
import 'package:gopeed/features/extensions/application/extensions_controller.dart';
|
||||
import 'package:gopeed/features/extensions/presentation/pages/extension_details_page.dart';
|
||||
import 'package:gopeed/features/extensions/presentation/widgets/extension_detail_view.dart';
|
||||
import 'package:gopeed/shared/theme/app_theme.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
void main() {
|
||||
for (final mobile in [false, true]) {
|
||||
testWidgets('local README renders while the market request is pending (mobile: $mobile)', (tester) async {
|
||||
final directory = Directory.systemTemp.createTempSync('gopeed-readme-');
|
||||
addTearDown(() => directory.deleteSync(recursive: true));
|
||||
File(path.join(directory.path, 'README.md')).writeAsStringSync('# Local README');
|
||||
final installed = _installed(directory.path);
|
||||
final controller = _ReadmeController(
|
||||
ExtensionsState(installedExtensions: [installed], loadingStore: true),
|
||||
pendingMarket: true,
|
||||
);
|
||||
await _pumpDetails(tester, controller, ExtensionListItem(installed: installed), mobile: mobile);
|
||||
|
||||
expect(controller.marketRequest.isCompleted, isFalse);
|
||||
expect(find.text('Local README'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// A stale market response must not replace the installed documentation.
|
||||
controller.finishMarket();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Local README'), findsOneWidget);
|
||||
expect(find.text('Old market README'), findsNothing);
|
||||
});
|
||||
|
||||
for (final mutate in [false, true]) {
|
||||
testWidgets('open README reloads after an upgrade (mobile: $mobile, mutate: $mutate)', (tester) async {
|
||||
final directory = Directory.systemTemp.createTempSync('gopeed-readme-');
|
||||
addTearDown(() => directory.deleteSync(recursive: true));
|
||||
final readme = File(path.join(directory.path, 'README.md'))..writeAsStringSync('# Before upgrade');
|
||||
final installed = _installed(directory.path);
|
||||
final controller = _ReadmeController(
|
||||
ExtensionsState(installedExtensions: [installed], storeExtensions: [_store()]),
|
||||
);
|
||||
await _pumpDetails(
|
||||
tester,
|
||||
controller,
|
||||
ExtensionListItem(installed: installed, store: _store()),
|
||||
mobile: mobile,
|
||||
);
|
||||
expect(find.text('Before upgrade'), findsOneWidget);
|
||||
|
||||
readme.writeAsStringSync('# After upgrade');
|
||||
controller.publishVersion('2.0.0', mutate: mutate);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('After upgrade'), findsOneWidget);
|
||||
expect(find.text('Before upgrade'), findsNothing);
|
||||
expect(find.text('Old market README'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets('missing local README falls back to the store copy', (tester) async {
|
||||
final directory = Directory.systemTemp.createTempSync('gopeed-readme-');
|
||||
addTearDown(() => directory.deleteSync(recursive: true));
|
||||
final installed = _installed(directory.path);
|
||||
final controller = _ReadmeController(
|
||||
ExtensionsState(installedExtensions: [installed], storeExtensions: [_store()]),
|
||||
);
|
||||
await _pumpDetails(tester, controller, ExtensionListItem(installed: installed, store: _store()), mobile: false);
|
||||
expect(find.text('Old market README'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpDetails(
|
||||
WidgetTester tester,
|
||||
_ReadmeController controller,
|
||||
ExtensionListItem item, {
|
||||
required bool mobile,
|
||||
}) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = Size(mobile ? 390 : 1100, 900);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(() => controller)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: mobile
|
||||
? ExtensionDetailsPage(extensionId: item.id, initialItem: item)
|
||||
: ExtensionDetailDrawer(item: item, onClose: () {}),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
class _ReadmeController extends ExtensionsController {
|
||||
_ReadmeController(this.initial, {this.pendingMarket = false});
|
||||
|
||||
final ExtensionsState initial;
|
||||
final bool pendingMarket;
|
||||
final marketRequest = Completer<ExtensionsState>();
|
||||
|
||||
@override
|
||||
Future<ExtensionsState> build() async {
|
||||
if (!pendingMarket) return initial;
|
||||
state = AsyncValue.data(initial);
|
||||
return marketRequest.future;
|
||||
}
|
||||
|
||||
void finishMarket() {
|
||||
marketRequest.complete(state.requireValue.copyWith(storeExtensions: [_store()], loadingStore: false));
|
||||
}
|
||||
|
||||
void publishVersion(String version, {required bool mutate}) {
|
||||
final current = state.requireValue;
|
||||
final installed = current.installedExtensions.single;
|
||||
final updated = mutate ? installed : Extension.fromJson(installed.toJson());
|
||||
updated.version = version;
|
||||
state = AsyncValue.data(current.copyWith(installedExtensions: [updated]));
|
||||
}
|
||||
}
|
||||
|
||||
Extension _installed(String directory) => Extension(
|
||||
identity: 'gopeed@test',
|
||||
name: 'test',
|
||||
author: 'Gopeed',
|
||||
title: 'Test extension',
|
||||
description: 'Installed extension',
|
||||
icon: '',
|
||||
version: '1.0.0',
|
||||
homepage: '',
|
||||
repository: null,
|
||||
disabled: false,
|
||||
devMode: true,
|
||||
devPath: directory,
|
||||
);
|
||||
|
||||
StoreExtension _store() => StoreExtension(
|
||||
id: 'gopeed@test',
|
||||
repoFullName: 'GopeedLab/test',
|
||||
repoUrl: 'https://github.com/GopeedLab/test',
|
||||
name: 'test',
|
||||
author: 'Gopeed',
|
||||
title: 'Test extension',
|
||||
description: 'Store extension',
|
||||
version: '1.0.0',
|
||||
readme: '# Old market README',
|
||||
installCount: 1,
|
||||
stars: 1,
|
||||
topics: const [],
|
||||
);
|
||||
@@ -50,6 +50,7 @@ import 'package:gopeed/features/home/presentation/widgets/tasks_top_bar.dart';
|
||||
import 'package:gopeed/features/extensions/application/extensions_controller.dart';
|
||||
import 'package:gopeed/features/extensions/presentation/pages/extension_details_page.dart';
|
||||
import 'package:gopeed/features/extensions/presentation/pages/extensions_page.dart';
|
||||
import 'package:gopeed/features/extensions/presentation/widgets/extension_icon.dart';
|
||||
import 'package:gopeed/features/home/presentation/widgets/primary_rail.dart';
|
||||
import 'package:gopeed/features/tasks/application/pending_update_task.dart';
|
||||
import 'package:gopeed/features/tasks/application/task_batch_selection_controller.dart';
|
||||
@@ -688,6 +689,30 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
for (final width in [320.0, 1100.0]) {
|
||||
testWidgets('extension update status keeps the card header at icon height at $width', (tester) async {
|
||||
await _setTestSize(tester, Size(width, 900));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(UpdateableExtensionsController.new)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final card = find.byKey(const ValueKey('extension-card-extension-0'));
|
||||
final icon = find.descendant(of: card, matching: find.byType(ExtensionIcon));
|
||||
final description = find.descendant(of: card, matching: find.text('Installed extension'));
|
||||
expect(find.byKey(const ValueKey('extension-card-update-status-extension-0')), findsOneWidget);
|
||||
expect(tester.getRect(description).top - tester.getRect(icon).bottom, closeTo(10, 0.01));
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('extension details reuse the desktop detail drawer', (WidgetTester tester) async {
|
||||
await _setTestSize(tester, const Size(1100, 900));
|
||||
final controller = FakeExtensionsController();
|
||||
@@ -817,7 +842,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 +2354,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();
|
||||
|
||||
Reference in new issue
Block a user