Files
Ichino/test/best50_poster_test.dart
T
Cookiues 8dda399f76 feat: 旅行伙伴发放与编组、修复退登卡死、优化宽屏布局、加 CI
自动退出登录卡死:_performLogoutAndExit 只 pop() 一层,而解锁/收藏品页是 push
在 HomePage 之上的,退登后被弹回「风险」hub,同时 _loggingOut 仍为 true,
PopScope(canPop:false) + 返回键转圈形成死局。改为 popUntil(isFirst)。

旅行伙伴发不了:它是 ItemKind.Character = 9,与搭档 Partner = 10 是两种东西,
原先收藏品的类型列表 [1,2,3,10,11,12] 里根本没有 9。按客户端实现,角色不走
userItemList(ExportUserItems 不导出 itemKind 9),而是
upsertUserAll.userCharacterList 的 {characterId, level, awakening, useCount},
配 isNewCharacterList 一行一字符。新增发放与 5 槽 charaSlot 编组页,
含「把槽 0 复制到其他槽位」,并校验所选角色确为已拥有或本次待发放。

宽屏布局:各页只有 ConstrainedBox 限宽、没有居中容器,1440 窗口下内容贴左、
右侧大片空白。抽出 responsiveBody 统一居中;B50 预览改用按宽度适配
(InteractiveViewer constrained:false + FittedBox fitWidth),横屏下不再被
contain 缩成中间一小块,导出仍是原始 1762x1850。

CI:.github/workflows/build.yml 先跑 analyze+test 作为门槛,再产出
Windows exe、Android apk、iOS 未签名 ipa(macOS runner,无证书故不可安装)。
README 从 Flutter 模板改写为实际文档,含 path_provider/jni 与 JBR jlink
两个构建环境坑的说明。
2026-09-26 00:37:54 +08:00

