feat: 实现起卦、设置与积分系统
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meeyao_qianwen/core/auth/session_store.dart';
|
||||
import 'package:meeyao_qianwen/data/network/api_client.dart';
|
||||
import 'package:meeyao_qianwen/data/storage/local_kv_store.dart';
|
||||
import 'package:meeyao_qianwen/features/auth/data/apis/auth_api.dart';
|
||||
import 'package:meeyao_qianwen/features/auth/data/repositories/auth_repository.dart';
|
||||
|
||||
class _FakeSessionStore extends SessionStore {
|
||||
_FakeSessionStore({this.refreshToken, this.throwOnGetRefreshToken = false})
|
||||
: super(LocalKvStore());
|
||||
|
||||
String? refreshToken;
|
||||
bool throwOnGetRefreshToken;
|
||||
|
||||
bool clearTokenCalled = false;
|
||||
bool clearRefreshTokenCalled = false;
|
||||
bool clearEmailCalled = false;
|
||||
|
||||
@override
|
||||
Future<String?> getRefreshToken() async {
|
||||
if (throwOnGetRefreshToken) {
|
||||
throw Exception('read refresh token failed');
|
||||
}
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearToken() async {
|
||||
clearTokenCalled = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearRefreshToken() async {
|
||||
clearRefreshTokenCalled = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearEmail() async {
|
||||
clearEmailCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeAuthApi extends AuthApi {
|
||||
_FakeAuthApi()
|
||||
: super(apiClient: ApiClient(baseUrl: 'http://127.0.0.1:5775'));
|
||||
|
||||
bool deleteSessionCalled = false;
|
||||
bool throwOnDeleteSession = false;
|
||||
|
||||
@override
|
||||
Future<void> deleteSession({required String refreshToken}) async {
|
||||
deleteSessionCalled = true;
|
||||
if (throwOnDeleteSession) {
|
||||
throw Exception('delete session failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'logout should clear local session when getRefreshToken throws',
|
||||
() async {
|
||||
final authApi = _FakeAuthApi();
|
||||
final sessionStore = _FakeSessionStore(throwOnGetRefreshToken: true);
|
||||
final repository = AuthRepositoryImpl(
|
||||
authApi: authApi,
|
||||
sessionStore: sessionStore,
|
||||
);
|
||||
|
||||
await expectLater(repository.logout(), throwsA(isA<Exception>()));
|
||||
|
||||
expect(authApi.deleteSessionCalled, isFalse);
|
||||
expect(sessionStore.clearTokenCalled, isTrue);
|
||||
expect(sessionStore.clearRefreshTokenCalled, isTrue);
|
||||
expect(sessionStore.clearEmailCalled, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'logout should skip deleteSession when refresh token is empty',
|
||||
() async {
|
||||
final authApi = _FakeAuthApi();
|
||||
final sessionStore = _FakeSessionStore(refreshToken: '');
|
||||
final repository = AuthRepositoryImpl(
|
||||
authApi: authApi,
|
||||
sessionStore: sessionStore,
|
||||
);
|
||||
|
||||
await repository.logout();
|
||||
|
||||
expect(authApi.deleteSessionCalled, isFalse);
|
||||
expect(sessionStore.clearTokenCalled, isTrue);
|
||||
expect(sessionStore.clearRefreshTokenCalled, isTrue);
|
||||
expect(sessionStore.clearEmailCalled, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'logout should still clear local session when deleteSession throws',
|
||||
() async {
|
||||
final authApi = _FakeAuthApi()..throwOnDeleteSession = true;
|
||||
final sessionStore = _FakeSessionStore(refreshToken: 'r1');
|
||||
final repository = AuthRepositoryImpl(
|
||||
authApi: authApi,
|
||||
sessionStore: sessionStore,
|
||||
);
|
||||
|
||||
await expectLater(repository.logout(), throwsA(isA<Exception>()));
|
||||
|
||||
expect(authApi.deleteSessionCalled, isTrue);
|
||||
expect(sessionStore.clearTokenCalled, isTrue);
|
||||
expect(sessionStore.clearRefreshTokenCalled, isTrue);
|
||||
expect(sessionStore.clearEmailCalled, isTrue);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
|
||||
|
||||
void main() {
|
||||
test('mock data contains valid defaults', () {
|
||||
final params = DivinationMockData.initial();
|
||||
|
||||
expect(params.method, DivinationMethod.manual);
|
||||
expect(params.questionType, QuestionType.career);
|
||||
expect(params.coinBalance, greaterThan(0));
|
||||
expect(params.userId, isNotEmpty);
|
||||
});
|
||||
|
||||
test('toPayload returns normalized payload map', () {
|
||||
final params = DivinationParams(
|
||||
method: DivinationMethod.auto,
|
||||
questionType: QuestionType.health,
|
||||
question: '最近体检是否顺利',
|
||||
divinationTime: DateTime(2026, 4, 3, 10, 30),
|
||||
coinBalance: 6,
|
||||
userId: 'mock_2',
|
||||
);
|
||||
|
||||
final payload = params.toPayload();
|
||||
expect(payload['method'], 'auto');
|
||||
expect(payload['questionType'], 'health');
|
||||
expect(payload['question'], '最近体检是否顺利');
|
||||
expect(payload['coinBalance'], 6);
|
||||
expect(payload['userId'], 'mock_2');
|
||||
});
|
||||
|
||||
test('toBinary and toChangedBinary mappings are correct', () {
|
||||
final params = DivinationMockData.initial();
|
||||
final states = <YaoType>[
|
||||
YaoType.oldYin,
|
||||
YaoType.youngYang,
|
||||
YaoType.youngYin,
|
||||
YaoType.oldYang,
|
||||
YaoType.youngYang,
|
||||
YaoType.oldYin,
|
||||
];
|
||||
|
||||
expect(params.toBinary(states), '010110');
|
||||
expect(params.toChangedBinary(states), '110011');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/data/services/divination_result_builder.dart';
|
||||
|
||||
void main() {
|
||||
final builder = DivinationResultBuilder();
|
||||
|
||||
test('build returns result with hexagram names and section text', () {
|
||||
final params = DivinationMockData.initial().copyWith(
|
||||
method: DivinationMethod.auto,
|
||||
question: '近期工作是否会有突破',
|
||||
questionType: QuestionType.career,
|
||||
);
|
||||
|
||||
final result = builder.build(
|
||||
params: params,
|
||||
yaoStates: const [
|
||||
YaoType.youngYang,
|
||||
YaoType.youngYin,
|
||||
YaoType.oldYang,
|
||||
YaoType.youngYin,
|
||||
YaoType.oldYin,
|
||||
YaoType.youngYang,
|
||||
],
|
||||
);
|
||||
|
||||
expect(result.guaName, isNotEmpty);
|
||||
expect(result.targetGuaName, isNotEmpty);
|
||||
expect(result.binaryCode, hasLength(6));
|
||||
expect(result.changedBinaryCode, hasLength(6));
|
||||
expect(result.keywords, contains('签'));
|
||||
expect(result.conclusion, contains('这个卦象的结果为'));
|
||||
expect(result.yaoLines.length, 6);
|
||||
expect(result.targetYaoLines.length, 6);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meeyao_qianwen/app/app_theme.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/data/services/divination_result_builder.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/presentation/screens/divination_result_screen.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('result screen shows key sections', (tester) async {
|
||||
final params = DivinationMockData.initial().copyWith(
|
||||
method: DivinationMethod.auto,
|
||||
questionType: QuestionType.health,
|
||||
question: '近期状态是否平稳',
|
||||
);
|
||||
final data = DivinationResultBuilder().build(
|
||||
params: params,
|
||||
yaoStates: const [
|
||||
YaoType.oldYin,
|
||||
YaoType.youngYang,
|
||||
YaoType.youngYin,
|
||||
YaoType.oldYang,
|
||||
YaoType.youngYang,
|
||||
YaoType.oldYin,
|
||||
],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: DivinationResultScreen(data: data),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.text('天机推演中'), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 450));
|
||||
expect(find.text('正在解卦'), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 850));
|
||||
expect(find.text('解卦完成\n点击查看'), findsOneWidget);
|
||||
|
||||
expect(find.text('解卦结果'), findsOneWidget);
|
||||
expect(find.text('AI解卦'), findsOneWidget);
|
||||
expect(find.text('基础信息'), findsOneWidget);
|
||||
expect(find.text('卦象详情'), findsOneWidget);
|
||||
expect(find.text('解卦结论'), findsOneWidget);
|
||||
expect(find.text('○ 老阳(变)'), findsOneWidget);
|
||||
expect(find.text('× 老阴(变)'), findsOneWidget);
|
||||
expect(find.text('○'), findsWidgets);
|
||||
expect(find.text('×'), findsWidgets);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meeyao_qianwen/app/app_theme.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/presentation/screens/auto_divination_screen.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/presentation/screens/divination_screen.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/presentation/screens/manual_divination_screen.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('divination screen navigates to auto screen', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(theme: AppTheme.light(), home: const DivinationScreen()),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('自动起卦'));
|
||||
await tester.enterText(find.byType(TextField), '最近事业发展是否顺利');
|
||||
await tester.tap(find.text('开始起卦'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AutoDivinationScreen), findsOneWidget);
|
||||
expect(find.text('○ 老阳(变)'), findsOneWidget);
|
||||
expect(find.text('× 老阴(变)'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('auto screen keeps resolve button disabled initially', (
|
||||
tester,
|
||||
) async {
|
||||
final params = DivinationMockData.initial().copyWith(
|
||||
method: DivinationMethod.auto,
|
||||
question: '测试问题',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: AutoDivinationScreen(params: params),
|
||||
),
|
||||
);
|
||||
|
||||
final resolveButton = tester.widget<FilledButton>(
|
||||
find.widgetWithText(FilledButton, '开始解卦'),
|
||||
);
|
||||
|
||||
expect(resolveButton.onPressed, isNull);
|
||||
expect(find.text('您还需摇 6 次'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('divination screen navigates to manual screen by default', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(theme: AppTheme.light(), home: const DivinationScreen()),
|
||||
);
|
||||
|
||||
await tester.enterText(find.byType(TextField), '近期感情是否稳定');
|
||||
await tester.tap(find.text('开始起卦'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(find.byType(ManualDivinationScreen), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meeyao_qianwen/app/app_theme.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/presentation/screens/manual_divination_screen.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('manual screen shows yao legend', (tester) async {
|
||||
final params = DivinationMockData.initial().copyWith(
|
||||
method: DivinationMethod.manual,
|
||||
question: '测试问题',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: ManualDivinationScreen(params: params),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('○ 老阳(变)'), findsOneWidget);
|
||||
expect(find.text('× 老阴(变)'), findsOneWidget);
|
||||
expect(find.text('初爻'), findsOneWidget);
|
||||
expect(find.text('上爻'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meeyao_qianwen/core/auth/session_store.dart';
|
||||
import 'package:meeyao_qianwen/app/app_theme.dart';
|
||||
import 'package:meeyao_qianwen/data/storage/local_kv_store.dart';
|
||||
import 'package:meeyao_qianwen/features/home/presentation/screens/home_screen.dart';
|
||||
import 'package:meeyao_qianwen/l10n/app_localizations.dart';
|
||||
|
||||
class _FakeSessionStore extends SessionStore {
|
||||
_FakeSessionStore({required this.hasReadWelcomeValue})
|
||||
: super(LocalKvStore());
|
||||
|
||||
bool hasReadWelcomeValue;
|
||||
bool setWelcomeReadCalled = false;
|
||||
|
||||
@override
|
||||
Future<bool> hasReadWelcome() async {
|
||||
return hasReadWelcomeValue;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setWelcomeRead(bool value) async {
|
||||
setWelcomeReadCalled = value;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('history cards should use full available width', (tester) async {
|
||||
final sessionStore = _FakeSessionStore(hasReadWelcomeValue: true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: HomeScreen(
|
||||
account: 'user@example.com',
|
||||
sessionStore: sessionStore,
|
||||
onLogout: () async {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final historyCard = find.byType(Card).first;
|
||||
final cardWidth = tester.getSize(historyCard).width;
|
||||
final viewportWidth =
|
||||
tester.view.physicalSize.width / tester.view.devicePixelRatio;
|
||||
|
||||
expect(cardWidth, viewportWidth);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
|
||||
import 'package:meeyao_qianwen/shared/widgets/divination/yao_glyph.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('youngYang renders one solid segment', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(body: YaoGlyph(type: YaoType.youngYang)),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('yao_glyph_solid')), findsOneWidget);
|
||||
expect(find.byKey(const Key('yao_glyph_split_left')), findsNothing);
|
||||
expect(find.byKey(const Key('yao_glyph_split_right')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('youngYin renders two split segments', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(body: YaoGlyph(type: YaoType.youngYin)),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('yao_glyph_solid')), findsNothing);
|
||||
expect(find.byKey(const Key('yao_glyph_split_left')), findsOneWidget);
|
||||
expect(find.byKey(const Key('yao_glyph_split_right')), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meeyao_qianwen/shared/widgets/divination/yao_legend.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('legend shows yang yin and changing symbols', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: Scaffold(body: YaoLegend())),
|
||||
);
|
||||
|
||||
expect(find.text('— 阳'), findsOneWidget);
|
||||
expect(find.text('-- 阴'), findsOneWidget);
|
||||
expect(find.text('○ 老阳(变)'), findsOneWidget);
|
||||
expect(find.text('× 老阴(变)'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
|
||||
import 'package:meeyao_qianwen/shared/widgets/divination/yao_line_row.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('oldYang shows circle mark when enabled', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: YaoLineRow(
|
||||
name: '初爻',
|
||||
type: YaoType.oldYang,
|
||||
showChangeMark: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('初爻'), findsOneWidget);
|
||||
expect(find.text('○'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('oldYin does not show mark when showChangeMark=false', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: YaoLineRow(
|
||||
name: '二爻',
|
||||
type: YaoType.oldYin,
|
||||
showChangeMark: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('×'), findsNothing);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user