import 'dart:io'; import 'dart:ui' show Locale; import 'package:audio_service/audio_service.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/modelos/preset_ecualizador.dart'; import 'package:pluriwave/servicios/servicio_audio.dart'; /// Item 4 (Android Auto: equalizer custom actions) — the pure, handler- /// independent half of the fix. `PluriWaveAudioHandler` cannot be /// instantiated in unit tests (a real `just_audio.AudioPlayer` requires /// platform MethodChannels), so the preset-cycling decision, the preset-name /// localization and the `MediaControl` list construction are extracted as /// pure top-level functions here. The handler's own `customAction` dispatch /// and `playbackState` wiring are static-review-only, same as the existing /// EQ re-apply/session-id wiring. void main() { final l10n = lookupAppLocalizations(const Locale('es')); group('presetSiguiente (item 4 — cycling presets)', () { test('advances to the next preset in order', () { expect(presetSiguiente(PresetEcualizador.flat), PresetEcualizador.rock); expect(presetSiguiente(PresetEcualizador.rock), PresetEcualizador.pop); }); test('wraps around after the last preset', () { expect( presetSiguiente(PresetEcualizador.presets.last), PresetEcualizador.presets.first, ); }); test('an unknown/custom preset (e.g. a user-tweaked "Personalizado" band ' 'set) starts from the FIRST preset instead of throwing', () { final personalizado = PresetEcualizador( nombre: 'Personalizado', bandas: [1.0, 2.0, 3.0, 4.0, 5.0], ); expect(presetSiguiente(personalizado), PresetEcualizador.presets.first); }); test('respects an injected presets list instead of the default 6', () { final propios = [PresetEcualizador.jazz, PresetEcualizador.voz]; expect( presetSiguiente(PresetEcualizador.jazz, presets: propios), PresetEcualizador.voz, ); expect( presetSiguiente(PresetEcualizador.voz, presets: propios), PresetEcualizador.jazz, ); }); }); group('nombrePresetVisible (item 4)', () { test('maps every factory preset name to its localized ARB string', () { expect(nombrePresetVisible(l10n, 'Flat'), l10n.equalizerPresetFlat); expect(nombrePresetVisible(l10n, 'Rock'), l10n.equalizerPresetRock); expect(nombrePresetVisible(l10n, 'Pop'), l10n.equalizerPresetPop); expect( nombrePresetVisible(l10n, 'Bass Boost'), l10n.equalizerPresetBassBoost, ); expect(nombrePresetVisible(l10n, 'Jazz'), l10n.equalizerPresetJazz); expect(nombrePresetVisible(l10n, 'Voz'), l10n.equalizerPresetVoice); expect( nombrePresetVisible(l10n, 'Personalizado'), l10n.equalizerPresetCustom, ); }); test('an unrecognized name falls through verbatim', () { expect( nombrePresetVisible(l10n, 'Mi Preset Guardado'), 'Mi Preset Guardado', ); }); }); group('controlesEcualizadorPersonalizados (item 4)', () { test('empty when the equalizer is not available on this device', () { final controles = controlesEcualizadorPersonalizados( disponible: false, activo: true, l10n: l10n, ); expect(controles, isEmpty); }); test('exactly 1 custom action when available: the on/off toggle -- ' 'decision `auto/ecualizador-diseno` REMOVES the preset-cycling ' 'action that used to sit alongside it; preset selection now lives ' 'in the "Ecualizador" browsable folder instead (see ' '`itemsEcualizadorAuto`)', () { final controles = controlesEcualizadorPersonalizados( disponible: true, activo: true, l10n: l10n, ); expect(controles, hasLength(1)); expect(controles.single.action, MediaAction.custom); expect(controles.single.customAction?.name, accionEqToggle); }); test('toggle label reflects ON -> shows "disable" action', () { final controles = controlesEcualizadorPersonalizados( disponible: true, activo: true, l10n: l10n, ); final toggle = controles.firstWhere( (c) => c.customAction?.name == accionEqToggle, ); expect(toggle.label, l10n.eqCustomActionDisableLabel); }); test('toggle label reflects OFF -> shows "enable" action', () { final controles = controlesEcualizadorPersonalizados( disponible: true, activo: false, l10n: l10n, ); final toggle = controles.firstWhere( (c) => c.customAction?.name == accionEqToggle, ); expect(toggle.label, l10n.eqCustomActionEnableLabel); }); test('toggle icon reflects EQ state: ON uses ic_auto_eq_on, OFF uses ' 'ic_auto_eq_off -- a car head unit foregrounds the icon over the ' 'label, so the icon itself must change, not just the text', () { final activado = controlesEcualizadorPersonalizados( disponible: true, activo: true, l10n: l10n, ).firstWhere((c) => c.customAction?.name == accionEqToggle); final desactivado = controlesEcualizadorPersonalizados( disponible: true, activo: false, l10n: l10n, ).firstWhere((c) => c.customAction?.name == accionEqToggle); expect(activado.androidIcon, 'drawable/ic_auto_eq_on'); expect(desactivado.androidIcon, 'drawable/ic_auto_eq_off'); }); }); group( 'action name constants (item 4 -- collision-free with car-tree ids)', () { test('accionEqToggle is non-empty and does not collide with any ' 'existing browse-tree media-id prefix -- accionEqPresetSiguiente ' '(decision `auto/ecualizador-diseno`: removed, superseded by the ' '"Ecualizador" browsable folder) no longer exists as a symbol at ' 'all, which this file compiling proves on its own', () { expect(accionEqToggle, isNotEmpty); }); }, ); group('equalizer drawable assets on disk (on-device feedback follow-up: the ' 'two custom actions used to share one drawable and were visually ' 'indistinguishable)', () { test('ic_auto_eq_on and ic_auto_eq_off exist under ' 'android/app/src/main/res/drawable/ -- a missing drawable is not a ' 'build error, it silently renders blank/default on the head unit, ' 'so this is the only safety net that would have caught the ' 'original duplication. ic_auto_eq_preset is deliberately NOT ' 'checked here anymore -- decision `auto/ecualizador-diseno` ' 'removes the preset-cycling action and its drawable', () { for (final nombre in ['ic_auto_eq_on', 'ic_auto_eq_off']) { final archivo = File('android/app/src/main/res/drawable/$nombre.xml'); expect( archivo.existsSync(), isTrue, reason: '$nombre.xml must exist under ' 'android/app/src/main/res/drawable/', ); } expect( File( 'android/app/src/main/res/drawable/ic_auto_eq_preset.xml', ).existsSync(), isFalse, reason: 'ic_auto_eq_preset.xml must be REMOVED -- decision ' '`auto/ecualizador-diseno` retires the preset-cycling ' 'custom action it belonged to', ); }); }); }