192 lines
6.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ichino/config/strings.dart';
import 'package:ichino/models/user_rating.dart';
import 'package:ichino/pages/best50_page.dart';
import 'package:ichino/widgets/best50_poster.dart';
List<Best50CardData> _cards(int count, {String tag = 'a'}) => [
for (var i = 0; i < count; i++)
Best50CardData(
musicId: 1000 + i,
level: MusicLevel.values[i % MusicLevel.values.length],
title: '$tag#$i',
achievementText: '100.${(1000 + i).toString().padLeft(4, '0')}%',
comboStatus: i % 5,
syncStatus: i % 5,
ra: 50 + i,
rate: const [
'D', 'C', 'B', 'BB', 'BBB', 'A', 'AA',
'AAA', 'S', 'Sp', 'SS', 'SSp', 'SSS', 'SSSp',
][i % 14],
),
];
Future<void> _pumpPoster(
WidgetTester tester, {
required List<Best50CardData> best35,
required List<Best50CardData> best15,
}) async {
tester.view.physicalSize = const Size(1800, 1900);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Best50Poster(
userName: 'Test Player',
iconId: 0,
playerRating: 12345,
best35: best35,
best15: best15,
),
),
),
);
await tester.pump();
}
void main() {
group('卡片栅格坐标(对齐 Empurple)', () {
test('每行 5 张,列距 320、首列 65', () {
expect(Best50Poster.cardLeft(0), 65);
expect(Best50Poster.cardLeft(4), 65 + 320 * 4);
expect(Best50Poster.cardLeft(5), 65);
});
test('BEST35 区块固定从 350 起,每行下移 120', () {
expect(Best50Poster.cardTopBest35(0), 350);
expect(Best50Poster.cardTopBest35(4), 350);
expect(Best50Poster.cardTopBest35(5), 350 + 120);
expect(Best50Poster.cardTopBest35(34), 350 + 120 * 6);
});
test('BEST15 区块固定从 1250 起,与 BEST35 有多少条无关', () {
expect(Best50Poster.cardTopBest15(0), 1250);
expect(Best50Poster.cardTopBest15(5), 1250 + 120);
expect(Best50Poster.cardTopBest15(14), 1250 + 120 * 2);
});
test('满 50 张时最后一张不越出底图', () {
final right =
Best50Poster.cardLeft(Best50Poster.columnsPerRow - 1) +
Best50Poster.cardWidth;
final bottom =
Best50Poster.cardTopBest15(Best50Poster.maxBest15 - 1) +
Best50Poster.cardHeight;
expect(right, lessThanOrEqualTo(Best50Poster.canvasWidth));
expect(bottom, lessThanOrEqualTo(Best50Poster.canvasHeight));
});
});
group('Best50Poster 渲染', () {
testWidgets('满员 35+15 按底图尺寸渲染且无溢出', (tester) async {
await _pumpPoster(tester, best35: _cards(35), best15: _cards(15));
expect(tester.takeException(), isNull);
final size = tester.getSize(find.byType(Best50Poster));
expect(size.width, Best50Poster.canvasWidth);
expect(size.height, Best50Poster.canvasHeight);
});
testWidgets('条目不足时不报错', (tester) async {
await _pumpPoster(tester, best35: _cards(3), best15: const []);
expect(tester.takeException(), isNull);
});
testWidgets('BEST35 未打满时 BEST15 仍固定在第二区块', (tester) async {
// 回归用例:以前按 35+15 拼接后的全局序号算行,BEST35 不满 35 条时
// BEST15 会整体上移、混进上半区。
await _pumpPoster(tester, best35: _cards(3), best15: _cards(5, tag: 'b'));
expect(tester.takeException(), isNull);
final firstOf35 = tester.getTopLeft(find.text('a#0')).dy;
final lastOf35 = tester.getTopLeft(find.text('a#2')).dy;
final firstOf15 = tester.getTopLeft(find.text('b#0')).dy;
final lastOf15 = tester.getTopLeft(find.text('b#4')).dy;
expect(firstOf35, greaterThanOrEqualTo(Best50Poster.gridTopFirstBlock));
expect(lastOf35, lessThan(Best50Poster.gridTopSecondBlock));
expect(firstOf15, greaterThanOrEqualTo(Best50Poster.gridTopSecondBlock));
expect(lastOf15, greaterThanOrEqualTo(Best50Poster.gridTopSecondBlock));
});
testWidgets('窄视口里海报仍保持底图尺寸(FittedBox 预览)', (tester) async {
// 回归用例:InteractiveViewer 的 constrained 默认为 true,直接把固定尺寸的
// 海报当子节点会被压成视口大小——底图压扁、Positioned 的卡片掉出画布被裁掉。
// 中间垫一层 FittedBox 才是对的:视口拿到的是缩放后的显示,边界录到的仍是原尺寸。
tester.view.physicalSize = const Size(400, 900);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
children: [
Expanded(
child: InteractiveViewer(
constrained: false,
child: FittedBox(
fit: BoxFit.fitWidth,
child: Best50Poster(
userName: 'Test Player',
iconId: 0,
playerRating: 12345,
best35: _cards(35),
best15: _cards(15, tag: 'b'),
),
),
),
),
],
),
),
),
);
await tester.pump();
expect(tester.takeException(), isNull);
expect(
tester.getSize(find.byType(Best50Poster)),
const Size(
Best50Poster.canvasWidth,
Best50Poster.canvasHeight,
),
);
});
testWidgets('超出上限的条目被裁剪到 35+15', (tester) async {
await _pumpPoster(tester, best35: _cards(60), best15: _cards(40, tag: 'b'));
expect(tester.takeException(), isNull);
// BEST35 只保留前 35 条,BEST15 只保留前 15 条。
expect(find.text('a#34'), findsOneWidget);
expect(find.text('a#35'), findsNothing);
expect(find.text('b#14'), findsOneWidget);
expect(find.text('b#15'), findsNothing);
});
});
testWidgets('Best50Page 在未配置 Title Server 时给出提示', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Best50Page(
userId: 1,
userName: 'Nobody',
iconId: 0,
playerRating: 0,
),
),
),
);
await tester.pump();
expect(find.text(AppStrings.ticketNotConfigured), findsOneWidget);
expect(find.byIcon(Icons.refresh), findsOneWidget);
});
}