Parse-only DTO surface deleted across trackers and catalog models: quality-preset storageKey/fromStorage round-trips (persistence uses EnumPref on .name), PlexHome's placeholder identity fields (id stays — deriveHomeSecret reads it), SeerrUser.email/avatar, SimklSearchResult.endpointType, SimklDetail.type, SimklAllItemsEntry.status/isShow, TraktCatalogEntry.rank, CatalogItem.relevance, and the never-produced CatalogRankScope.favorited/.seasonal members with their label arms. Tracker root: OAuthProxyStart.expiresIn, TrackerSession.scope/isExpired, copyWith slimmed to its one real parameter, TrackerHttpClient.service. TrackerAccountStore.service and the trackerAccountStore() alias are live (persisted-blob validation, callers) and stay.
570 lines
21 KiB
Dart
570 lines
21 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/testing.dart';
|
|
import 'package:plezy/media/media_kind.dart';
|
|
import 'package:plezy/models/catalog/catalog_item.dart';
|
|
import 'package:plezy/models/catalog/catalog_metadata.dart';
|
|
import 'package:plezy/models/seerr/seerr_session.dart';
|
|
import 'package:plezy/services/catalog/catalog_source.dart';
|
|
import 'package:plezy/services/catalog/seerr_catalog_source.dart';
|
|
import 'package:plezy/services/seerr/seerr_client.dart';
|
|
import 'package:plezy/utils/external_ids.dart';
|
|
|
|
import '../../test_helpers/http_fixtures.dart';
|
|
|
|
SeerrCatalogSource _source(MockClient mock) {
|
|
final client = SeerrClient(
|
|
const SeerrSession(
|
|
baseUrl: 'https://seerr.example.com',
|
|
method: SeerrAuthMethod.local,
|
|
identifier: 'a@b.c',
|
|
secret: 'pw',
|
|
cookie: 'cookie',
|
|
userId: 1,
|
|
permissions: 2,
|
|
displayName: 'Alice',
|
|
instanceLabel: 'Seerr',
|
|
createdAt: 0,
|
|
),
|
|
onSessionInvalidated: () {},
|
|
httpClient: mock,
|
|
);
|
|
final source = SeerrCatalogSource(client);
|
|
addTearDown(() {
|
|
source.dispose();
|
|
client.dispose();
|
|
});
|
|
return source;
|
|
}
|
|
|
|
void main() {
|
|
group('SeerrCatalogSource', () {
|
|
test('trending maps row metadata, total results, and the responsive TMDB image ladders', () async {
|
|
final source = _source(
|
|
MockClient((request) async {
|
|
expect(request.url.path, '/api/v1/discover/trending');
|
|
return jsonResponse({
|
|
'page': 1,
|
|
'totalPages': 3,
|
|
'totalResults': 41,
|
|
'results': [
|
|
{
|
|
'id': 603,
|
|
'mediaType': 'movie',
|
|
'title': 'The Matrix',
|
|
'originalTitle': 'Matrix, The',
|
|
'originalLanguage': 'en',
|
|
'releaseDate': '1999-03-30',
|
|
'posterPath': '/matrix.jpg',
|
|
'backdropPath': '/matrix-backdrop.jpg',
|
|
'voteAverage': 8.2,
|
|
'voteCount': 26000,
|
|
'popularity': 42.5,
|
|
'adult': false,
|
|
},
|
|
{'id': 9, 'mediaType': 'person', 'name': 'Keanu Reeves'},
|
|
{
|
|
'id': 1396,
|
|
'mediaType': 'tv',
|
|
'name': 'Breaking Bad',
|
|
'firstAirDate': '2008-01-20',
|
|
'originCountry': ['us', 'GB'],
|
|
},
|
|
],
|
|
});
|
|
}),
|
|
);
|
|
|
|
final page = await source.fetchRow(CatalogRowId.trending);
|
|
expect(page.hasMore, isTrue);
|
|
expect(page.totalResults, 41);
|
|
expect(page.items, hasLength(2));
|
|
|
|
final matrix = page.items.first;
|
|
expect(matrix.source, CatalogSourceId.seerr);
|
|
expect(matrix.kind, MediaKind.movie);
|
|
expect(matrix.title, 'The Matrix');
|
|
expect(matrix.year, 1999);
|
|
expect(matrix.releaseDate, DateTime(1999, 3, 30));
|
|
expect(matrix.rating, 8.2);
|
|
expect(matrix.votes, 26000);
|
|
expect(matrix.ids.tmdb, 603);
|
|
expect(matrix.originalTitle, 'Matrix, The');
|
|
expect(matrix.languages, ['en']);
|
|
expect(matrix.isAdult, isFalse);
|
|
expect(matrix.posterUrl, 'https://image.tmdb.org/t/p/w600_and_h900_bestv2/matrix.jpg');
|
|
expect(matrix.backdropUrl, 'https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/matrix-backdrop.jpg');
|
|
expect(matrix.posterVariants?.keys, [92, 154, 185, 342, 500, 600, 780]);
|
|
expect(matrix.posterVariants?[342], 'https://image.tmdb.org/t/p/w342/matrix.jpg');
|
|
expect(matrix.posterVariants?[600], matrix.posterUrl);
|
|
expect(matrix.backdropVariants?.keys, [300, 780, 1280, 1920]);
|
|
expect(matrix.backdropVariants?[1920], matrix.backdropUrl);
|
|
|
|
expect(page.items.last.kind, MediaKind.show);
|
|
expect(page.items.last.title, 'Breaking Bad');
|
|
expect(page.items.last.countries, ['US', 'GB']);
|
|
expect(page.items.last.posterVariants, isNull);
|
|
expect(page.items.last.serverState, isNull);
|
|
});
|
|
|
|
test('maps availability and request state independently with explicit pending semantics', () async {
|
|
final source = _source(
|
|
MockClient(
|
|
(request) async => jsonResponse({
|
|
'page': 1,
|
|
'totalPages': 1,
|
|
'results': [
|
|
{
|
|
'id': 1,
|
|
'mediaType': 'movie',
|
|
'title': 'Available',
|
|
'mediaInfo': {'status': 5},
|
|
},
|
|
{
|
|
'id': 2,
|
|
'mediaType': 'tv',
|
|
'name': 'Partial',
|
|
'mediaInfo': {
|
|
'status': 4,
|
|
'seasons': [
|
|
{'seasonNumber': 0, 'status': 5},
|
|
{'seasonNumber': 1, 'status': 5},
|
|
{'seasonNumber': 2, 'status': 4},
|
|
{'seasonNumber': 3, 'status': 1},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
'id': 3,
|
|
'mediaType': 'movie',
|
|
'title': 'HD available, 4K pending',
|
|
'mediaInfo': {
|
|
'status': 5,
|
|
'status4k': 2,
|
|
'requests': [
|
|
{'id': 31, 'status': 1, 'is4k': true},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
'id': 4,
|
|
'mediaType': 'movie',
|
|
'title': 'Requested',
|
|
'mediaInfo': {'status': 2},
|
|
},
|
|
{
|
|
'id': 5,
|
|
'mediaType': 'movie',
|
|
'title': 'Pending approval',
|
|
'mediaInfo': {
|
|
'status': 1,
|
|
'requests': [
|
|
{'id': 51, 'status': 1, 'is4k': false},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
'id': 6,
|
|
'mediaType': 'movie',
|
|
'title': 'Declined',
|
|
'mediaInfo': {
|
|
'status': 1,
|
|
'requests': [
|
|
{'id': 61, 'status': 3, 'is4k': false},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
'id': 7,
|
|
'mediaType': 'movie',
|
|
'title': 'Processing',
|
|
'mediaInfo': {
|
|
'status': 3,
|
|
'requests': [
|
|
{'id': 71, 'status': 2, 'is4k': false},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
);
|
|
|
|
final page = await source.fetchRow(CatalogRowId.trending);
|
|
final byTitle = {for (final item in page.items) item.title: item.serverState};
|
|
|
|
expect(byTitle['Available']?.availability, CatalogAvailability.available);
|
|
expect(byTitle['Available']?.request, isNull);
|
|
|
|
expect(byTitle['Partial']?.availability, CatalogAvailability.partiallyAvailable);
|
|
expect(byTitle['Partial']?.availableSeasons, 1);
|
|
expect(byTitle['Partial']?.totalSeasons, 3);
|
|
|
|
final pending4k = byTitle['HD available, 4K pending'];
|
|
expect(pending4k?.availability, CatalogAvailability.available);
|
|
expect(pending4k?.availability4k, CatalogAvailability.unavailable);
|
|
expect(pending4k?.request, isNull);
|
|
expect(pending4k?.request4k, CatalogRequestState.pending);
|
|
|
|
// MediaInfo.status=2 is acquisition-pipeline "pending", not approval
|
|
// pending. Only requests[].status=1 maps to CatalogRequestState.pending.
|
|
expect(byTitle['Requested']?.availability, CatalogAvailability.unavailable);
|
|
expect(byTitle['Requested']?.request, CatalogRequestState.approved);
|
|
expect(byTitle['Pending approval']?.availability, isNull);
|
|
expect(byTitle['Pending approval']?.request, CatalogRequestState.pending);
|
|
expect(byTitle['Declined']?.request, CatalogRequestState.declined);
|
|
expect(byTitle['Processing']?.availability, CatalogAvailability.unavailable);
|
|
expect(byTitle['Processing']?.request, CatalogRequestState.processing);
|
|
});
|
|
|
|
test('preserves full dates and retains the coarse year fallback for malformed input', () async {
|
|
final source = _source(
|
|
MockClient(
|
|
(request) async => jsonResponse({
|
|
'page': 1,
|
|
'totalPages': 1,
|
|
'results': [
|
|
{'id': 1, 'title': 'Valid', 'releaseDate': '2027-04-09'},
|
|
{'id': 2, 'title': 'Malformed', 'releaseDate': '2028-not-a-date'},
|
|
{'id': 3, 'title': 'Empty', 'releaseDate': ''},
|
|
],
|
|
}),
|
|
),
|
|
);
|
|
|
|
final page = await source.fetchRow(CatalogRowId.upcomingMovies);
|
|
expect(page.items[0].releaseDate, DateTime(2027, 4, 9));
|
|
expect(page.items[0].year, 2027);
|
|
expect(page.items[1].releaseDate, isNull);
|
|
expect(page.items[1].year, 2028);
|
|
expect(page.items[2].releaseDate, isNull);
|
|
expect(page.items[2].year, isNull);
|
|
});
|
|
|
|
test('single-type rows hit their endpoint and coerce the kind', () async {
|
|
late Uri uri;
|
|
final source = _source(
|
|
MockClient((request) async {
|
|
uri = request.url;
|
|
return jsonResponse({
|
|
'page': 2,
|
|
'totalPages': 2,
|
|
'results': [
|
|
{'id': 335984, 'title': 'Blade Runner 2049', 'releaseDate': '2017-10-04'},
|
|
],
|
|
});
|
|
}),
|
|
);
|
|
|
|
final page = await source.fetchRow(CatalogRowId.upcomingMovies, page: 2);
|
|
expect(uri.path, '/api/v1/discover/movies/upcoming');
|
|
expect(uri.queryParameters['page'], '2');
|
|
expect(uri.queryParameters['language'], isNotEmpty);
|
|
expect(page.items.single.kind, MediaKind.movie);
|
|
expect(page.hasMore, isFalse);
|
|
expect(page.totalResults, isNull);
|
|
});
|
|
|
|
test('rows Seerr does not serve throw', () {
|
|
final source = _source(MockClient((request) async => jsonResponse({})));
|
|
expect(() => source.fetchRow(CatalogRowId.watchlist), throwsArgumentError);
|
|
expect(() => source.fetchRow(CatalogRowId.suggestedAnime), throwsArgumentError);
|
|
});
|
|
|
|
test('resolveItemIds needs a tmdb id', () async {
|
|
final source = _source(MockClient((request) async => jsonResponse({})));
|
|
final resolved = await source.resolveItemIds(MediaKind.movie, const ExternalIds(tmdb: 603, imdb: 'tt0133093'));
|
|
expect(resolved?.tmdb, 603);
|
|
expect(resolved?.imdb, 'tt0133093');
|
|
expect(await source.resolveItemIds(MediaKind.movie, const ExternalIds(imdb: 'tt0133093')), isNull);
|
|
});
|
|
|
|
test('fetchDetail runs both GETs concurrently and enriches TV metadata, cast, and recommendations', () async {
|
|
final started = <String>{};
|
|
final bothStarted = Completer<void>();
|
|
final source = _source(
|
|
MockClient((request) async {
|
|
started.add(request.url.path);
|
|
if (!bothStarted.isCompleted && started.length == 2) bothStarted.complete();
|
|
await bothStarted.future;
|
|
|
|
if (request.url.path == '/api/v1/tv/1396/recommendations') {
|
|
return jsonResponse({
|
|
'page': 1,
|
|
'totalPages': 1,
|
|
'results': [
|
|
{'id': 60059, 'name': 'Better Call Saul', 'firstAirDate': '2015-02-08'},
|
|
],
|
|
});
|
|
}
|
|
expect(request.url.path, '/api/v1/tv/1396');
|
|
return jsonResponse({
|
|
'id': 1396,
|
|
'name': 'Breaking Bad',
|
|
'originalName': 'Breaking Bad Original',
|
|
'originalLanguage': 'en',
|
|
'languages': ['en', 'es'],
|
|
'originCountry': ['gb'],
|
|
'firstAirDate': '2008-01-20',
|
|
'lastAirDate': '2013-09-29',
|
|
'episodeRunTime': [47],
|
|
'numberOfEpisodes': 62,
|
|
'genres': [
|
|
{'id': 18, 'name': 'Drama'},
|
|
{'id': 80, 'name': 'Crime'},
|
|
],
|
|
'networks': [
|
|
{'id': 174, 'name': 'AMC'},
|
|
],
|
|
'productionCompanies': [
|
|
{'id': 11073, 'name': 'Sony Pictures Television'},
|
|
],
|
|
'productionCountries': [
|
|
{'iso_3166_1': 'us', 'name': 'United States of America'},
|
|
],
|
|
'status': 'Ended',
|
|
'tagline': 'All bad things must come to an end.',
|
|
'contentRatings': {
|
|
'results': [
|
|
{'iso_3166_1': 'US', 'rating': 'TV-14'},
|
|
],
|
|
},
|
|
'externalIds': {'imdbId': 'tt0903747', 'tvdbId': 81189},
|
|
'createdBy': [
|
|
{'id': 1, 'name': 'Vince Gilligan'},
|
|
],
|
|
'keywords': [
|
|
{'id': 1, 'name': 'new mexico'},
|
|
],
|
|
'credits': {
|
|
'cast': [
|
|
{'name': 'Bryan Cranston', 'character': 'Walter White', 'profilePath': '/bc.jpg'},
|
|
{'name': '', 'character': 'nobody'},
|
|
{'name': 'Aaron Paul', 'character': 'Jesse Pinkman'},
|
|
],
|
|
'crew': [
|
|
{'name': 'Michelle MacLaren', 'job': 'Director', 'department': 'Directing'},
|
|
],
|
|
},
|
|
});
|
|
}),
|
|
);
|
|
|
|
final rowItem = CatalogItem(
|
|
source: CatalogSourceId.seerr,
|
|
kind: MediaKind.show,
|
|
title: 'Row title',
|
|
ids: const CatalogItemIds(tmdb: 1396),
|
|
);
|
|
final detail = await source.fetchDetail(rowItem);
|
|
|
|
expect(started, {'/api/v1/tv/1396', '/api/v1/tv/1396/recommendations'});
|
|
expect(detail.item.title, 'Breaking Bad');
|
|
expect(detail.item.runtimeMinutes, 47);
|
|
expect(detail.item.genres, ['Drama', 'Crime']);
|
|
expect(detail.item.certification, 'TV-14');
|
|
expect(detail.item.airStatus, CatalogAirStatus.ended);
|
|
expect(detail.item.episodeCount, 62);
|
|
expect(detail.item.network, 'AMC');
|
|
expect(detail.item.ids.imdb, 'tt0903747');
|
|
expect(detail.item.ids.tvdb, 81189);
|
|
expect(detail.item.releaseDate, DateTime(2008, 1, 20));
|
|
expect(detail.item.endDate, DateTime(2013, 9, 29));
|
|
expect(detail.item.originalTitle, 'Breaking Bad Original');
|
|
expect(detail.item.tagline, 'All bad things must come to an end.');
|
|
expect(detail.item.studios, ['Sony Pictures Television']);
|
|
expect(detail.item.countries, ['GB', 'US']);
|
|
expect(detail.item.languages, ['en', 'es']);
|
|
expect(detail.item.credits?.map((credit) => credit.name), containsAll(['Vince Gilligan', 'Michelle MacLaren']));
|
|
expect(detail.item.tags?.single.name, 'new mexico');
|
|
|
|
expect(detail.cast, hasLength(2));
|
|
expect(detail.cast.first.name, 'Bryan Cranston');
|
|
expect(detail.cast.first.secondary, 'Walter White');
|
|
expect(detail.cast.first.imageUrl, 'https://image.tmdb.org/t/p/w300/bc.jpg');
|
|
expect(detail.cast.last.imageUrl, isNull);
|
|
expect(detail.related.single.title, 'Better Call Saul');
|
|
expect(detail.related.single.kind, MediaKind.show);
|
|
});
|
|
|
|
test('search proxies /search and filters persons', () async {
|
|
final source = _source(
|
|
MockClient((request) async {
|
|
expect(request.url.path, '/api/v1/search');
|
|
expect(request.url.queryParameters['query'], 'the matrix');
|
|
return jsonResponse({
|
|
'page': 1,
|
|
'totalPages': 1,
|
|
'results': [
|
|
{'id': 603, 'mediaType': 'movie', 'title': 'The Matrix', 'releaseDate': '1999-03-30'},
|
|
{'id': 6384, 'mediaType': 'person', 'name': 'Keanu Reeves'},
|
|
],
|
|
});
|
|
}),
|
|
);
|
|
final items = await source.search('the matrix');
|
|
expect(items.single.title, 'The Matrix');
|
|
expect(items.single.ids.tmdb, 603);
|
|
});
|
|
|
|
test('fetchDetail keeps movie enrichment when recommendations fail', () async {
|
|
final source = _source(
|
|
MockClient((request) async {
|
|
if (request.url.path == '/api/v1/movie/603/recommendations') {
|
|
return jsonResponse({'message': 'recommendations unavailable'}, status: 500);
|
|
}
|
|
expect(request.url.path, '/api/v1/movie/603');
|
|
return jsonResponse({
|
|
'id': 603,
|
|
'imdbId': 'tt0133093',
|
|
'title': 'The Matrix',
|
|
'originalTitle': 'The Matrix Original',
|
|
'originalLanguage': 'en',
|
|
'releaseDate': '1999-03-30',
|
|
'runtime': 136,
|
|
'adult': true,
|
|
'budget': 63000000,
|
|
'revenue': 466000000,
|
|
'genres': [
|
|
{'id': 28, 'name': 'Action'},
|
|
{'id': 878, 'name': 'Science Fiction'},
|
|
],
|
|
'productionCompanies': [
|
|
{'id': 79, 'name': 'Village Roadshow Pictures'},
|
|
],
|
|
'productionCountries': [
|
|
{'name': 'United States of America'},
|
|
],
|
|
'spokenLanguages': [
|
|
{'iso_639_1': 'en', 'name': 'English'},
|
|
],
|
|
'status': 'Released',
|
|
'tagline': 'Welcome to the Real World.',
|
|
'releases': {
|
|
'results': [
|
|
{
|
|
'iso_3166_1': 'CA',
|
|
'release_dates': [
|
|
{'certification': '14A'},
|
|
],
|
|
},
|
|
{
|
|
'iso_3166_1': 'US',
|
|
'release_dates': [
|
|
{'certification': 'R'},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
'relatedVideos': [
|
|
{'site': 'YouTube', 'key': 'clip', 'type': 'Clip'},
|
|
{'site': 'YouTube', 'key': 'm8e-FF8MsqU', 'type': 'Trailer'},
|
|
],
|
|
'credits': {
|
|
'crew': [
|
|
{'name': 'Lana Wachowski', 'job': 'Director', 'department': 'Directing'},
|
|
],
|
|
},
|
|
});
|
|
}),
|
|
);
|
|
final item = CatalogItem(
|
|
source: CatalogSourceId.seerr,
|
|
kind: MediaKind.movie,
|
|
title: 'Row title',
|
|
ids: const CatalogItemIds(tmdb: 603),
|
|
);
|
|
|
|
final detail = await source.fetchDetail(item);
|
|
expect(detail.related, isEmpty);
|
|
expect(detail.item.title, 'The Matrix');
|
|
expect(detail.item.runtimeMinutes, 136);
|
|
expect(detail.item.certification, 'R');
|
|
expect(detail.item.trailerUrl, 'https://www.youtube.com/watch?v=m8e-FF8MsqU');
|
|
expect(detail.item.airStatus, isNull);
|
|
expect(detail.item.ids.imdb, 'tt0133093');
|
|
expect(detail.item.isAdult, isTrue);
|
|
expect(detail.item.budget, 63000000);
|
|
expect(detail.item.revenue, 466000000);
|
|
expect(detail.item.studios, ['Village Roadshow Pictures']);
|
|
expect(detail.item.countries, ['US']);
|
|
expect(detail.item.languages, ['en']);
|
|
expect(detail.item.credits?.single.role, CatalogCreditRole.director);
|
|
});
|
|
|
|
test('fetchDetail keeps recommendations when the detail GET fails', () async {
|
|
final source = _source(
|
|
MockClient((request) async {
|
|
if (request.url.path == '/api/v1/movie/603') {
|
|
return jsonResponse({'message': 'detail unavailable'}, status: 500);
|
|
}
|
|
expect(request.url.path, '/api/v1/movie/603/recommendations');
|
|
return jsonResponse({
|
|
'page': 1,
|
|
'totalPages': 1,
|
|
'results': [
|
|
{'id': 604, 'title': 'The Matrix Reloaded', 'releaseDate': '2003-05-15'},
|
|
],
|
|
});
|
|
}),
|
|
);
|
|
final item = CatalogItem(
|
|
source: CatalogSourceId.seerr,
|
|
kind: MediaKind.movie,
|
|
title: 'The Matrix',
|
|
ids: const CatalogItemIds(tmdb: 603),
|
|
);
|
|
|
|
final detail = await source.fetchDetail(item);
|
|
expect(detail.item, same(item));
|
|
expect(detail.cast, isEmpty);
|
|
expect(detail.related.single.title, 'The Matrix Reloaded');
|
|
expect(detail.related.single.kind, MediaKind.movie);
|
|
});
|
|
|
|
test('canRequest honors the per-kind permission split', () {
|
|
// permissions: 2 = ADMIN in the fixture session → everything allowed.
|
|
final source = _source(MockClient((request) async => jsonResponse({})));
|
|
expect(source.canRequest(MediaKind.movie), isTrue);
|
|
expect(source.canRequest(MediaKind.show), isTrue);
|
|
});
|
|
|
|
test('has no watchlist: membership unknown, mutations unsupported', () async {
|
|
final source = _source(MockClient((request) async => jsonResponse({})));
|
|
expect(source.supportsWatchlist, isFalse);
|
|
expect(source.isOnWatchlist(MediaKind.movie, const CatalogItemIds(tmdb: 603)), isNull);
|
|
expect(() => source.addToWatchlist(MediaKind.movie, const CatalogItemIds(tmdb: 603)), throwsUnsupportedError);
|
|
});
|
|
|
|
test('an untranslated locale degrades to the original title instead of blanking', () async {
|
|
// TMDB answers `language=` for a title it has no translation of with an
|
|
// empty string, not a missing field. Sending the app locale must not
|
|
// turn a populated row into a blank one.
|
|
final source = _source(
|
|
MockClient((request) async {
|
|
return jsonResponse({
|
|
'page': 1,
|
|
'totalPages': 1,
|
|
'totalResults': 1,
|
|
'results': [
|
|
{
|
|
'id': 603,
|
|
'mediaType': 'movie',
|
|
'title': '',
|
|
'originalTitle': 'The Matrix',
|
|
'overview': '',
|
|
'releaseDate': '1999-03-30',
|
|
},
|
|
],
|
|
});
|
|
}),
|
|
);
|
|
|
|
final item = (await source.fetchRow(CatalogRowId.trending)).items.single;
|
|
expect(item.title, 'The Matrix');
|
|
expect(item.overview, isNull, reason: 'a blank overview must not render as an empty block');
|
|
});
|
|
});
|
|
}
|