Files
sm-emby-share/test/features/aggregated/aggregated_grouping_test.dart
2026-07-14 11:11:36 +08:00

320 lines
8.9 KiB
Dart
Raw Permalink 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_test/flutter_test.dart';
import 'package:smplayer/core/contracts/library.dart';
import 'package:smplayer/features/aggregated/aggregated_grouping.dart';
GlobalSearchServerResult _server(
String id,
String name,
List<EmbyRawItem> items,
) => GlobalSearchServerResult(
serverId: id,
serverName: name,
serverUrl: 'http://$id',
token: 'tok-$id',
userId: 'user-$id',
items: items,
);
EmbyRawItem _item(
String id,
String name, {
String? type,
Map<String, dynamic>? providerIds,
String? lastPlayedDate,
int? playbackPositionTicks,
int? runtimeTicks,
bool? played,
}) => EmbyRawItem(
Id: id,
Name: name,
Type: type,
RunTimeTicks: runtimeTicks,
UserData: playbackPositionTicks != null || played != null
? EmbyRawUserData(
PlaybackPositionTicks: playbackPositionTicks,
Played: played,
)
: null,
extra: {
if (providerIds != null) 'ProviderIds': providerIds,
if (lastPlayedDate != null) 'UserData': {'LastPlayedDate': lastPlayedDate},
},
);
void main() {
group('flatten default behavior (dedup off)', () {
test('returns all items from all servers', () {
final results = [
_server('s1', 'S1', [_item('a', 'Movie A')]),
_server('s2', 'S2', [_item('b', 'Movie B')]),
];
final entries = flatten(results);
expect(entries.length, 2);
});
test('respects serverFilter', () {
final results = [
_server('s1', 'S1', [_item('a', 'A')]),
_server('s2', 'S2', [_item('b', 'B')]),
];
final entries = flatten(results, serverFilter: 's1');
expect(entries.length, 1);
expect(entries.first.source.serverId, 's1');
});
});
group('flatten dedup on, serverFilter null', () {
test('collapses same TMDB Movie to newest LastPlayedDate', () {
final results = [
_server('s1', 'S1', [
_item(
'a',
'Movie',
type: 'Movie',
providerIds: {'Tmdb': '100'},
lastPlayedDate: '2026-06-20T10:00:00Z',
),
]),
_server('s2', 'S2', [
_item(
'b',
'Movie',
type: 'Movie',
providerIds: {'Tmdb': '100'},
lastPlayedDate: '2026-06-24T15:00:00Z',
),
]),
];
final entries = flatten(results, deduplicateAcrossServers: true);
expect(entries.length, 1);
expect(entries.first.source.serverId, 's2');
expect(entries.first.sources.map((source) => source.source.serverId), [
's2',
's1',
]);
});
test('preserves each server item and its independent playback state', () {
final results = [
_server('s1', 'S1', [
_item(
'a',
'Movie',
type: 'Movie',
providerIds: {'Tmdb': '100'},
lastPlayedDate: '2026-06-20T10:00:00Z',
playbackPositionTicks: 100,
runtimeTicks: 1000,
),
]),
_server('s2', 'S2', [
_item(
'b',
'Movie',
type: 'Movie',
providerIds: {'Tmdb': '100'},
lastPlayedDate: '2026-06-24T15:00:00Z',
playbackPositionTicks: 800,
runtimeTicks: 1000,
),
]),
];
final entry = flatten(results, deduplicateAcrossServers: true).single;
expect(entry.sources, hasLength(2));
expect(entry.sources[0].item.UserData?.PlaybackPositionTicks, 800);
expect(entry.sources[1].item.UserData?.PlaybackPositionTicks, 100);
expect(entry.sources[0].item.RunTimeTicks, 1000);
expect(entry.sources[1].item.RunTimeTicks, 1000);
});
test('keeps input order when sources have equal LastPlayedDate', () {
final results = [
_server('s1', 'S1', [
_item(
'a',
'Movie',
type: 'Movie',
providerIds: {'Tmdb': '100'},
lastPlayedDate: '2026-06-24T15:00:00Z',
),
]),
_server('s2', 'S2', [
_item(
'b',
'Movie',
type: 'Movie',
providerIds: {'Tmdb': '100'},
lastPlayedDate: '2026-06-24T15:00:00Z',
),
]),
];
final entry = flatten(results, deduplicateAcrossServers: true).single;
expect(entry.sources.map((source) => source.source.serverId), [
's1',
's2',
]);
});
});
group('flatten dedup on + serverFilter non-null', () {
test('skips dedup when server filter is active', () {
final results = [
_server('s1', 'S1', [
_item('a', 'Movie', type: 'Movie', providerIds: {'Tmdb': '100'}),
]),
_server('s2', 'S2', [
_item('b', 'Movie', type: 'Movie', providerIds: {'Tmdb': '100'}),
]),
];
final entries = flatten(
results,
serverFilter: 's1',
deduplicateAcrossServers: true,
);
expect(entries.length, 1);
expect(entries.first.source.serverId, 's1');
expect(entries.first.sources, hasLength(1));
});
});
group('no ProviderIds never deduped', () {
test('items without ProviderIds stay separate', () {
final results = [
_server('s1', 'S1', [_item('a', 'Same Name', type: 'Movie')]),
_server('s2', 'S2', [_item('b', 'Same Name', type: 'Movie')]),
];
final entries = flatten(results, deduplicateAcrossServers: true);
expect(entries.length, 2);
});
});
group('Type namespace isolation', () {
test('Movie and Series with same tmdb id stay separate', () {
final results = [
_server('s1', 'S1', [
_item('a', 'Title', type: 'Movie', providerIds: {'Tmdb': '100'}),
]),
_server('s2', 'S2', [
_item('b', 'Title', type: 'Series', providerIds: {'Tmdb': '100'}),
]),
];
final entries = flatten(results, deduplicateAcrossServers: true);
expect(entries.length, 2);
});
});
group('ProviderIds key case insensitivity', () {
test('Tmdb vs tmdb vs TMDB all match', () {
final results = [
_server('s1', 'S1', [
_item(
'a',
'M',
type: 'Movie',
providerIds: {'Tmdb': '99'},
lastPlayedDate: '2026-06-20T00:00:00Z',
),
]),
_server('s2', 'S2', [
_item(
'b',
'M',
type: 'Movie',
providerIds: {'tmdb': '99'},
lastPlayedDate: '2026-06-21T00:00:00Z',
),
]),
_server('s3', 'S3', [
_item(
'c',
'M',
type: 'Movie',
providerIds: {'TMDB': '99'},
lastPlayedDate: '2026-06-22T00:00:00Z',
),
]),
];
final entries = flatten(results, deduplicateAcrossServers: true);
expect(entries.length, 1);
expect(entries.first.source.serverId, 's3');
});
});
group('null LastPlayedDate handling', () {
test('null loses to valid date', () {
final results = [
_server('s1', 'S1', [
_item('a', 'M', type: 'Movie', providerIds: {'Tmdb': '1'}),
]),
_server('s2', 'S2', [
_item(
'b',
'M',
type: 'Movie',
providerIds: {'Tmdb': '1'},
lastPlayedDate: '2026-06-01T00:00:00Z',
),
]),
];
final entries = flatten(results, deduplicateAcrossServers: true);
expect(entries.length, 1);
expect(entries.first.source.serverId, 's2');
});
test('both null keeps first encountered', () {
final results = [
_server('s1', 'S1', [
_item('a', 'M', type: 'Movie', providerIds: {'Tmdb': '1'}),
]),
_server('s2', 'S2', [
_item('b', 'M', type: 'Movie', providerIds: {'Tmdb': '1'}),
]),
];
final entries = flatten(results, deduplicateAcrossServers: true);
expect(entries.length, 1);
expect(entries.first.source.serverId, 's1');
});
});
group('transitive matching', () {
test('A(tmdb:1) B(tmdb:1,imdb:tt2) C(imdb:tt2) collapse to one', () {
final results = [
_server('s1', 'S1', [
_item(
'a',
'M',
type: 'Movie',
providerIds: {'Tmdb': '1'},
lastPlayedDate: '2026-06-20T00:00:00Z',
),
]),
_server('s2', 'S2', [
_item(
'b',
'M',
type: 'Movie',
providerIds: {'Tmdb': '1', 'Imdb': 'tt2'},
lastPlayedDate: '2026-06-22T00:00:00Z',
),
]),
_server('s3', 'S3', [
_item(
'c',
'M',
type: 'Movie',
providerIds: {'Imdb': 'tt2'},
lastPlayedDate: '2026-06-21T00:00:00Z',
),
]),
];
final entries = flatten(results, deduplicateAcrossServers: true);
expect(entries.length, 1);
expect(entries.first.source.serverId, 's2');
});
});
}