Files
photo-live/apps/mobile/test/features/album_form_test.dart
T
2026-09-19 01:56:38 +08:00

241 lines
8.4 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.
/// 相册表单纯逻辑测试(P3 计划 Task 4)。
///
/// 覆盖校验矩阵(默认语言覆盖/其他字段漏填标题/时间序/时区)与载荷
/// 构建:创建体形状、PATCH 最小化(无变更→null、单开关独立、endAt
/// 显式清除序列化为 null、translations 全量替换且不带其他键)。
library;
import 'package:flutter_test/flutter_test.dart';
import 'package:photo_live/features/albums/album_form.dart';
import 'package:photo_live/features/albums/models.dart';
import '../helpers/fakes.dart';
/// 与 albumJson 缺省值对齐的草稿(「原样」编辑基线)。
AlbumFormDraft draftOf({
String zhTitle = '开幕晚宴',
String zhVenue = '大宴会厅',
String enTitle = 'Opening Gala',
String defaultLocale = 'zh-CN',
String timezone = 'Asia/Shanghai',
DateTime? startAt,
DateTime? endAt,
bool uploadEnabled = true,
bool publicEnabled = true,
bool downloadEnabled = true,
bool originalDownloadEnabled = false,
}) =>
AlbumFormDraft(
zh: AlbumTranslationDraft(
locale: 'zh-CN',
title: zhTitle,
venue: zhVenue,
),
en: AlbumTranslationDraft(locale: 'en', title: enTitle),
defaultLocale: defaultLocale,
timezone: timezone,
startAt: startAt ?? DateTime.utc(2026, 9, 18, 6, 30),
endAt: endAt,
uploadEnabled: uploadEnabled,
publicEnabled: publicEnabled,
downloadEnabled: downloadEnabled,
originalDownloadEnabled: originalDownloadEnabled,
);
void main() {
group('validate(客户端预检矩阵)', () {
test('默认语言标题为空 → defaultLocaleMissing;未选开始 → startAtRequired', () {
// 直接构造(不经 draftOf:其 startAt 缺省会吞掉显式 null)。
final errors = AlbumFormDraft(
zh: const AlbumTranslationDraft(
locale: 'zh-CN',
title: ' ',
venue: '大宴会厅',
),
en: const AlbumTranslationDraft(locale: 'en', title: 'Opening Gala'),
defaultLocale: 'zh-CN',
timezone: 'Asia/Shanghai',
startAt: null,
).validate();
expect(errors, containsAll(<AlbumFormError>[
AlbumFormError.defaultLocaleMissing,
AlbumFormError.startAtRequired,
]));
});
test('非默认语言填了其他字段但没填标题 → titleRequiredWithFields', () {
final errors = draftOf(enTitle: '')
.validate()
.where((e) => e == AlbumFormError.titleRequiredWithFields)
.toList();
// en 无标题且无其他字段 → 条目整体跳过,不算错误。
expect(errors, isEmpty);
final errors2 = AlbumFormDraft(
zh: const AlbumTranslationDraft(locale: 'zh-CN', title: '标题'),
en: const AlbumTranslationDraft(locale: 'en', venue: 'Main Hall'),
defaultLocale: 'zh-CN',
timezone: 'Asia/Shanghai',
startAt: DateTime.utc(2026, 9, 18, 6, 30),
).validate();
expect(errors2, contains(AlbumFormError.titleRequiredWithFields));
expect(errors2, isNot(contains(AlbumFormError.defaultLocaleMissing)));
});
test('endAt 必须严格晚于 startAt(相等/早于报错,null 放行)', () {
final startAt = DateTime.utc(2026, 9, 18, 6, 30);
expect(
draftOf(startAt: startAt, endAt: startAt).validate(),
contains(AlbumFormError.endAtNotAfterStartAt),
);
expect(
draftOf(
startAt: startAt,
endAt: startAt.subtract(const Duration(minutes: 1)),
).validate(),
contains(AlbumFormError.endAtNotAfterStartAt),
);
expect(
draftOf(startAt: startAt, endAt: null).validate(),
isNot(contains(AlbumFormError.endAtNotAfterStartAt)),
);
});
test('时区校验:空白/非法 IANA 名报错,Asia/Shanghai 通过', () {
expect(
draftOf(timezone: '').validate(),
contains(AlbumFormError.timezoneInvalid),
);
expect(
draftOf(timezone: 'Mars/Olympus').validate(),
contains(AlbumFormError.timezoneInvalid),
);
expect(
draftOf(timezone: 'Asia/Shanghai').validate(),
isNot(contains(AlbumFormError.timezoneInvalid)),
);
});
});
group('toCreateRequest', () {
test('载荷形状:trim、空字段归一 null、不含 upload/public', () {
final req = draftOf(
zhTitle: ' 开幕晚宴 ',
zhVenue: ' ',
enTitle: 'Opening Gala',
downloadEnabled: false,
originalDownloadEnabled: true,
).toCreateRequest();
final json = req.toJson();
expect(json['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,
},
]);
expect(json['defaultLocale'], 'zh-CN');
expect(json['startAt'], '2026-09-18T06:30:00.000Z');
expect(json['endAt'], isNull);
expect(json['timezone'], 'Asia/Shanghai');
expect(json['downloadEnabled'], false);
expect(json['originalDownloadEnabled'], true);
// 契约:创建请求体不含 uploadEnabled/publicEnabled(服务端缺省 true)。
expect(json.containsKey('uploadEnabled'), isFalse);
expect(json.containsKey('publicEnabled'), isFalse);
});
});
group('toUpdatePatch(PATCH 最小化)', () {
test('与原值完全一致 → null(不提交)', () {
final original = Album.fromJson(albumJson());
expect(draftOf().toUpdatePatch(original), isNull);
});
test('仅拨动 originalDownloadEnabled → 单键载荷(开关独立性)', () {
final original = Album.fromJson(albumJson());
final patch =
draftOf(originalDownloadEnabled: true).toUpdatePatch(original);
expect(patch, isNotNull);
expect(patch!.toJson(), <String, dynamic>{
'originalDownloadEnabled': true,
});
});
test('endAt 显式清除 → 序列化为 "endAt": null', () {
final original = Album.fromJson(
albumJson(endAt: '2026-09-19T10:00:00Z'),
);
// 用户清除 endAt 后:startAt/endAt 为原开始时间与 null。
final patch = AlbumFormDraft(
zh: AlbumTranslationDraft(
locale: 'zh-CN',
title: '开幕晚宴',
venue: '大宴会厅',
),
en: const AlbumTranslationDraft(locale: 'en', title: 'Opening Gala'),
defaultLocale: 'zh-CN',
timezone: 'Asia/Shanghai',
startAt: original.startAt,
endAt: null,
).toUpdatePatch(original);
expect(patch, isNotNull);
expect(patch!.toJson(), <String, dynamic>{'endAt': null});
});
test('仅修改标题 → translations 全量替换且不含其他键', () {
final original = Album.fromJson(albumJson());
final patch =
draftOf(zhTitle: '闭幕演出').toUpdatePatch(original);
expect(patch, isNotNull);
final json = patch!.toJson();
expect(json.keys, <String>['translations']);
expect(json['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,
},
]);
});
test('切换 defaultLocale(内容未变)→ 仅 defaultLocale 单键', () {
final original = Album.fromJson(albumJson());
final patch = draftOf(defaultLocale: 'en').toUpdatePatch(original);
expect(patch, isNotNull);
final json = patch!.toJson();
// 最小化:translations 未变则不重发(服务端按存量校验 en 覆盖,
// 客户端 validate 已先行拦截 en 标题缺失的切换)。
expect(json, <String, dynamic>{'defaultLocale': 'en'});
});
test('修改开始时间 → 仅 startAt 键', () {
final original = Album.fromJson(albumJson());
final patch = draftOf(
startAt: DateTime.utc(2026, 9, 18, 8, 0),
).toUpdatePatch(original);
expect(patch!.toJson(), <String, dynamic>{
'startAt': '2026-09-18T08:00:00.000Z',
});
});
});
}