Files
photo-live/apps/mobile/test/pages/share_qr_page_test.dart
T
浅念 8741bded7f fix(app): 海报下载反馈改弹层内呈现——SnackBar 被模态弹层遮挡不可见
根因:保存成功/失败的 SnackBar 弹在弹层底下的页面 Scaffold 上,被模态
bottom sheet + 遮罩整层盖住,用户永远看不到(保存本身大概率成功)。

- 成功:下载按钮翻成功态(✓+已保存到相册,kColorSuccess)2.5s 自动复原
- 失败:按钮下方弹层内红字留置可重试(对齐隐藏照片弹层面板内提示模式)
- 删除被遮挡的 SnackBar 路径;复原 Timer 于 dispose 取消

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 11:25:49 +08:00

260 lines
9.7 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.
/// 相册分享页 widget 测试(UI-03 §4 页面 19)。
///
/// 覆盖:全屏页结构(标题/真实封面占位/名称/日期地点/二维码/扫码说明/
/// 可选中链接)、链接携带正确相册 token 与 `?lang=` 语言参数、Copy link
/// 主操作写剪贴板 + 已复制提示、Open in browser 次操作经注入替身启动器
/// 打开并退出页面、未公开相册显示「该链接目前无法公开浏览」琥珀提示
/// (公开相册不显示)、海报弹层内「中文/English」切换(默认跟 App 语言,
/// 提示文案/相册标题/二维码 lang 随切换,弹层 chrome 仍跟 App 语言)、
/// 下载海报反馈在弹层内可见(成功=按钮翻成功态自动复原;失败=弹层内
/// 红字可重试;SnackBar 在模态弹层下不可见,不得作为反馈通道)。
library;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:photo_live/features/albums/models.dart';
import 'package:photo_live/l10n/app_localizations.dart';
import 'package:photo_live/pages/share_qr_dialog.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../helpers/fakes.dart';
void main() {
const token = 'sharetoken000000000001';
Future<void> pumpPage(
WidgetTester tester, {
bool publicEnabled = true,
UrlLauncher? launcher,
PosterSaver? saver,
}) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
if (launcher != null)
urlLauncherProvider.overrideWith((ref) => launcher),
if (saver != null)
posterSaverProvider.overrideWith((ref) => saver),
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: ShareQrPage(
album: Album.fromJson(
albumJson(publicToken: token, publicEnabled: publicEnabled),
),
),
),
),
);
await tester.pumpAndSettle();
}
testWidgets('全屏页结构:标题/封面/名称/日期地点/二维码/扫码说明/链接', (
tester,
) async {
await pumpPage(tester);
// 全屏页 AppBar 标题(不再是 Dialog)。
expect(find.byType(AppBar), findsOneWidget);
expect(find.text('Share album'), findsOneWidget);
// 真实封面位(无封面 → 同尺寸中性占位,不破图不空行)。
expect(find.byIcon(Icons.image_outlined), findsOneWidget);
// 名称与日期地点(en 回退:Opening Gala;上海墙钟 14:30)。
expect(find.text('Opening Gala'), findsOneWidget);
expect(
find.text('2026/9/18 · 大宴会厅'),
findsOneWidget,
reason: '开始时间(相册时区;结束未定不伪造区间)+ 场地(展示回退链)',
);
// UI06 三方式:系统分享(默认)/ 生成二维码海报 / 复制链接。
expect(find.text('Share to...'), findsOneWidget);
expect(find.text('Generate QR poster'), findsOneWidget);
expect(find.text('Copy link'), findsOneWidget);
// 可选中链接(二维码与复制共用同一 URL)。
final url = 'http://10.0.2.2:5175/a/$token?lang=en';
expect(
tester.widget<SelectableText>(find.byType(SelectableText)).data,
url,
reason: '链接文本(?lang= 随 App 语言)',
);
// 点「生成二维码海报」→ 弹层:标题/时间/地址/二维码/下载按钮。
await tester.tap(find.text('Generate QR poster'));
await tester.pumpAndSettle();
expect(find.byType(QrImageView), findsOneWidget);
expect(find.text('Scan to view photos'), findsOneWidget);
expect(find.text('Download poster image'), findsOneWidget);
// UI06:海报日期粒度(无时间)。
expect(find.text('2026/9/18'), findsOneWidget);
});
testWidgets('海报语言切换:默认跟 App 语言,切中文后提示/标题/二维码随之', (
tester,
) async {
await pumpPage(tester);
await tester.tap(find.text('Generate QR poster'));
await tester.pumpAndSettle();
// 默认 English 档(App 语言 en):英文提示 + lang=en 的二维码
// (qr_flutter 4.1 无公开 data getter,经 semanticsLabel 断言编码内容)。
expect(find.text('Scan to view photos'), findsOneWidget);
expect(
tester.widget<QrImageView>(find.byType(QrImageView)).semanticsLabel,
'http://10.0.2.2:5175/a/$token?lang=en',
reason: '二维码 lang 跟海报语言(默认=App 语言前缀)',
);
// 两档切换(语言名以自身语言书写,与语言选择面板同约定)。
expect(find.text('中文'), findsOneWidget);
expect(find.text('English'), findsOneWidget);
// 英文档标题取英文翻译(中文翻译不出现在海报上)。
expect(find.text('开幕晚宴'), findsNothing);
// 切「中文」:提示/标题换中文、二维码 lang=zh-CN。
await tester.tap(find.text('中文'));
await tester.pumpAndSettle();
expect(find.text('扫码查看照片'), findsOneWidget);
expect(find.text('Scan to view photos'), findsNothing);
expect(find.text('开幕晚宴'), findsOneWidget);
expect(
tester.widget<QrImageView>(find.byType(QrImageView)).semanticsLabel,
'http://10.0.2.2:5175/a/$token?lang=zh-CN',
);
// 弹层 chrome(标题/下载按钮)不随海报语言切换,仍跟 App 语言。
expect(find.text('Download poster image'), findsOneWidget);
});
testWidgets('下载海报成功:按钮翻成功态并自动复原,全程无 SnackBar', (
tester,
) async {
await pumpPage(tester, saver: (_) async {});
await tester.tap(find.text('Generate QR poster'));
await tester.pumpAndSettle();
await tester.ensureVisible(find.text('Download poster image'));
await tester.pumpAndSettle();
await tester.tap(find.text('Download poster image'));
await tester.pumpAndSettle();
// 反馈必须在弹层内可见(SnackBar 被模态弹层遮挡,禁用)。
expect(find.byType(SnackBar), findsNothing);
expect(
find.descendant(
of: find.byType(BottomSheet),
matching: find.text('Poster saved to gallery'),
),
findsOneWidget,
reason: '成功反馈显示在下载按钮上(弹层内)',
);
// 约 2.5s 后自动复原,可再次下载(配合语言切换存第二张)。
await tester.pump(const Duration(seconds: 3));
await tester.pumpAndSettle();
expect(find.text('Poster saved to gallery'), findsNothing);
expect(find.text('Download poster image'), findsOneWidget);
});
testWidgets('下载海报失败:弹层内红字提示且可重试', (tester) async {
var calls = 0;
await pumpPage(
tester,
saver: (_) async {
calls++;
throw StateError('save failed');
},
);
await tester.tap(find.text('Generate QR poster'));
await tester.pumpAndSettle();
await tester.ensureVisible(find.text('Download poster image'));
await tester.pumpAndSettle();
await tester.tap(find.text('Download poster image'));
await tester.pumpAndSettle();
expect(find.byType(SnackBar), findsNothing);
expect(
find.descendant(
of: find.byType(BottomSheet),
matching: find.text('Failed to save poster'),
),
findsOneWidget,
reason: '失败反馈在弹层内(对齐隐藏照片弹层的面板内提示模式)',
);
// 按钮复位可重试。
expect(find.text('Download poster image'), findsOneWidget);
await tester.ensureVisible(find.text('Download poster image'));
await tester.pumpAndSettle();
await tester.tap(find.text('Download poster image'));
await tester.pumpAndSettle();
expect(calls, 2, reason: '失败后可重试');
});
testWidgets('Copy link(主操作)→ 剪贴板收到带 lang 的 URL + 已复制提示',
(tester) async {
final clipboardCalls = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(call) async {
clipboardCalls.add(call);
return null;
},
);
await pumpPage(tester);
await tester.ensureVisible(find.text('Copy link'));
await tester.pumpAndSettle();
await tester.tap(find.text('Copy link'));
await tester.pumpAndSettle();
expect(
clipboardCalls.any(
(c) =>
c.method == 'Clipboard.setData' &&
(c.arguments as Map<String, dynamic>)['text'] ==
'http://10.0.2.2:5175/a/$token?lang=en',
),
isTrue,
);
expect(find.text('Copied'), findsOneWidget);
});
testWidgets('Open in browser(次操作)→ 启动器收到 URL + 退出分享页',
(tester) async {
final launchedUrls = <String>[];
await pumpPage(
tester,
launcher: (url) async {
launchedUrls.add(url);
return true;
},
);
await tester.ensureVisible(find.text('Open in browser'));
await tester.pumpAndSettle();
await tester.tap(find.text('Open in browser'));
await tester.pumpAndSettle();
expect(launchedUrls, ['http://10.0.2.2:5175/a/$token?lang=en']);
expect(find.byType(ShareQrPage), findsNothing, reason: '打开成功后退出');
});
testWidgets('未公开相册:琥珀提示链接不可公开浏览;公开相册不提示',
(tester) async {
await pumpPage(tester, publicEnabled: false);
expect(
find.text('This link is not publicly accessible yet'),
findsOneWidget,
);
await pumpPage(tester, publicEnabled: true);
expect(
find.text('This link is not publicly accessible yet'),
findsNothing,
reason: '公开相册不得显示不可浏览提示',
);
});
}