fix: create task panel mobile UI (#1488)

This commit is contained in:
Levi authored and GitHub committed 2026-09-10 23:02:53 +08:00
1 parent b991482617
commit 0cc2b690f8
36 files changed
+869 -827

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -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);
});
+63 -3
View File
@@ -449,7 +449,7 @@ void main() {
child: shad.ShadcnApp(
theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(),
home: const ExtensionsPage(),
home: const AppComponentThemes(child: ExtensionsPage()),
),
),
);
@@ -469,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));
@@ -501,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),
@@ -745,7 +747,7 @@ void main() {
child: shad.ShadcnApp(
theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(),
home: const ExtensionsPage(),
home: const AppComponentThemes(child: ExtensionsPage()),
),
),
);
@@ -1895,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();
@@ -2337,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)),
@@ -2439,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()));
@@ -2626,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,
@@ -4854,7 +4914,7 @@ class _CreateTaskPageHarness extends StatelessWidget {
return shad.ShadcnApp(
theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(),
home: const CreateTaskWindowPage(),
home: const AppComponentThemes(child: CreateTaskWindowPage()),
);
}
}