fix: eliminate cross-app consistency drift

This commit is contained in:
edde746
2026-07-13 23:13:53 +02:00
parent ec249d2ed4
commit d5f3e581df
142 changed files with 4751 additions and 1075 deletions
+83
View File
@@ -0,0 +1,83 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:logger/logger.dart';
import 'package:plezy/exceptions/media_server_exceptions.dart';
import 'package:plezy/i18n/strings.g.dart';
import 'package:plezy/utils/app_logger.dart' as logging;
import 'package:plezy/utils/error_message_utils.dart';
void main() {
late Logger originalLogger;
late _RecordingLogOutput logOutput;
setUpAll(() async => LocaleSettings.setLocale(AppLocale.es));
tearDownAll(() => LocaleSettings.setLocaleSync(AppLocale.en));
setUp(() {
originalLogger = logging.appLogger;
logOutput = _RecordingLogOutput();
logging.appLogger = Logger(printer: SimplePrinter(), output: logOutput);
});
tearDown(() {
logging.appLogger = originalLogger;
});
group('localizedLoadErrorMessage', () {
test('distinguishes connection timeouts from connection failures', () {
for (final type in [MediaServerHttpErrorType.connectionTimeout, MediaServerHttpErrorType.receiveTimeout]) {
final message = localizedLoadErrorMessage(
MediaServerHttpException(type: type, message: 'private timeout detail'),
StackTrace.current,
context: 'Biblioteca',
);
expect(message, 'Tiempo de conexión agotado al cargar Biblioteca');
expect(message, isNot(contains('private timeout detail')));
}
final connectionMessage = localizedLoadErrorMessage(
MediaServerHttpException(type: MediaServerHttpErrorType.connectionError, message: 'private connection detail'),
StackTrace.current,
context: 'Biblioteca',
);
expect(connectionMessage, 'No se puede conectar al servidor multimedia');
expect(connectionMessage, isNot(contains('private connection detail')));
});
test('logs an unexpected failure once and returns only the localized fallback', () {
const privateDetail = 'token=super-secret';
final message = localizedLoadErrorMessage(StateError(privateDetail), StackTrace.current, context: 'Biblioteca');
expect(message, 'No se pudo cargar Biblioteca. Inténtalo de nuevo.');
expect(message, isNot(contains(privateDetail)));
expect(logOutput.writeCount, 1);
});
test('never exposes server status text, response data, or exception messages', () {
const serverText = 'database failed for user@example.com';
final error = MediaServerHttpException(
type: MediaServerHttpErrorType.unknown,
statusCode: 500,
message: serverText,
responseData: {'error': serverText},
);
final message = localizedLoadErrorMessage(error, StackTrace.current, context: 'Biblioteca');
expect(message, 'No se pudo cargar Biblioteca. Inténtalo de nuevo.');
expect(message, isNot(contains('500')));
expect(message, isNot(contains(serverText)));
});
});
}
class _RecordingLogOutput extends LogOutput {
int writeCount = 0;
@override
void output(OutputEvent event) {
writeCount++;
}
}
+80
View File
@@ -1,9 +1,15 @@
import 'package:cached_network_image_ce/cached_network_image.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_server_client.dart';
import 'package:plezy/services/device_performance.dart';
import 'package:plezy/services/settings_service.dart';
import 'package:plezy/utils/media_image_helper.dart';
import '../test_helpers/media_items.dart';
/// Only [thumbnailUrl] is exercised; everything else throws via noSuchMethod.
class _SizedUrlFakeClient implements MediaServerClient {
@override
@@ -14,6 +20,8 @@ class _SizedUrlFakeClient implements MediaServerClient {
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
MediaItem _item(MediaKind kind) => testMediaItem(kind: kind);
void main() {
group('MediaImageHelper.getOptimizedImageUrl', () {
test('adds size hints to absolute Jellyfin artwork URLs', () {
@@ -134,6 +142,78 @@ void main() {
});
});
group('MediaImageHelper image type budgets', () {
tearDown(DevicePerformance.debugReset);
test('hero logos get a larger budget without changing ordinary logos', () {
DevicePerformance.debugReset(autoReduced: false, override: VisualEffectsSetting.auto);
expect(
MediaImageHelper.getMemCacheDimensions(displayWidth: 4000, displayHeight: 4000, imageType: ImageType.logo),
(600, 300),
);
expect(
MediaImageHelper.getMemCacheDimensions(displayWidth: 4000, displayHeight: 4000, imageType: ImageType.heroLogo),
(1000, 500),
);
});
test('card artwork follows square, wide, and poster media shapes', () {
for (final kind in [MediaKind.artist, MediaKind.album, MediaKind.track]) {
expect(
MediaImageHelper.cardImageType(_item(kind), EpisodePosterMode.episodeThumbnail),
ImageType.square,
reason: '${kind.id} artwork must keep its square music cache budget',
);
}
expect(
MediaImageHelper.cardImageType(_item(MediaKind.episode), EpisodePosterMode.episodeThumbnail),
ImageType.thumb,
);
expect(
MediaImageHelper.cardImageType(_item(MediaKind.episode), EpisodePosterMode.seriesPoster),
ImageType.poster,
);
expect(
MediaImageHelper.cardImageType(_item(MediaKind.movie), EpisodePosterMode.episodeThumbnail),
ImageType.poster,
);
});
});
group('MediaImageHelper.serverArtworkProvider', () {
test('reuses cache identity and disk key while bounding both decode axes', () {
const url = 'https://example.invalid/library/poster.jpg';
final first = MediaImageHelper.serverArtworkProvider(imageUrl: url, memWidth: 640, memHeight: 360) as ResizeImage;
final second =
MediaImageHelper.serverArtworkProvider(imageUrl: url, memWidth: 640, memHeight: 360) as ResizeImage;
final firstCached = first.imageProvider as CachedNetworkImageProvider;
final secondCached = second.imageProvider as CachedNetworkImageProvider;
expect(firstCached, secondCached);
expect(first.width, second.width);
expect(first.width, 640);
expect(first.height, second.height);
expect(first.height, 360);
expect(first.policy, second.policy);
expect(first.policy, ResizeImagePolicy.fit);
expect(first.allowUpscaling, second.allowUpscaling);
expect(first.allowUpscaling, isFalse);
expect(firstCached.url, secondCached.url);
expect(firstCached.url, url);
expect(firstCached.headers, secondCached.headers);
expect(firstCached.headers, const {'User-Agent': 'Plezy'});
expect(firstCached.cacheKey, secondCached.cacheKey);
expect(firstCached.cacheKey, isNotNull);
expect(firstCached.maxWidth, secondCached.maxWidth);
expect(firstCached.maxWidth, isNull);
expect(firstCached.maxHeight, secondCached.maxHeight);
expect(firstCached.maxHeight, isNull);
});
});
group('MediaImageHelper.boundedDecode', () {
test('bounds both axes with fit policy (no distortion, no upscale)', () {
const base = NetworkImage('https://example/img');