feat(auth): 登录页改为 ymb 品牌并支持后台配置背景图

This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-30 17:14:30 +08:00
1 parent 22cc669bbe
commit e7399fa3fd
24 files changed
+378 -188

No files matched your search

+5
View File
@@ -8,12 +8,14 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p
import 'package:fruitshorts/core/data/remote/api_client.dart';
import 'package:fruitshorts/core/data/remote/auth_api.dart';
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
import 'package:fruitshorts/core/data/services/auth_controller.dart';
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
import 'package:fruitshorts/core/platform/haptics.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/auth/auth_view.dart';
import '../support/fake_app_config.dart';
import '../support/fake_haptics.dart';
import '../support/fake_http_adapter.dart';
@@ -71,6 +73,9 @@ Future<void> _pumpAuth(WidgetTester tester) async {
MultiProvider(
providers: [
ChangeNotifierProvider<AuthController>.value(value: auth),
ChangeNotifierProvider<AppConfigController>.value(
value: await fakeAppConfigController(),
),
Provider<Haptics>.value(value: FakeHaptics()),
],
child: MaterialApp(
+5
View File
@@ -8,12 +8,14 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p
import 'package:fruitshorts/core/data/remote/api_client.dart';
import 'package:fruitshorts/core/data/remote/auth_api.dart';
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
import 'package:fruitshorts/core/data/services/auth_controller.dart';
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
import 'package:fruitshorts/core/platform/haptics.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/auth/auth_view.dart';
import '../support/fake_app_config.dart';
import '../support/fake_haptics.dart';
import '../support/fake_http_adapter.dart';
@@ -36,6 +38,9 @@ void main() {
MultiProvider(
providers: [
ChangeNotifierProvider<AuthController>.value(value: auth),
ChangeNotifierProvider<AppConfigController>.value(
value: await fakeAppConfigController(),
),
Provider<Haptics>.value(value: FakeHaptics()),
],
child: MaterialApp(
+12 -7
View File
@@ -11,12 +11,14 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p
import 'package:fruitshorts/core/data/models/auth_session.dart';
import 'package:fruitshorts/core/data/remote/auth_api.dart';
import 'package:fruitshorts/core/data/remote/api_client.dart';
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
import 'package:fruitshorts/core/data/services/auth_controller.dart';
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
import 'package:fruitshorts/core/platform/haptics.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/auth/auth_view.dart';
import '../support/fake_app_config.dart';
import '../support/fake_http_adapter.dart';
import '../support/fake_haptics.dart';
@@ -29,17 +31,17 @@ void main() {
// 不打桩这个通道调用会一直等不到回复(提交按钮转圈、pumpAndSettle 超时)。
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.stormg.lumadrama/device'),
(call) async => call.method == 'region' ? 'ID' : null,
);
const MethodChannel('com.stormg.lumadrama/device'),
(call) async => call.method == 'region' ? 'ID' : null,
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.stormg.lumadrama/device'),
null,
);
const MethodChannel('com.stormg.lumadrama/device'),
null,
);
});
Future<void> pumpAuthPage(
@@ -65,6 +67,9 @@ void main() {
MultiProvider(
providers: [
ChangeNotifierProvider<AuthController>.value(value: auth),
ChangeNotifierProvider<AppConfigController>.value(
value: await fakeAppConfigController(),
),
Provider<Haptics>.value(value: haptics ?? FakeHaptics()),
],
child: MaterialApp(
@@ -212,7 +217,7 @@ void main() {
) async {
await pumpAuthPage(tester);
expect(find.text('FruitShorts'), findsOneWidget);
expect(find.text('ymb'), findsNothing);
expect(find.text('Short dramas made for Indonesia'), findsOneWidget);
final email = tester.widget<TextField>(find.byKey(const Key('auth-email')));
final password = tester.widget<TextField>(
+59 -33
View File
@@ -9,13 +9,16 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p
import 'package:fruitshorts/core/data/models/auth_session.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/app_config_controller.dart';
import 'package:fruitshorts/core/data/services/auth_controller.dart';
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
import 'package:fruitshorts/core/platform/haptics.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/core/widgets/remote_image.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/auth/auth_view.dart';
import '../support/fake_app_config.dart';
import '../support/fake_haptics.dart';
import '../support/fake_http_adapter.dart';
@@ -29,6 +32,7 @@ void main() {
WidgetTester tester, {
AuthApi? api,
FakeHaptics? haptics,
String loginBackgroundUrl = '',
bool openEmail = true,
bool asRoute = false,
}) async {
@@ -44,10 +48,14 @@ void main() {
),
AuthSessionStore(),
);
final appConfig = await fakeAppConfigController(
loginBackgroundUrl: loginBackgroundUrl,
);
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider<AuthController>.value(value: auth),
ChangeNotifierProvider<AppConfigController>.value(value: appConfig),
Provider<Haptics>.value(value: haptics ?? FakeHaptics()),
],
child: MaterialApp(
@@ -76,11 +84,8 @@ void main() {
await pumpAuthPage(tester, openEmail: false);
expect(find.byKey(const Key('auth-landing')), findsOneWidget);
expect(find.text('FruitShorts'), findsOneWidget);
final brand = tester.widget<Text>(find.text('FruitShorts'));
expect(brand.style?.fontSize, 26);
expect(brand.style?.fontWeight, FontWeight.w700);
expect(brand.style?.letterSpacing, 2);
// 落地页只留图标,不再显示品牌文字。
expect(find.text('ymb'), findsNothing);
expect(
tester.getSize(find.byKey(const Key('auth-brand-logo'))),
const Size(88, 88),
@@ -136,10 +141,7 @@ void main() {
final email = tester.widget<TextField>(
find.byKey(const Key('auth-email')),
);
expect(
email.decoration?.fillColor,
Colors.white.withValues(alpha: 0.05),
);
expect(email.decoration?.fillColor, Colors.white.withValues(alpha: 0.05));
expect(email.style?.color, AppColors.onSurface);
expect(email.style?.fontSize, 15);
expect(email.cursorColor, AppColors.brandRed);
@@ -182,37 +184,63 @@ void main() {
},
);
testWidgets('should_use_backend_login_background_when_configured', (
tester,
) async {
await pumpAuthPage(
tester,
openEmail: false,
loginBackgroundUrl: '/files/login_bg/bg.jpg',
);
final backdrop = tester.widget<RemoteImage>(
find.byKey(const Key('auth-poster-backdrop')),
);
expect(backdrop.path, '/files/login_bg/bg.jpg');
});
testWidgets(
'should_show_app_logo_lower_on_the_page_when_landing_opens',
'should_fall_back_to_bundled_login_background_when_not_configured',
(tester) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await pumpAuthPage(tester, openEmail: false);
final image = tester.widget<Image>(
find.descendant(
of: find.byKey(const Key('auth-brand-logo')),
matching: find.byType(Image),
),
final backdrop = tester.widget<RemoteImage>(
find.byKey(const Key('auth-poster-backdrop')),
);
expect(backdrop.path, isEmpty);
// 兜底图是我们塞给 RemoteImage 的 fallbackWidget,用 auth-poster-default 定位。
expect(
(image.image as AssetImage).assetName,
'assets/icon/fruitshorts.png',
);
// 新版落地页用三等分 Spacer 排版,Logo 居中且落在两个入口按钮上方。
final logo = find.byKey(const Key('auth-brand-logo'));
expect(tester.getCenter(logo).dx, closeTo(195, 0.1));
expect(
tester.getBottomLeft(logo).dy,
lessThan(
tester.getTopLeft(find.byKey(const Key('auth-entry-phone'))).dy,
),
find.byKey(const Key('auth-poster-default')),
findsOneWidget,
);
},
);
testWidgets('should_show_app_logo_lower_on_the_page_when_landing_opens', (
tester,
) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await pumpAuthPage(tester, openEmail: false);
final image = tester.widget<Image>(
find.descendant(
of: find.byKey(const Key('auth-brand-logo')),
matching: find.byType(Image),
),
);
expect((image.image as AssetImage).assetName, 'assets/icon/ymb.png');
// 新版落地页用三等分 Spacer 排版,Logo 居中且落在两个入口按钮上方。
final logo = find.byKey(const Key('auth-brand-logo'));
expect(tester.getCenter(logo).dx, closeTo(195, 0.1));
expect(
tester.getBottomLeft(logo).dy,
lessThan(tester.getTopLeft(find.byKey(const Key('auth-entry-phone'))).dy),
);
});
testWidgets('should_render_40px_back_button_when_page_is_pushed', (
tester,
) async {
@@ -335,9 +363,7 @@ void main() {
);
expect(
tester
.widget<GestureDetector>(
find.byKey(const Key('auth-mode-sign-up')),
)
.widget<GestureDetector>(find.byKey(const Key('auth-mode-sign-up')))
.onTap,
isNull,
);
+1
View File
@@ -74,6 +74,7 @@ void main() {
final prod = Uri.parse(ProdConfig.baseUrl);
expect(dev.scheme, 'http');
expect(dev.host, isNot(prod.host));
expect(dev.port, 8081);
expect(dev.path, isEmpty);
expect(prod.scheme, 'http');
+53
View File
@@ -0,0 +1,53 @@
// Author: fruit.com.cn
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/remote/api_client.dart';
import 'package:fruitshorts/core/data/remote/config_api.dart';
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
import '../support/fake_http_adapter.dart';
AppConfigController _controller(FakeHttpAdapter adapter) => AppConfigController(
ConfigApi(ApiClient(baseUrl: 'http://127.0.0.1:1', adapter: adapter)),
);
void main() {
test('should_use_backend_url_when_login_background_is_configured', () async {
final controller = _controller(
FakeHttpAdapter(
(_) async => FakeHttpResponse(
'{"code":0,"data":{"loginBackgroundUrl":"/files/login_bg/bg.jpg"}}',
),
),
);
await controller.load();
expect(controller.loginBackgroundUrl, '/files/login_bg/bg.jpg');
});
test('should_treat_blank_value_as_not_configured', () async {
final controller = _controller(
FakeHttpAdapter(
(_) async =>
FakeHttpResponse('{"code":0,"data":{"loginBackgroundUrl":" "}}'),
),
);
await controller.load();
expect(controller.loginBackgroundUrl, isEmpty);
});
test('should_keep_local_default_when_config_request_fails', () async {
var notified = 0;
final controller = _controller(
FakeHttpAdapter((_) async => FakeHttpResponse('{}', 500)),
)..addListener(() => notified++);
await controller.load();
expect(controller.loginBackgroundUrl, isEmpty);
expect(notified, 0);
});
}
+31
View File
@@ -0,0 +1,31 @@
// Author: fruit.com.cn
import 'package:fruitshorts/core/data/remote/api_client.dart';
import 'package:fruitshorts/core/data/remote/config_api.dart';
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
/// 直接 pump 登录页的测试需要 App 配置 Provider。
///
/// 只用内存里的结论、不发真实请求:widget 测试的 fake-async 下等待 dio
/// 的 HTTP 请求会挂住,所以这里换掉 [ConfigApi] 的实现。
Future<AppConfigController> fakeAppConfigController({
String loginBackgroundUrl = '',
}) async {
final controller = AppConfigController(
_FakeConfigApi(loginBackgroundUrl),
);
await controller.load();
return controller;
}
class _FakeConfigApi extends ConfigApi {
_FakeConfigApi(this.loginBackgroundUrl)
: super(ApiClient(baseUrl: 'http://127.0.0.1:1'));
final String loginBackgroundUrl;
@override
Future<Map<String, dynamic>> appConfig() async => {
'loginBackgroundUrl': loginBackgroundUrl,
};
}
+2 -2
View File
@@ -138,7 +138,7 @@ void main() {
await tester.tap(find.byKey(const Key('tab-mine')));
await tester.pumpAndSettle();
expect(find.text('Vicky sheng'), findsOneWidget);
expect(find.text('FruitShorts VIP'), findsOneWidget);
expect(find.text('VIP'), findsOneWidget);
expect(find.byKey(const Key('mine-language')), findsOneWidget);
expect(find.byKey(const Key('mine-wallet')), findsOneWidget);
@@ -265,7 +265,7 @@ void main() {
await tester.tap(find.byKey(const Key('player-rail-share')));
await tester.pumpAndSettle();
expect(sharedText, contains('City Lights & Velvet Nights'));
expect(sharedText, contains('FruitShorts'));
expect(sharedText, contains('ymb'));
expect(tester.takeException(), isNull);
});