feat: 风险页新增数值修改、一键跑图与万花筒专区

- 修改总 Rating:只写 playerRating 与 userRating.rating,钳 0~99999,
  每首歌各自的 ratingList 原样带回
- 添加舞里程(UserDetail.point):完整 int32 区间,支持累加与直接设定,
  允许负值即扣减
- 一键跑图:isClear/isComplete 在客户端是从 distance 派生的,单发 flag 会被
  冲掉,故把 distance 推过 End 里程针;unlockFlag 语义是反的
- 万花筒专区:发现门与获取钥匙落在 userKaleidxScopeList 同一行的两个布尔位,
  钥匙不走 userItemList;上行是整行替换,所以先读回原行再叠加
- 旅行伙伴:修正误导性的示例 ID 1001(合法是三位数,表里没有的 ID 服务器照收
  但客户端静默跳过),编组槽位可设定等级,上传后回读复查以区分失败原因
- 补 MIT 许可证
This commit is contained in:
Cookiues committed 2026-09-27 01:49:55 +08:00
1 parent 329845f228
commit 0808e367f7
15 files changed
+3597 -42

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ichino/config/strings.dart';
import 'package:ichino/config/title_server_config.dart';
import 'package:ichino/pages/kaleidx_scope_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<void> _pumpPage(WidgetTester tester, {required bool loggedIn}) async {
SharedPreferences.setMockInitialValues({});
await TitleServerConfigHolder().update(
const TitleServerConfig(
titleServerUrl: 'http://127.0.0.1:9999',
aesKey: '0123456789abcdef',
aesIv: '0123456789abcdef',
clientId: 'A63E01C2805',
),
);
tester.view.physicalSize = const Size(420, 1700);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
home: KaleidxScopePage(
userId: 42,
cookies: 'JSESSIONID=X',
loginDateTime: loggedIn ? 1 : null,
loginId: loggedIn ? 7 : null,
onExitToTitle: () async {},
),
),
);
await tester.pump();
}
Finder _runFinder(WidgetTester tester) => find.ancestor(
of: find.text(AppStrings.kaleidxRun),
matching: find.byType(FilledButton),
);
void main() {
testWidgets('渲染门 ID 输入与两个动作勾选,且无溢出', (tester) async {
await _pumpPage(tester, loggedIn: true);
expect(tester.takeException(), isNull);
expect(find.text(AppStrings.kaleidxGateIdLabel), findsOneWidget);
expect(find.text(AppStrings.kaleidxActionDiscover), findsOneWidget);
expect(find.text(AppStrings.kaleidxActionKey), findsOneWidget);
// 没勾钥匙时不占版面。
expect(find.text(AppStrings.kaleidxKeyNeedsGate), findsNothing);
});
testWidgets('勾了钥匙才解释「钥匙离不开门」', (tester) async {
await _pumpPage(tester, loggedIn: true);
await tester.tap(find.text(AppStrings.kaleidxActionKey));
await tester.pump();
expect(find.text(AppStrings.kaleidxKeyNeedsGate), findsOneWidget);
});
testWidgets('添加门 ID 后出现在列表并带状态', (tester) async {
await _pumpPage(tester, loggedIn: true);
await tester.enterText(find.byType(TextField), '3');
await tester.tap(find.text(AppStrings.listAddButton));
await tester.pump();
expect(find.textContaining('#3'), findsOneWidget);
expect(find.text(AppStrings.kaleidxPendingEmpty), findsNothing);
});
testWidgets('两个动作都不勾时被拦下', (tester) async {
await _pumpPage(tester, loggedIn: true);
await tester.enterText(find.byType(TextField), '3');
await tester.tap(find.text(AppStrings.listAddButton));
await tester.pump();
await tester.tap(find.text(AppStrings.kaleidxActionDiscover));
await tester.pump();
await tester.tap(_runFinder(tester));
await tester.pump();
expect(find.text(AppStrings.kaleidxNeedAction), findsOneWidget);
});
testWidgets('列表为空时执行被拦下', (tester) async {
await _pumpPage(tester, loggedIn: true);
await tester.tap(_runFinder(tester));
await tester.pump();
expect(find.text(AppStrings.kaleidxNeedGateId), findsOneWidget);
});
testWidgets('未登录时禁用执行', (tester) async {
await _pumpPage(tester, loggedIn: false);
expect(tester.takeException(), isNull);
expect(find.text(AppStrings.kaleidxNotLoggedIn), findsOneWidget);
final run = tester.widget<FilledButton>(_runFinder(tester));
expect(run.onPressed, isNull);
});
}
+88
View File
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ichino/config/strings.dart';
import 'package:ichino/config/title_server_config.dart';
import 'package:ichino/pages/map_traverse_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<void> _pumpPage(WidgetTester tester, {required bool loggedIn}) async {
SharedPreferences.setMockInitialValues({});
await TitleServerConfigHolder().update(
const TitleServerConfig(
titleServerUrl: 'http://127.0.0.1:9999',
aesKey: '0123456789abcdef',
aesIv: '0123456789abcdef',
clientId: 'A63E01C2805',
),
);
tester.view.physicalSize = const Size(420, 1600);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
home: MapTraversePage(
userId: 42,
cookies: 'JSESSIONID=X',
loginDateTime: loggedIn ? 1 : null,
loginId: loggedIn ? 7 : null,
onExitToTitle: () async {},
),
),
);
await tester.pump();
}
Finder _runFinder(WidgetTester tester) => find.ancestor(
of: find.text(AppStrings.mapRun),
matching: find.byType(FilledButton),
);
void main() {
testWidgets('渲染区域 ID 输入与收藏品限制说明,且无溢出', (tester) async {
await _pumpPage(tester, loggedIn: true);
expect(tester.takeException(), isNull);
expect(find.text(AppStrings.mapIdLabel), findsOneWidget);
expect(find.text(AppStrings.mapCollectiblesNotice), findsOneWidget);
expect(find.text(AppStrings.mapNoData), findsOneWidget);
});
testWidgets('添加区域 ID 后进入待处理列表,重复添加被拒', (tester) async {
await _pumpPage(tester, loggedIn: true);
await tester.enterText(find.byType(TextField), '550001');
await tester.tap(find.text(AppStrings.listAddButton));
await tester.pump();
expect(find.text('#550001'), findsOneWidget);
expect(find.text(AppStrings.mapNeedMapId), findsNothing);
await tester.enterText(find.byType(TextField), '550001');
await tester.tap(find.text(AppStrings.listAddButton));
await tester.pump();
expect(find.text(AppStrings.listDuplicate), findsOneWidget);
expect(find.text('#550001'), findsOneWidget);
});
testWidgets('列表为空时执行被拦下', (tester) async {
await _pumpPage(tester, loggedIn: true);
await tester.tap(_runFinder(tester));
await tester.pump();
expect(find.text(AppStrings.mapNeedMapId), findsOneWidget);
});
testWidgets('未登录时禁用执行', (tester) async {
await _pumpPage(tester, loggedIn: false);
expect(tester.takeException(), isNull);
expect(find.text(AppStrings.mapNotLoggedIn), findsOneWidget);
final run = tester.widget<FilledButton>(_runFinder(tester));
expect(run.onPressed, isNull);
});
}
+142
View File
@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ichino/config/strings.dart';
import 'package:ichino/config/title_server_config.dart';
import 'package:ichino/pages/number_patch_page.dart';
import 'package:ichino/services/user_all_payload_builder.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<void> _pumpPage(
WidgetTester tester,
NumberPatchMode mode, {
bool loggedIn = true,
}) async {
SharedPreferences.setMockInitialValues({});
await TitleServerConfigHolder().update(
const TitleServerConfig(
titleServerUrl: 'http://127.0.0.1:9999',
aesKey: '0123456789abcdef',
aesIv: '0123456789abcdef',
clientId: 'A63E01C2805',
),
);
tester.view.physicalSize = const Size(420, 1500);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
home: NumberPatchPage(
userId: 42,
cookies: 'JSESSIONID=X',
loginDateTime: loggedIn ? 1 : null,
loginId: loggedIn ? 7 : null,
onExitToTitle: () async {},
mode: mode,
),
),
);
await tester.pump();
}
Finder _runFinder(WidgetTester tester) => find.ancestor(
of: find.text(AppStrings.numberPatchRun),
matching: find.byType(FilledButton),
);
FilledButton _runButton(WidgetTester tester) =>
tester.widget<FilledButton>(_runFinder(tester));
void main() {
testWidgets('Rating 模式渲染总 Rating 输入框且可执行,无溢出', (tester) async {
await _pumpPage(tester, NumberPatchMode.rating);
expect(tester.takeException(), isNull);
// 标题在 AppBar 与说明卡各出现一次。
expect(find.text(AppStrings.ratingFeatureTitle), findsNWidgets(2));
expect(find.text(AppStrings.ratingFeatureDesc), findsOneWidget);
expect(find.text(AppStrings.ratingValueLabel), findsOneWidget);
// 舞里程才有的「累加 / 直接设定」选择器不该出现。
expect(find.text(AppStrings.maiMileModeLabel), findsNothing);
expect(_runButton(tester).onPressed, isNotNull);
});
testWidgets('舞里程模式提供累加与设定两种方式', (tester) async {
await _pumpPage(tester, NumberPatchMode.maiMile);
expect(tester.takeException(), isNull);
expect(find.text(AppStrings.maiMileModeAdd), findsOneWidget);
expect(find.text(AppStrings.maiMileValueLabel), findsOneWidget);
// 累加要基于服务器上的当前余额,没拉取数据前不猜、不预览。
await tester.enterText(find.byType(TextField), '1000');
await tester.pump();
expect(find.textContaining('→'), findsNothing);
});
testWidgets('未登录时禁用执行', (tester) async {
await _pumpPage(tester, NumberPatchMode.rating, loggedIn: false);
expect(tester.takeException(), isNull);
expect(find.text(AppStrings.numberPatchNotLoggedIn), findsOneWidget);
expect(_runButton(tester).onPressed, isNull);
});
testWidgets('Rating 超过 99999 时被拦下', (tester) async {
await _pumpPage(tester, NumberPatchMode.rating);
await tester.enterText(
find.byType(TextField),
'${UserAllPayloadBuilder.maxRating + 1}',
);
await tester.pump();
await tester.tap(_runFinder(tester));
await tester.pump();
expect(
find.text(
AppStrings.numberPatchOutOfRange(
0,
UserAllPayloadBuilder.maxRating,
),
),
findsOneWidget,
);
});
testWidgets('舞里程模式接受 int32 两端的大数', (tester) async {
await _pumpPage(tester, NumberPatchMode.maiMile);
for (final value in [
UserAllPayloadBuilder.maxValue,
UserAllPayloadBuilder.minValue,
]) {
await tester.enterText(find.byType(TextField), '$value');
await tester.pump();
await tester.tap(_runFinder(tester));
await tester.pump();
// 不该报越界;会走到拉数据/发包那步,这里只断言没有越界提示。
expect(
find.text(
AppStrings.numberPatchOutOfRange(
UserAllPayloadBuilder.minValue,
UserAllPayloadBuilder.maxValue,
),
),
findsNothing,
reason: '$value 在 int32 区间内,不该被拦下',
);
}
});
testWidgets('空输入时给出填写提示', (tester) async {
await _pumpPage(tester, NumberPatchMode.maiMile);
await tester.tap(_runFinder(tester));
await tester.pump();
expect(find.text(AppStrings.maiMileNeedValue), findsOneWidget);
});
}
+394
View File
@@ -1,6 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:ichino/config/strings.dart';
import 'package:ichino/config/title_server_config.dart';
import 'package:ichino/models/user_character.dart';
import 'package:ichino/models/user_kaleidx_scope.dart';
import 'package:ichino/services/user_all_payload_builder.dart';
const _config = TitleServerConfig(
@@ -113,4 +115,396 @@ void main() {
expect(upsert['isNewItemList'], '11');
});
});
group('总 Rating patch', () {
test('只改 playerRating 与 userRating.rating,每首歌的 ratingList 不动', () {
final builder = UserAllPayloadBuilder(_config);
final packet = builder.build(
userId: 42,
loginId: 7,
loginDateTime: 1700000000,
musicData: const {'musicId': 11538, 'level': 0, 'achievement': 0},
generalUserInfo: const {
'GetUserDataApi': {
'userData': {
'playerRating': 12345,
'highestRating': 13000,
'gradeRating': 5000,
'charaSlot': [101],
},
},
'GetUserRatingApi': {
'userRating': {
'rating': 12345,
'ratingList': [
{'musicId': 834, 'level': 4, 'rating': 100},
],
'newRatingList': [
{'musicId': 11538, 'level': 3, 'rating': 200},
],
},
},
},
);
builder.applyRatingPatch(packet, rating: 99999);
final upsert = _upsert(packet);
final userData = (upsert['userData'] as List).first
as Map<String, dynamic>;
final userRating = (upsert['userRatingList'] as List).first
as Map<String, dynamic>;
expect(userData['playerRating'], 99999);
expect(userRating['rating'], 99999);
// 每首歌各自的 Rating 必须原样带回。
expect(userRating['ratingList'], [
{'musicId': 834, 'level': 4, 'rating': 100},
]);
expect(userRating['newRatingList'], [
{'musicId': 11538, 'level': 3, 'rating': 200},
]);
// 历史最高、段位 Rating 这些不属于「总 Rating」。
expect(userData['highestRating'], 13000);
expect(userData['gradeRating'], 5000);
final playlog = (packet['userPlaylogList'] as List).first
as Map<String, dynamic>;
expect(playlog['beforeRating'], 12345);
expect(playlog['afterRating'], 99999);
expect(playlog['beforeDeluxRating'], 12345);
expect(playlog['afterDeluxRating'], 99999);
});
test('Rating 超过 99999 被钳到上限,负数钳到 0', () {
final builder = UserAllPayloadBuilder(_config);
final packet = _buildPacket(builder);
builder.applyRatingPatch(
packet,
rating: UserAllPayloadBuilder.maxRating + 500,
);
final userData =
((_upsert(packet))['userData'] as List).first as Map<String, dynamic>;
expect(userData['playerRating'], UserAllPayloadBuilder.maxRating);
builder.applyRatingPatch(packet, rating: -5);
final clamped = (_upsert(packet)['userData'] as List).first
as Map<String, dynamic>;
expect(clamped['playerRating'], 0);
});
test('GetUserRatingApi 缺数据时不会写到 const 空 map 上崩掉', () {
final builder = UserAllPayloadBuilder(_config);
final packet = _buildPacket(builder);
expect(() => builder.applyRatingPatch(packet, rating: 5000), returnsNormally);
final userRating = (_upsert(packet))['userRatingList'] as List;
expect(userRating.first, {'rating': 5000});
});
});
group('舞里程 patch', () {
test('point 与 totalPoint 分别写入,totalPoint 省略时保持原值', () {
final builder = UserAllPayloadBuilder(_config);
final packet = builder.build(
userId: 42,
loginId: 7,
loginDateTime: 1700000000,
musicData: const {'musicId': 11538, 'level': 0, 'achievement': 0},
generalUserInfo: const {
'GetUserDataApi': {
'userData': {'point': 8000, 'totalPoint': 120000},
},
},
);
builder.applyMaiMilePatch(packet, point: 99999);
final userData =
(_upsert(packet))['userData'] as List;
final row = userData.first as Map<String, dynamic>;
expect(row['point'], 99999);
expect(row['totalPoint'], 120000);
});
test('累加越过 int32 上界时被钳住', () {
const current = 2147483000;
final builder = UserAllPayloadBuilder(_config);
final packet = builder.build(
userId: 42,
loginId: 7,
loginDateTime: 1700000000,
musicData: const {'musicId': 11538, 'level': 0, 'achievement': 0},
generalUserInfo: const {
'GetUserDataApi': {
'userData': {'point': current, 'totalPoint': current},
},
},
);
// 累加 2000 会越过 int32 上界,应被钳住。
builder.applyMaiMilePatch(
packet,
point: UserAllPayloadBuilder.clampValue(current + 2000),
totalPoint: UserAllPayloadBuilder.clampValue(current + 2000),
);
final row = ((_upsert(packet))['userData'] as List).first
as Map<String, dynamic>;
expect(row['point'], UserAllPayloadBuilder.maxValue);
expect(row['totalPoint'], UserAllPayloadBuilder.maxValue);
expect(UserAllPayloadBuilder.maxValue, 2147483647);
expect(UserAllPayloadBuilder.minValue, -2147483648);
});
test('负数舞里程原样写入,只有越过 int32 下界才钳', () {
final builder = UserAllPayloadBuilder(_config);
final packet = builder.build(
userId: 42,
loginId: 7,
loginDateTime: 1700000000,
musicData: const {'musicId': 11538, 'level': 0, 'achievement': 0},
generalUserInfo: const {
'GetUserDataApi': {
'userData': {'point': 5000, 'totalPoint': 5000},
},
},
);
// 累加模式下填负数就是扣里程,不该被当成非法值抬回 0。
builder.applyMaiMilePatch(packet, point: -7);
final row = (_upsert(packet)['userData'] as List).first
as Map<String, dynamic>;
expect(row['point'], -7);
});
test('clampValue 的两侧边界', () {
expect(UserAllPayloadBuilder.clampValue(0), 0);
expect(UserAllPayloadBuilder.clampValue(-7), -7);
expect(
UserAllPayloadBuilder.clampValue(UserAllPayloadBuilder.maxValue + 1),
UserAllPayloadBuilder.maxValue,
);
expect(
UserAllPayloadBuilder.clampValue(UserAllPayloadBuilder.minValue - 1),
UserAllPayloadBuilder.minValue,
);
});
});
group('旅行伙伴等级 patch', () {
test('新增行标 1、改等级行标 0,两段拼成 isNewCharacterList', () {
final builder = UserAllPayloadBuilder(_config);
final packet = _buildPacket(builder);
builder.applyCharacterListPatch(
packet,
characters: [
{'characterId': 101, 'level': 999999, 'awakening': 0, 'useCount': 0},
{'characterId': 102, 'level': 5000, 'awakening': 3, 'useCount': 77},
],
newFlags: '10',
);
final upsert = _upsert(packet);
expect(upsert['isNewCharacterList'], '10');
expect(upsert['userCharacterList'], hasLength(2));
});
test('省略 newFlags 时沿用全 1 的新增语义', () {
final builder = UserAllPayloadBuilder(_config);
final packet = _buildPacket(builder);
builder.applyCharacterListPatch(packet, characters: [
{'characterId': 101, 'level': 1, 'awakening': 0, 'useCount': 0},
]);
expect(_upsert(packet)['isNewCharacterList'], '1');
});
test('等级钳位:0 以下抬到 1,超过真实等级上限压到 999999', () {
expect(UserAllPayloadBuilder.clampCharacterLevel(0), 1);
expect(UserAllPayloadBuilder.clampCharacterLevel(-9), 1);
expect(UserAllPayloadBuilder.clampCharacterLevel(10000000), 999999);
expect(UserAllPayloadBuilder.clampCharacterLevel(999999), 999999);
});
});
group('一键跑图 userMapList patch', () {
test('完成行的字段与 Net.VO.Mai2.UserMap 一一对应', () {
final row = UserAllPayloadBuilder.completedMap(550001);
// unlockFlag 语义是反的:0 才表示已开启(IsFinishedOpening)。
expect(row.keys.toSet(), {
'mapId',
'distance',
'isLock',
'isClear',
'isComplete',
'unlockFlag',
});
expect(row['mapId'], 550001);
expect(row['isClear'], isTrue);
expect(row['isComplete'], isTrue);
expect(row['unlockFlag'], 0);
// distance 必须推过 End 针,否则客户端会用 distance 把 flag 冲回 false。
expect(row['distance'], UserAllPayloadBuilder.maxMapDistance);
});
test('isNewMapList 每位对应一行,服务器已有的行标 0', () {
final builder = UserAllPayloadBuilder(_config);
final packet = _buildPacket(builder);
builder.applyMapPatch(
packet,
maps: [
UserAllPayloadBuilder.completedMap(1),
UserAllPayloadBuilder.completedMap(2),
UserAllPayloadBuilder.completedMap(3),
],
newFlags: '010',
);
final upsert = _upsert(packet);
expect(upsert['userMapList'], hasLength(3));
expect(upsert['isNewMapList'], '010');
expect(upsert['isNewMapList'].length,
(upsert['userMapList'] as List).length);
});
test('可以单独抬高 distance 而不动其它字段', () {
final row = UserAllPayloadBuilder.completedMap(9, distance: 12345);
expect(row['distance'], 12345);
});
});
group('万花筒 userKaleidxScopeList patch', () {
test('wire 行的 15 个字段与 UserKaleidxScope 结构一致', () {
final row = const UserKaleidxScopeBean(gateId: 3).toWireJson();
// totalDeluxscore 的拼写是原样的 Deluxscore,写错服务器认不出。
expect(row.keys.toList(), [
'gateId',
'isGateFound',
'isKeyFound',
'isClear',
'totalRestLife',
'totalAchievement',
'totalDeluxscore',
'bestAchievement',
'bestDeluxscore',
'bestAchievementDate',
'bestDeluxscoreDate',
'playCount',
'clearDate',
'lastPlayDate',
'isInfoWatched',
]);
// 日期字段用 "" 而不是 null,对齐客户端 class 构造器的默认值。
expect(row['bestAchievementDate'], '');
expect(row['clearDate'], '');
});
test('isNewKaleidxScopeList 位数与行数一致,已有行标 0', () {
final builder = UserAllPayloadBuilder(_config);
final packet = _buildPacket(builder);
final scopes = [
const UserKaleidxScopeBean(gateId: 1, isGateFound: true),
const UserKaleidxScopeBean(gateId: 2, isGateFound: true),
];
builder.applyKaleidxScopePatch(
packet,
scopes: [for (final s in scopes) s.toWireJson()],
newFlags: '01',
);
final upsert = _upsert(packet);
expect(upsert['userKaleidxScopeList'], hasLength(2));
expect(upsert['isNewKaleidxScopeList'], '01');
expect(
(upsert['isNewKaleidxScopeList'] as String).length,
(upsert['userKaleidxScopeList'] as List).length,
);
});
test('新发现门:isGateFound=true,给钥匙时连带把门标为已发现', () {
final foundOnly = UserKaleidxScopeBean.discovered(
7,
giveKey: false,
).toWireJson();
expect(foundOnly['isGateFound'], isTrue);
expect(foundOnly['isKeyFound'], isFalse);
final withKey = UserKaleidxScopeBean.discovered(
7,
giveKey: true,
).toWireJson();
expect(withKey['isKeyFound'], isTrue);
expect(withKey['isGateFound'], isTrue);
});
test('已有行只叠加布尔位,成绩与日期不被清零', () {
final server = UserKaleidxScopeBean.fromJson(const {
'gateId': 4,
'isGateFound': true,
'isKeyFound': false,
'isClear': false,
'totalRestLife': 5,
'totalAchievement': 612345,
'totalDeluxscore': 987,
'bestAchievement': 1012345,
'bestDeluxscore': 55,
'bestAchievementDate': '2026-01-02 03:04:05.0',
'bestDeluxscoreDate': '2026-01-02 03:04:05.0',
'playCount': 12,
'clearDate': '',
'lastPlayDate': '2026-09-01 00:00:00.0',
'isInfoWatched': true,
});
final updated = server
.withActions(discover: false, giveKey: true)
.toWireJson();
expect(updated['isKeyFound'], isTrue);
expect(updated['isGateFound'], isTrue);
expect(updated['bestAchievement'], 1012345);
expect(updated['bestAchievementDate'], '2026-01-02 03:04:05.0');
expect(updated['playCount'], 12);
expect(updated['totalRestLife'], 5);
expect(updated['isInfoWatched'], isTrue);
expect(updated['lastPlayDate'], '2026-09-01 00:00:00.0');
});
test('withActions 不会把已经为 true 的位改回 false', () {
const cleared = UserKaleidxScopeBean(
gateId: 9,
isGateFound: true,
isKeyFound: true,
isClear: true,
);
final again = cleared.withActions(discover: true, giveKey: false);
expect(again.isGateFound, isTrue);
expect(again.isKeyFound, isTrue);
expect(again.isClear, isTrue);
});
test('状态文案覆盖四种组合,隐形态单独说明', () {
expect(AppStrings.kaleidxGateState(false, false, false), '未见过');
expect(AppStrings.kaleidxGateState(true, false, false), '已发现 · 未解锁');
expect(AppStrings.kaleidxGateState(true, true, false), '可挑战');
expect(AppStrings.kaleidxGateState(true, true, true), '已通关');
// !found && key 是隐形,不是「锁着的门」。
expect(
AppStrings.kaleidxGateState(false, true, false),
contains('隐形'),
);
});
});
}