bbea1dd364
Co-Authored-By: Claude Code <noreply@anthropic.com>
322 lines
10 KiB
Dart
322 lines
10 KiB
Dart
/// 相册创建/编辑表单 widget 测试(P3 计划 Task 4)。
|
|
///
|
|
/// 覆盖:校验矩阵(默认语言标题缺失/未选开始时间/endAt 早于 startAt)、
|
|
/// 创建成功 POST 载荷形状、编辑预加载(字段与四开关初值、创建仅两开关)、
|
|
/// PATCH 最小化(仅拨动的开关出现在请求体)、endAt 显式清除、
|
|
/// 标题修改仅发全量 translations。
|
|
library;
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:photo_live/core/api_client.dart';
|
|
import 'package:photo_live/features/albums/album_form.dart';
|
|
import 'package:photo_live/features/albums/models.dart';
|
|
import 'package:photo_live/l10n/app_localizations.dart';
|
|
import 'package:photo_live/pages/album_form_page.dart';
|
|
|
|
import '../helpers/fakes.dart';
|
|
|
|
void main() {
|
|
late FakeSessionStore store;
|
|
|
|
setUp(() {
|
|
store = FakeSessionStore();
|
|
});
|
|
|
|
ApiClient clientOf(FakeAdapter adapter) => ApiClient(
|
|
sessionStore: store,
|
|
dio: Dio(BaseOptions(baseUrl: 'http://test.local'))
|
|
..httpClientAdapter = adapter,
|
|
);
|
|
|
|
Future<void> pumpForm(
|
|
WidgetTester tester,
|
|
ApiClient client, {
|
|
Album? existing,
|
|
AlbumFormDraft? initialDraft,
|
|
}) async {
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [apiClientProvider.overrideWith((ref) => client)],
|
|
child: MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: AlbumFormPage(existing: existing, initialDraft: initialDraft),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
/// 创建模式合法草稿(测试注入开始/结束时间,避开平台日期选择器)。
|
|
AlbumFormDraft createDraft({
|
|
String zhTitle = '开幕晚宴',
|
|
String enTitle = 'Opening Gala',
|
|
String defaultLocale = 'zh-CN',
|
|
DateTime? startAt,
|
|
DateTime? endAt,
|
|
bool downloadEnabled = true,
|
|
bool originalDownloadEnabled = false,
|
|
}) =>
|
|
AlbumFormDraft(
|
|
zh: AlbumTranslationDraft(locale: 'zh-CN', title: zhTitle),
|
|
en: AlbumTranslationDraft(locale: 'en', title: enTitle),
|
|
defaultLocale: defaultLocale,
|
|
timezone: 'Asia/Shanghai',
|
|
startAt: startAt ?? DateTime.utc(2026, 9, 18, 6, 30),
|
|
endAt: endAt,
|
|
downloadEnabled: downloadEnabled,
|
|
originalDownloadEnabled: originalDownloadEnabled,
|
|
);
|
|
|
|
/// 表单长于测试视口(800x600):按钮常在折叠线下,tap 前先滚动可见。
|
|
Future<void> tapVisible(WidgetTester tester, Finder finder) async {
|
|
await tester.ensureVisible(finder);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(finder);
|
|
}
|
|
|
|
testWidgets('创建:空表单提交 → 汇总错误(默认语言标题/开始时间),不发请求',
|
|
(tester) async {
|
|
final adapter = FakeAdapter((_) => throw StateError('不应发起请求'));
|
|
await pumpForm(tester, clientOf(adapter));
|
|
|
|
// 创建模式仅两开关(download/original)。
|
|
expect(find.byType(SwitchListTile), findsNWidgets(2));
|
|
|
|
await tapVisible(tester, find.text('Create'));
|
|
await tester.pump();
|
|
|
|
expect(find.text('Default locale must have a title'), findsOneWidget);
|
|
// 开始时间行占位与汇总错误复用同一文案 → 2 处。
|
|
expect(find.text('Pick a start time'), findsNWidgets(2));
|
|
expect(adapter.requests, isEmpty);
|
|
});
|
|
|
|
testWidgets('创建:defaultLocale=en 而 en 标题空 → 默认语言覆盖错误',
|
|
(tester) async {
|
|
final adapter = FakeAdapter((_) => throw StateError('不应发起请求'));
|
|
await pumpForm(
|
|
tester,
|
|
clientOf(adapter),
|
|
initialDraft: createDraft(defaultLocale: 'en', enTitle: ''),
|
|
);
|
|
|
|
await tapVisible(tester, find.text('Create'));
|
|
await tester.pump();
|
|
|
|
expect(find.text('Default locale must have a title'), findsOneWidget);
|
|
expect(adapter.requests, isEmpty);
|
|
});
|
|
|
|
testWidgets('创建:endAt 早于 startAt → 时间序错误', (tester) async {
|
|
final adapter = FakeAdapter((_) => throw StateError('不应发起请求'));
|
|
await pumpForm(
|
|
tester,
|
|
clientOf(adapter),
|
|
initialDraft: createDraft(
|
|
endAt: DateTime.utc(2026, 9, 18, 5, 0),
|
|
),
|
|
);
|
|
|
|
await tapVisible(tester, find.text('Create'));
|
|
await tester.pump();
|
|
|
|
expect(find.text('End time must be after start time'), findsOneWidget);
|
|
expect(adapter.requests, isEmpty);
|
|
});
|
|
|
|
testWidgets('创建:合法提交 → POST 载荷形状(双语 translations/UTC 时间/开关)',
|
|
(tester) async {
|
|
final adapter = FakeAdapter(
|
|
(o) => o.path == '/v1/albums'
|
|
? (201, albumJson())
|
|
: throw StateError('unexpected ${o.path}'),
|
|
);
|
|
await pumpForm(
|
|
tester,
|
|
clientOf(adapter),
|
|
initialDraft: createDraft(
|
|
endAt: DateTime.utc(2026, 9, 19, 10, 0),
|
|
downloadEnabled: false,
|
|
originalDownloadEnabled: true,
|
|
),
|
|
);
|
|
|
|
await tapVisible(tester, find.text('Create'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final request = adapter.requests.single;
|
|
expect(request.method, 'POST');
|
|
expect(request.path, '/v1/albums');
|
|
expect(request.body, <String, dynamic>{
|
|
'translations': <Map<String, dynamic>>[
|
|
<String, dynamic>{
|
|
'locale': 'zh-CN',
|
|
'title': '开幕晚宴',
|
|
'description': null,
|
|
'venue': null,
|
|
'address': null,
|
|
},
|
|
<String, dynamic>{
|
|
'locale': 'en',
|
|
'title': 'Opening Gala',
|
|
'description': null,
|
|
'venue': null,
|
|
'address': null,
|
|
},
|
|
],
|
|
'defaultLocale': 'zh-CN',
|
|
'startAt': '2026-09-18T06:30:00.000Z',
|
|
'endAt': '2026-09-19T10:00:00.000Z',
|
|
'timezone': 'Asia/Shanghai',
|
|
'downloadEnabled': false,
|
|
'originalDownloadEnabled': true,
|
|
});
|
|
});
|
|
|
|
testWidgets('编辑:预加载既有值(字段、四开关独立初值、无变更时保存禁用)',
|
|
(tester) async {
|
|
final adapter = FakeAdapter(
|
|
(o) => throw StateError('编辑预加载不应发起请求:${o.path}'),
|
|
);
|
|
final existing = Album.fromJson(
|
|
albumJson(
|
|
publicEnabled: false,
|
|
originalDownloadEnabled: true,
|
|
endAt: '2026-09-19T10:00:00Z',
|
|
),
|
|
);
|
|
await pumpForm(tester, clientOf(adapter), existing: existing);
|
|
|
|
// 编辑模式四开关。
|
|
expect(find.byType(SwitchListTile), findsNWidgets(4));
|
|
// 字段预填(当前 zh tab;title 为第一个文本字段)。
|
|
final titleField = tester.widget<TextFormField>(
|
|
find.byType(TextFormField).first,
|
|
);
|
|
expect(titleField.controller!.text, '开幕晚宴');
|
|
// 开关初值与原相册一致。
|
|
expect(
|
|
tester
|
|
.widget<SwitchListTile>(
|
|
find.widgetWithText(SwitchListTile, 'Allow uploads'),
|
|
)
|
|
.value,
|
|
true,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<SwitchListTile>(
|
|
find.widgetWithText(SwitchListTile, 'Publicly visible'),
|
|
)
|
|
.value,
|
|
false,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<SwitchListTile>(
|
|
find.widgetWithText(SwitchListTile, 'Allow original downloads'),
|
|
)
|
|
.value,
|
|
true,
|
|
);
|
|
// 时间回显按相册时区。
|
|
expect(find.text('2026/9/18 14:30'), findsOneWidget);
|
|
expect(find.text('2026/9/19 18:00'), findsOneWidget);
|
|
// 无任何变更 → 保存禁用(PATCH minProperties 1)。
|
|
expect(
|
|
tester.widget<FilledButton>(find.byType(FilledButton)).onPressed,
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
testWidgets('编辑:仅拨动 originalDownloadEnabled → PATCH 单键载荷',
|
|
(tester) async {
|
|
final adapter = FakeAdapter(
|
|
(o) => o.path == '/v1/albums/alb-1'
|
|
? (200, albumJson(originalDownloadEnabled: true))
|
|
: throw StateError('unexpected ${o.path}'),
|
|
);
|
|
final existing = Album.fromJson(albumJson());
|
|
await pumpForm(tester, clientOf(adapter), existing: existing);
|
|
|
|
await tapVisible(
|
|
tester,
|
|
find.widgetWithText(SwitchListTile, 'Allow original downloads'),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tapVisible(tester, find.text('Save'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final request = adapter.requests.single;
|
|
expect(request.method, 'PATCH');
|
|
expect(request.path, '/v1/albums/alb-1');
|
|
expect(request.body, <String, dynamic>{
|
|
'originalDownloadEnabled': true,
|
|
});
|
|
});
|
|
|
|
testWidgets('编辑:清除 endAt → PATCH 序列化 "endAt": null', (tester) async {
|
|
final adapter = FakeAdapter(
|
|
(o) => o.path == '/v1/albums/alb-1'
|
|
? (200, albumJson(endAt: null))
|
|
: throw StateError('unexpected ${o.path}'),
|
|
);
|
|
final existing = Album.fromJson(
|
|
albumJson(endAt: '2026-09-19T10:00:00Z'),
|
|
);
|
|
await pumpForm(tester, clientOf(adapter), existing: existing);
|
|
|
|
await tapVisible(tester, find.byTooltip('Clear end time'));
|
|
await tester.pump();
|
|
|
|
await tapVisible(tester, find.text('Save'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(adapter.requests.single.body, <String, dynamic>{'endAt': null});
|
|
});
|
|
|
|
testWidgets('编辑:修改 zh 标题 → 仅发全量 translations(不含开关/时间)',
|
|
(tester) async {
|
|
final adapter = FakeAdapter(
|
|
(o) => o.path == '/v1/albums/alb-1'
|
|
? (200, albumJson())
|
|
: throw StateError('unexpected ${o.path}'),
|
|
);
|
|
final existing = Album.fromJson(albumJson());
|
|
await pumpForm(tester, clientOf(adapter), existing: existing);
|
|
|
|
await tester.enterText(
|
|
find.byType(TextFormField).first,
|
|
'闭幕演出',
|
|
);
|
|
await tester.pump();
|
|
|
|
await tapVisible(tester, find.text('Save'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final body = adapter.requests.single.body as Map<String, dynamic>;
|
|
expect(body.keys, <String>['translations']);
|
|
expect(body['translations'], <Map<String, dynamic>>[
|
|
<String, dynamic>{
|
|
'locale': 'zh-CN',
|
|
'title': '闭幕演出',
|
|
'description': null,
|
|
'venue': '大宴会厅',
|
|
'address': null,
|
|
},
|
|
<String, dynamic>{
|
|
'locale': 'en',
|
|
'title': 'Opening Gala',
|
|
'description': null,
|
|
'venue': null,
|
|
'address': null,
|
|
},
|
|
]);
|
|
});
|
|
}
|