feat(account): 增加账号中心页面与入口
This commit is contained in:
1 parent
c68f02449f
commit
da095115d4
23 files changed
+2343
-60
No files matched your search
@@ -13,9 +13,8 @@ void main() {
|
||||
String envelope(Object? data, {int code = 0, String message = 'ok'}) =>
|
||||
jsonEncode({'code': code, 'message': message, 'data': data});
|
||||
|
||||
AccountApi apiWith(FakeHttpAdapter adapter) => AccountApi(
|
||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||
);
|
||||
AccountApi apiWith(FakeHttpAdapter adapter) =>
|
||||
AccountApi(ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter));
|
||||
|
||||
test('should_get_profile_and_parse_numeric_vip_level', () async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
@@ -90,11 +89,7 @@ void main() {
|
||||
'status': 'active',
|
||||
'deleted': 0,
|
||||
},
|
||||
{
|
||||
'id': 7,
|
||||
'deviceId': 'd-android-5678',
|
||||
'platform': 'ANDROID',
|
||||
},
|
||||
{'id': 7, 'deviceId': 'd-android-5678', 'platform': 'ANDROID'},
|
||||
]),
|
||||
),
|
||||
);
|
||||
@@ -138,14 +133,10 @@ void main() {
|
||||
|
||||
test('should_request_deactivation_with_reason_and_parse_schedule', () async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse(
|
||||
envelope('2026-11-03T09:30:00'),
|
||||
),
|
||||
(_) async => FakeHttpResponse(envelope('2026-11-03T09:30:00')),
|
||||
);
|
||||
|
||||
final scheduledAt = await apiWith(
|
||||
adapter,
|
||||
).deactivate(reason: '不再使用');
|
||||
final scheduledAt = await apiWith(adapter).deactivate(reason: '不再使用');
|
||||
|
||||
expect(adapter.requests.single.method, 'POST');
|
||||
expect(adapter.requests.single.uri.path, '/app/me/deactivate');
|
||||
@@ -161,18 +152,14 @@ void main() {
|
||||
await apiWith(adapter).cancelDeactivation();
|
||||
|
||||
expect(adapter.requests.single.method, 'POST');
|
||||
expect(
|
||||
adapter.requests.single.uri.path,
|
||||
'/app/me/deactivate/cancel',
|
||||
);
|
||||
expect(adapter.requests.single.uri.path, '/app/me/deactivate/cancel');
|
||||
expect(adapter.requests.single.data, isNull);
|
||||
});
|
||||
|
||||
test('should_keep_backend_message_when_cancel_fails', () async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse(
|
||||
envelope(null, code: 100009, message: '没有待处理的注销申请'),
|
||||
),
|
||||
(_) async =>
|
||||
FakeHttpResponse(envelope(null, code: 100009, message: '没有待处理的注销申请')),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
|
||||
@@ -70,28 +70,28 @@ void main() {
|
||||
expect(copied, contains('n***@example.com'));
|
||||
});
|
||||
|
||||
test('should_clear_session_and_keep_schedule_when_deactivation_succeeds', () async {
|
||||
final repository = FakeAccountRepository();
|
||||
var cleared = false;
|
||||
final logic = AccountDeactivateLogic(
|
||||
repository,
|
||||
clearSession: () async => cleared = true,
|
||||
);
|
||||
test(
|
||||
'should_clear_session_and_keep_schedule_when_deactivation_succeeds',
|
||||
() async {
|
||||
final repository = FakeAccountRepository();
|
||||
var cleared = false;
|
||||
final logic = AccountDeactivateLogic(
|
||||
repository,
|
||||
clearSession: () async => cleared = true,
|
||||
);
|
||||
|
||||
final ok = await logic.request('不再使用');
|
||||
final ok = await logic.request('不再使用');
|
||||
|
||||
expect(ok, isTrue);
|
||||
expect(repository.lastDeactivateReason, '不再使用');
|
||||
expect(logic.scheduledAt, DateTime(2026, 11, 3, 9, 30));
|
||||
expect(cleared, isTrue);
|
||||
});
|
||||
expect(ok, isTrue);
|
||||
expect(repository.lastDeactivateReason, '不再使用');
|
||||
expect(logic.scheduledAt, DateTime(2026, 11, 3, 9, 30));
|
||||
expect(cleared, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test('should_show_backend_message_when_cancel_deactivation_fails', () async {
|
||||
final repository = FakeAccountRepository()..failCancel = true;
|
||||
final logic = AccountDeactivateLogic(
|
||||
repository,
|
||||
clearSession: () async {},
|
||||
);
|
||||
final logic = AccountDeactivateLogic(repository, clearSession: () async {});
|
||||
|
||||
final ok = await logic.cancel();
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/auth_session.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/account_repository.dart';
|
||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||
import 'package:fruitshorts/core/data/remote/auth_api.dart';
|
||||
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
||||
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/account/account_deactivate_page.dart';
|
||||
import 'package:fruitshorts/pages/account/account_devices_page.dart';
|
||||
import 'package:fruitshorts/pages/account/account_export_page.dart';
|
||||
import 'package:fruitshorts/pages/account/account_profile_page.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
|
||||
import '../support/fake_account_repository.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('should_fill_profile_and_save_nickname', (tester) async {
|
||||
final repository = FakeAccountRepository();
|
||||
final auth = await _signedInAuth();
|
||||
await _pumpPage(
|
||||
tester,
|
||||
AccountProfilePage(),
|
||||
repository: repository,
|
||||
auth: auth,
|
||||
);
|
||||
|
||||
final nickname = find.byKey(const Key('account-nickname'));
|
||||
expect(tester.widget<TextField>(nickname).controller?.text, 'Nadia');
|
||||
await tester.enterText(nickname, 'Nadia Baru');
|
||||
await tester.tap(find.byKey(const Key('account-profile-save')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repository.lastUpdate?.nickname, 'Nadia Baru');
|
||||
});
|
||||
|
||||
testWidgets('should_show_device_empty_state_when_list_is_empty', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = FakeAccountRepository();
|
||||
final auth = await _signedInAuth();
|
||||
await _pumpPage(
|
||||
tester,
|
||||
AccountDevicesPage(),
|
||||
repository: repository,
|
||||
auth: auth,
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('account-devices-empty')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_confirm_before_deactivation_and_show_schedule', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = FakeAccountRepository();
|
||||
final auth = await _signedInAuth();
|
||||
await _pumpPage(
|
||||
tester,
|
||||
AccountDeactivatePage(),
|
||||
repository: repository,
|
||||
auth: auth,
|
||||
);
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('account-deactivate-reason')),
|
||||
'不再使用',
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('account-deactivate-submit')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const Key('account-deactivate-confirm')), findsOneWidget);
|
||||
await tester.tap(find.byKey(const Key('account-deactivate-confirm')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repository.lastDeactivateReason, '不再使用');
|
||||
expect(
|
||||
find.byKey(const Key('account-deactivate-scheduled')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(auth.isSignedIn, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('should_retry_export_after_failure', (tester) async {
|
||||
final repository = FakeAccountRepository()..failExport = true;
|
||||
final auth = await _signedInAuth();
|
||||
await _pumpPage(
|
||||
tester,
|
||||
AccountExportPage(),
|
||||
repository: repository,
|
||||
auth: auth,
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('account-export-error')), findsOneWidget);
|
||||
repository.failExport = false;
|
||||
await tester.tap(find.byKey(const Key('account-export-retry')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('account-export-content')), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Future<AuthController> _signedInAuth() async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
await AuthSessionStore().save(
|
||||
const AuthSession(
|
||||
accessToken: 'access',
|
||||
refreshToken: 'refresh',
|
||||
user: AuthUser(uid: 'U-K19', nickname: 'Nadia'),
|
||||
),
|
||||
);
|
||||
final client = ApiClient(baseUrl: 'http://127.0.0.1:8081');
|
||||
final auth = AuthController(AuthApi(client), AuthSessionStore());
|
||||
await auth.load();
|
||||
return auth;
|
||||
}
|
||||
|
||||
Future<void> _pumpPage(
|
||||
WidgetTester tester,
|
||||
Widget page, {
|
||||
required FakeAccountRepository repository,
|
||||
required AuthController auth,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
Provider<AccountRepository>.value(value: repository),
|
||||
ChangeNotifierProvider<AuthController>.value(value: auth),
|
||||
],
|
||||
child: MaterialApp(
|
||||
locale: const Locale('en'),
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
home: Theme(data: ThemeData.dark(), child: page),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
@@ -27,11 +27,7 @@ void main() {
|
||||
'accessToken': 'access-1',
|
||||
'refreshToken': 'refresh-1',
|
||||
'expiresIn': 7200,
|
||||
'user': {
|
||||
'uid': 'U-OLD',
|
||||
'nickname': 'Old',
|
||||
'avatarUrl': 'old.png',
|
||||
},
|
||||
'user': {'uid': 'U-OLD', 'nickname': 'Old', 'avatarUrl': 'old.png'},
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/app.dart';
|
||||
import 'package:fruitshorts/core/data/models/auth_session.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||||
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
|
||||
import '../support/fake_account_repository.dart';
|
||||
import '../support/fake_catalog.dart';
|
||||
import '../support/fake_category.dart';
|
||||
import '../support/fake_interaction_repositories.dart';
|
||||
import '../support/fake_notice_repository.dart';
|
||||
import '../support/widget_player.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('should_hide_account_entries_when_signed_out', (tester) async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
await _pumpApp(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('tab-my')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('mine-account-card')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_show_four_account_entries_when_signed_in', (
|
||||
tester,
|
||||
) async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
await AuthSessionStore().save(
|
||||
const AuthSession(
|
||||
accessToken: 'access',
|
||||
refreshToken: 'refresh',
|
||||
user: AuthUser(uid: 'U-K19', nickname: 'Nadia'),
|
||||
),
|
||||
);
|
||||
await _pumpApp(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('tab-my')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('mine-account-card')), findsOneWidget);
|
||||
expect(find.byKey(const Key('mine-account-profile')), findsOneWidget);
|
||||
expect(find.byKey(const Key('mine-account-devices')), findsOneWidget);
|
||||
expect(find.byKey(const Key('mine-account-export')), findsOneWidget);
|
||||
expect(find.byKey(const Key('mine-account-deactivate')), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpApp(WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final catalog = FakeCatalogRepository();
|
||||
await tester.pumpWidget(
|
||||
LumaDramaApp(
|
||||
playerFactory: createWidgetPlayer,
|
||||
catalogRepository: catalog,
|
||||
categoryRepository: FakeCategoryRepository(catalog: catalog),
|
||||
watchHistoryRepository: InMemoryWatchHistoryRepository(),
|
||||
favoriteRepository: InMemoryFavoriteRepository(),
|
||||
playbackRepository: FakePlaybackRepository(),
|
||||
commentRepository: FakeCommentRepository(),
|
||||
interactionRepository: FakeInteractionRepository(),
|
||||
noticeRepository: FakeNoticeRepository(),
|
||||
accountRepository: FakeAccountRepository(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
@@ -55,8 +55,8 @@ class FakeAccountRepository implements AccountRepository {
|
||||
);
|
||||
|
||||
AccountProfile _profile;
|
||||
List<AccountDevice> _devices;
|
||||
AccountExport _export;
|
||||
final List<AccountDevice> _devices;
|
||||
final AccountExport _export;
|
||||
AccountProfileUpdate? lastUpdate;
|
||||
String? lastDeactivateReason;
|
||||
int profileCalls = 0;
|
||||
|
||||
Reference in new issue
Block a user