feat(client): C 端 1-2 账号链路(邮箱登录/注册 + 令牌与 401 + 匿名/登录分叉)
为什么:客户端此前完全没有账号概念(mine 页的"登录"只是本地 demo 开关), 而受限动作(收藏/评论)要对到具体账号。本批把账号链路接上,为后续互动、进度同步、下载打底。 - AuthApi + AuthController + AuthSessionStore:注册/登录/登出/刷新,令牌与设备号存 shared_preferences (用 SharedPreferencesAsync,与 engagement 仓库同一套 API) - ApiClient:统一带 Authorization 与 X-Device-Id;401 自动刷新一次并重试,刷新失败清登录态走 onUnauthorized - 登录/注册页(pages/auth,沿用 binding/logic/view 与现有视觉):邮箱格式、密码 8-64 位含字母数字本地先校验, 错误提示走 l10n;登录/注册一键切换 - 匿名/登录分叉:首页、发现、详情保持匿名可看;收藏/点赞等受限动作走 ensureSignedIn() 弹底部提示引导登录 - mine 页展示真实账号(昵称 + uid),拿不到才回落到演示文案 - 测试:新增 auth 控制器(注册落盘/401 刷新重试/刷新失败清会话/登出)与登录页(校验/切换)用例; widget_test 改为预置本地会话(账号不再是本地开关);修掉 playback 用例对 host 大小写的环境依赖断言 门禁:flutter analyze 0 问题;flutter test 148 全过。
This commit is contained in:
1 parent
77d5748dbb
commit
e1b98df509
24 files changed
+2871
-1016
No files matched your search
@@ -0,0 +1,227 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:nontonki/core/data/models/drama.dart';
|
||||
import 'package:nontonki/pages/discover/discover_logic.dart';
|
||||
import 'package:nontonki/pages/home/home_logic.dart';
|
||||
import 'package:nontonki/pages/mine/mine_logic.dart';
|
||||
|
||||
Drama _drama(String id) => Drama(
|
||||
id: id,
|
||||
title: id,
|
||||
synopsis: 'synopsis',
|
||||
posterUrl: '/files/poster.png',
|
||||
episodes: const [],
|
||||
);
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final dramas = [_drama('signal'), _drama('city')];
|
||||
|
||||
test('the first catalog entry becomes the current drama', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
expect(logic.currentDrama.id, 'signal');
|
||||
expect(logic.tab, 1);
|
||||
expect(logic.mySection, 0);
|
||||
expect(logic.drawerOpen, isFalse);
|
||||
expect(logic.profile, isFalse);
|
||||
expect(logic.videoImmersive, isFalse);
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('an empty catalog cannot build the shell', () {
|
||||
expect(() => HomeLogic(const []), throwsStateError);
|
||||
});
|
||||
|
||||
test('a plain tab switch raises no request counter', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
var notifications = 0;
|
||||
logic.addListener(() => notifications++);
|
||||
|
||||
logic.select(1);
|
||||
|
||||
expect(logic.tab, 1);
|
||||
expect(logic.searchRequest, 0);
|
||||
expect(logic.sectionRequest, 0);
|
||||
expect(logic.resumeRequest, 0);
|
||||
expect(logic.backRequest, 0);
|
||||
expect(notifications, 1);
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('each select flag raises exactly its own counter', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.select(1, search: true);
|
||||
expect(logic.tab, 1);
|
||||
expect(logic.searchRequest, 1);
|
||||
expect(logic.sectionRequest, 0);
|
||||
|
||||
logic.select(2, mySection: 0);
|
||||
expect(logic.mySection, 0);
|
||||
expect(logic.sectionRequest, 1);
|
||||
expect(logic.searchRequest, 1);
|
||||
|
||||
logic.select(0, resume: true);
|
||||
expect(logic.resumeRequest, 1);
|
||||
expect(logic.sectionRequest, 1);
|
||||
|
||||
expect(logic.backRequest, 0);
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('the watchlist shortcut defaults to the second section', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.select(2, watchlist: true);
|
||||
|
||||
expect(logic.tab, 2);
|
||||
expect(logic.mySection, 1);
|
||||
expect(logic.sectionRequest, 1);
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('openProfile and openLibrary disagree about the profile flag', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.openProfile();
|
||||
expect(logic.profile, isTrue);
|
||||
expect(logic.tab, 2);
|
||||
expect(logic.mySection, 0);
|
||||
expect(logic.sectionRequest, 1);
|
||||
|
||||
logic.openLibrary(section: 1);
|
||||
expect(logic.profile, isFalse);
|
||||
expect(logic.mySection, 1);
|
||||
expect(logic.sectionRequest, 2);
|
||||
|
||||
logic.openLibrary();
|
||||
expect(logic.profile, isFalse);
|
||||
expect(logic.mySection, 1);
|
||||
expect(logic.sectionRequest, 3);
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('system back returns home, and home itself opens the drawer', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.onSystemBack();
|
||||
expect(logic.drawerOpen, isTrue);
|
||||
expect(logic.tab, 1);
|
||||
expect(logic.backRequest, 0);
|
||||
logic.onDrawerChanged(false);
|
||||
|
||||
logic.select(0);
|
||||
logic.onSystemBack();
|
||||
expect(logic.tab, 1);
|
||||
expect(logic.backRequest, 0);
|
||||
|
||||
logic.select(0);
|
||||
logic.onImmersiveChanged(true);
|
||||
logic.onSystemBack();
|
||||
expect(logic.backRequest, 1);
|
||||
expect(logic.tab, 0);
|
||||
|
||||
logic.onDrawerChanged(true);
|
||||
logic.onSystemBack();
|
||||
expect(logic.backRequest, 1);
|
||||
|
||||
logic.onDrawerChanged(false);
|
||||
logic.onImmersiveChanged(false);
|
||||
logic.onSystemBack();
|
||||
expect(logic.tab, 1);
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('drawer state only notifies on an actual change', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
var notifications = 0;
|
||||
logic.addListener(() => notifications++);
|
||||
|
||||
logic.onDrawerChanged(false);
|
||||
expect(notifications, 0);
|
||||
|
||||
logic.onDrawerChanged(true);
|
||||
expect(logic.drawerOpen, isTrue);
|
||||
expect(notifications, 1);
|
||||
|
||||
logic.onDrawerChanged(true);
|
||||
expect(notifications, 1);
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('drama and immersive callbacks update state', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.onDramaChanged(dramas[1]);
|
||||
expect(logic.currentDrama.id, 'city');
|
||||
|
||||
logic.onImmersiveChanged(true);
|
||||
expect(logic.videoImmersive, isTrue);
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('my page actions map onto root tabs', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.myAction(MyPageAction.discover);
|
||||
expect(logic.tab, 1);
|
||||
expect(logic.searchRequest, 1);
|
||||
|
||||
logic.myAction(MyPageAction.resume);
|
||||
expect(logic.tab, 0);
|
||||
expect(logic.resumeRequest, 1);
|
||||
|
||||
logic.myAction(MyPageAction.home);
|
||||
expect(logic.tab, 0);
|
||||
expect(logic.resumeRequest, 1);
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('notifying after dispose is a no-op', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
var notifications = 0;
|
||||
logic.addListener(() => notifications++);
|
||||
|
||||
logic.dispose();
|
||||
logic.notify();
|
||||
|
||||
expect(notifications, 0);
|
||||
});
|
||||
|
||||
testWidgets('discover actions map onto root tabs', (tester) async {
|
||||
final logic = HomeLogic(dramas);
|
||||
late BuildContext context;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (inner) {
|
||||
context = inner;
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
logic.discoverAction(context, DiscoverPageAction.watchlist);
|
||||
expect(logic.tab, 2);
|
||||
expect(logic.profile, isFalse);
|
||||
expect(logic.mySection, 1);
|
||||
expect(logic.sectionRequest, 1);
|
||||
|
||||
logic.discoverAction(context, DiscoverPageAction.my);
|
||||
expect(logic.tab, 2);
|
||||
expect(logic.profile, isFalse);
|
||||
|
||||
logic.discoverAction(context, DiscoverPageAction.resume);
|
||||
expect(logic.tab, 0);
|
||||
expect(logic.resumeRequest, 1);
|
||||
|
||||
logic.discoverAction(context, DiscoverPageAction.home);
|
||||
expect(logic.tab, 0);
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
logic.dispose();
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user