Initial commit

This commit is contained in:
admin1
2026-07-14 11:11:36 +08:00
commit 656499cf94
604 changed files with 119518 additions and 0 deletions
@@ -0,0 +1,287 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:smplayer/core/contracts/library.dart';
import 'package:smplayer/shared/mappers/media_image_url.dart';
const _baseUrl = 'https://emby.example.com';
const _token = 'test-token';
EmbyRawItem _makeItem({
String id = 'item-1',
String? type,
String? primaryImageTag,
Map<String, String>? imageTags,
List<String>? backdropImageTags,
Map<String, dynamic> extra = const {},
}) {
return EmbyRawItem(
Id: id,
Name: 'Test',
Type: type,
PrimaryImageTag: primaryImageTag,
ImageTags: imageTags,
BackdropImageTags: backdropImageTags,
extra: extra,
);
}
void main() {
group('EmbyImageUrl.landscapeCard fallback chain', () {
test('returns Primary when PrimaryImageTag is present', () {
final item = _makeItem(primaryImageTag: 'primary-tag-1');
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, isNotNull);
expect(url, contains('/Images/Primary'));
expect(url, contains('tag=primary-tag-1'));
});
test('returns Primary from ImageTags map when PrimaryImageTag is null', () {
final item = _makeItem(imageTags: {'Primary': 'map-primary-tag'});
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, isNotNull);
expect(url, contains('/Images/Primary'));
expect(url, contains('tag=map-primary-tag'));
});
test('falls back to Thumb when Primary is missing', () {
final item = _makeItem(imageTags: {'Thumb': 'thumb-tag-1'});
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, isNotNull);
expect(url, contains('/Images/Thumb'));
expect(url, contains('tag=thumb-tag-1'));
});
test('falls back to Backdrop when Primary and Thumb are missing', () {
final item = _makeItem(backdropImageTags: ['backdrop-tag-1']);
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, isNotNull);
expect(url, contains('/Images/Backdrop'));
expect(url, contains('tag=backdrop-tag-1'));
});
test('falls back to parent Thumb from extra JSON', () {
final item = _makeItem(
extra: {
'ParentThumbItemId': 'parent-1',
'ParentThumbImageTag': 'parent-thumb-tag',
},
);
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, isNotNull);
expect(url, contains('/Items/parent-1/Images/Thumb'));
expect(url, contains('tag=parent-thumb-tag'));
});
test('falls back to parent Backdrop from extra JSON', () {
final item = _makeItem(
extra: {
'ParentBackdropItemId': 'parent-2',
'ParentBackdropImageTags': ['parent-backdrop-tag'],
},
);
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, isNotNull);
expect(url, contains('/Items/parent-2/Images/Backdrop'));
expect(url, contains('tag=parent-backdrop-tag'));
});
test('falls back to series Primary from extra JSON as last resort', () {
final item = _makeItem(
extra: {
'SeriesId': 'series-1',
'SeriesPrimaryImageTag': 'series-primary-tag',
},
);
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, isNotNull);
expect(url, contains('/Items/series-1/Images/Primary'));
expect(url, contains('tag=series-primary-tag'));
});
test('returns null when all image sources are absent', () {
final item = _makeItem();
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, isNull);
});
test('respects priority: Primary beats Thumb beats Backdrop', () {
final item = _makeItem(
primaryImageTag: 'primary-tag',
imageTags: {'Primary': 'primary-tag', 'Thumb': 'thumb-tag'},
backdropImageTags: ['backdrop-tag'],
);
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, contains('/Images/Primary'));
expect(url, contains('tag=primary-tag'));
});
test('parent Thumb takes priority over parent Backdrop', () {
final item = _makeItem(
extra: {
'ParentThumbItemId': 'parent-1',
'ParentThumbImageTag': 'parent-thumb-tag',
'ParentBackdropItemId': 'parent-2',
'ParentBackdropImageTags': ['parent-backdrop-tag'],
'SeriesId': 'series-1',
'SeriesPrimaryImageTag': 'series-primary-tag',
},
);
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, contains('/Items/parent-1/Images/Thumb'));
});
test('forwards maxWidth and quality parameters', () {
final item = _makeItem(primaryImageTag: 'tag');
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
maxWidth: 500,
quality: 80,
);
expect(url, contains('maxWidth=500'));
expect(url, contains('quality=80'));
});
});
group('EmbyImageUrl.landscapeCard movie-specific ordering', () {
test('movie prefers Backdrop over Primary', () {
final item = _makeItem(
type: 'Movie',
primaryImageTag: 'poster-tag',
backdropImageTags: ['backdrop-tag'],
);
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, contains('/Images/Backdrop'));
expect(url, contains('tag=backdrop-tag'));
});
test('movie falls back to Thumb when Backdrop is missing', () {
final item = _makeItem(
type: 'Movie',
primaryImageTag: 'poster-tag',
imageTags: {'Primary': 'poster-tag', 'Thumb': 'thumb-tag'},
);
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, contains('/Images/Thumb'));
expect(url, contains('tag=thumb-tag'));
});
test('movie falls back to Primary (poster) as last resort', () {
final item = _makeItem(type: 'Movie', primaryImageTag: 'poster-tag');
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, contains('/Images/Primary'));
expect(url, contains('tag=poster-tag'));
});
test('movie returns tagless Primary URL when all tag sources absent', () {
final item = _makeItem(type: 'Movie');
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, isNotNull);
expect(url, contains('/Items/item-1/Images/Primary'));
expect(url, isNot(contains('tag=')));
});
test('episode still prefers Primary over Backdrop', () {
final item = _makeItem(
type: 'Episode',
primaryImageTag: 'screenshot-tag',
backdropImageTags: ['backdrop-tag'],
);
final url = EmbyImageUrl.landscapeCard(
baseUrl: _baseUrl,
token: _token,
item: item,
);
expect(url, contains('/Images/Primary'));
expect(url, contains('tag=screenshot-tag'));
});
});
}
+141
View File
@@ -0,0 +1,141 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:smplayer/shared/widgets/version_grouping.dart';
import 'package:smplayer/shared/widgets/version_source_card.dart';
void main() {
VersionSourceCardData createEntry({
required String serverName,
required String sourceKey,
String? serverKey,
int? sizeBytes = 1000,
String resolutionLabel = '4K',
bool isCurrent = false,
bool selected = false,
}) {
return VersionSourceCardData(
topLabel: '$resolutionLabel SDR',
serverName: serverName,
serverKey: serverKey ?? sourceKey.split('|').first,
sourceKey: sourceKey,
sizeBytes: sizeBytes,
resolutionLabel: resolutionLabel,
isCurrent: isCurrent,
selected: selected,
);
}
group('groupVersionEntries', () {
test('merges equal size and resolution in source order', () {
final firstEntry = createEntry(
serverName: 'Server A',
sourceKey: 'server-a|source-a',
);
final secondEntry = createEntry(
serverName: 'Server B',
sourceKey: 'server-b|source-b',
);
final groups = groupVersionEntries([firstEntry, secondEntry]);
expect(groups, hasLength(1));
expect(groups.single.representative, same(firstEntry));
expect(groups.single.members, [firstEntry, secondEntry]);
expect(groups.single.serverNames, ['Server A', 'Server B']);
});
test('does not merge sources without a positive file size', () {
final missingSizeEntry = createEntry(
serverName: 'Server A',
sourceKey: 'server-a|source-a',
sizeBytes: null,
);
final zeroSizeEntry = createEntry(
serverName: 'Server B',
sourceKey: 'server-b|source-b',
sizeBytes: 0,
);
final groups = groupVersionEntries([missingSizeEntry, zeroSizeEntry]);
expect(groups, hasLength(2));
expect(groups[0].members, [missingSizeEntry]);
expect(groups[1].members, [zeroSizeEntry]);
});
test('keeps different resolution labels in separate groups', () {
final ultraHdEntry = createEntry(
serverName: 'Server A',
sourceKey: 'server-a|source-a',
);
final fullHdEntry = createEntry(
serverName: 'Server B',
sourceKey: 'server-b|source-b',
resolutionLabel: '1080P',
);
final groups = groupVersionEntries([ultraHdEntry, fullHdEntry]);
expect(groups, hasLength(2));
});
test('does not merge duplicate files from the same server', () {
final firstEntry = createEntry(
serverName: 'Server A',
sourceKey: 'server-a|source-a',
);
final secondEntry = createEntry(
serverName: 'Server A',
sourceKey: 'server-a|source-b',
);
final groups = groupVersionEntries([firstEntry, secondEntry]);
expect(groups, hasLength(2));
});
test(
'uses selected member as visible server without moving representative',
() {
final representative = createEntry(
serverName: 'Server A',
sourceKey: 'server-a|source-a',
);
final selectedMember = createEntry(
serverName: 'Server B',
sourceKey: 'server-b|source-b',
selected: true,
);
final group = groupVersionEntries([
representative,
selectedMember,
]).single;
expect(group.representative, same(representative));
expect(group.activeMember, same(selectedMember));
expect(group.displayData.serverName, 'Server B');
expect(group.displayData.sourceKey, 'server-b|source-b');
expect(group.displayData.selected, isTrue);
expect(group.displayData.mergedServerNames, ['Server A', 'Server B']);
},
);
test('propagates current state from any member to collapsed card', () {
final representative = createEntry(
serverName: 'Server A',
sourceKey: 'server-a|source-a',
);
final currentMember = createEntry(
serverName: 'Server B',
sourceKey: 'server-b|source-b',
isCurrent: true,
);
final group = groupVersionEntries([representative, currentMember]).single;
expect(group.hasCurrentMember, isTrue);
expect(group.displayData.isCurrent, isTrue);
expect(group.displayData.serverName, 'Server B');
});
});
}
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:forui/forui.dart';
import 'package:smplayer/shared/widgets/setting_select.dart';
void main() {
Widget host({
required String value,
required ValueChanged<String?> onChanged,
}) {
return MaterialApp(
home: FTheme(
data: FThemes.zinc.light.desktop,
child: Scaffold(
body: Center(
child: SettingSelect<String>(
value: value,
options: const ['a', 'b', 'c'],
labelOf: (v) => 'label-$v',
onChanged: onChanged,
),
),
),
),
);
}
testWidgets('触发行显示选中项 label', (tester) async {
await tester.pumpWidget(host(value: 'b', onChanged: (_) {}));
await tester.pumpAndSettle();
expect(find.text('label-b'), findsWidgets);
});
testWidgets('选另一项触发 onChanged', (tester) async {
String? picked;
await tester.pumpWidget(host(value: 'a', onChanged: (v) => picked = v));
await tester.pumpAndSettle();
await tester.tap(find.byType(SettingSelect<String>));
await tester.pumpAndSettle();
await tester.tap(find.text('label-c').last);
await tester.pumpAndSettle();
expect(picked, 'c');
});
testWidgets('Select 可放在 Row 的 trailing 位置', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: FTheme(
data: FThemes.zinc.light.desktop,
child: Scaffold(
body: SizedBox(
width: 480,
child: Row(
children: [
const Expanded(child: Text('设置项')),
SettingSelect<String>(
value: 'b',
options: const ['a', 'b', 'c'],
labelOf: (v) => 'label-$v',
onChanged: (_) {},
),
],
),
),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('label-b'), findsWidgets);
expect(tester.takeException(), isNull);
});
}