On a car head unit the custom actions render icon-first, so two actions sharing ic_stat_pluriwave were indistinguishable and the toggle gave no sign of whether the equalizer was on. Each action now has its own drawable, and the toggle swaps between ic_auto_eq_on and ic_auto_eq_off so its state is legible at a glance.
257 lines
8.4 KiB
Dart
257 lines
8.4 KiB
Dart
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,
|
|
presetActual: PresetEcualizador.flat,
|
|
l10n: l10n,
|
|
);
|
|
|
|
expect(controles, isEmpty);
|
|
});
|
|
|
|
test(
|
|
'exactly 2 custom actions when available: on/off toggle + preset '
|
|
'cycle -- Android Auto shows a limited number of custom actions, so '
|
|
'this is deliberately NOT one action per preset',
|
|
() {
|
|
final controles = controlesEcualizadorPersonalizados(
|
|
disponible: true,
|
|
activo: true,
|
|
presetActual: PresetEcualizador.rock,
|
|
l10n: l10n,
|
|
);
|
|
|
|
expect(controles, hasLength(2));
|
|
expect(controles.every((c) => c.action == MediaAction.custom), isTrue);
|
|
},
|
|
);
|
|
|
|
test('toggle label reflects ON -> shows "disable" action', () {
|
|
final controles = controlesEcualizadorPersonalizados(
|
|
disponible: true,
|
|
activo: true,
|
|
presetActual: PresetEcualizador.flat,
|
|
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,
|
|
presetActual: PresetEcualizador.flat,
|
|
l10n: l10n,
|
|
);
|
|
final toggle = controles.firstWhere(
|
|
(c) => c.customAction?.name == accionEqToggle,
|
|
);
|
|
|
|
expect(toggle.label, l10n.eqCustomActionEnableLabel);
|
|
});
|
|
|
|
test('preset-cycle label shows the CURRENT preset localized name', () {
|
|
final controles = controlesEcualizadorPersonalizados(
|
|
disponible: true,
|
|
activo: true,
|
|
presetActual: PresetEcualizador.jazz,
|
|
l10n: l10n,
|
|
);
|
|
final ciclo = controles.firstWhere(
|
|
(c) => c.customAction?.name == accionEqPresetSiguiente,
|
|
);
|
|
|
|
expect(
|
|
ciclo.label,
|
|
l10n.eqCustomActionPresetLabel(l10n.equalizerPresetJazz),
|
|
);
|
|
});
|
|
|
|
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,
|
|
presetActual: PresetEcualizador.flat,
|
|
l10n: l10n,
|
|
).firstWhere((c) => c.customAction?.name == accionEqToggle);
|
|
final desactivado = controlesEcualizadorPersonalizados(
|
|
disponible: true,
|
|
activo: false,
|
|
presetActual: PresetEcualizador.flat,
|
|
l10n: l10n,
|
|
).firstWhere((c) => c.customAction?.name == accionEqToggle);
|
|
|
|
expect(activado.androidIcon, 'drawable/ic_auto_eq_on');
|
|
expect(desactivado.androidIcon, 'drawable/ic_auto_eq_off');
|
|
},
|
|
);
|
|
|
|
test(
|
|
'the toggle and the preset-cycle action never share an androidIcon, '
|
|
'in either EQ state -- this was the on-device bug: both buttons used '
|
|
'the same drawable and were visually indistinguishable',
|
|
() {
|
|
for (final activo in [true, false]) {
|
|
final controles = controlesEcualizadorPersonalizados(
|
|
disponible: true,
|
|
activo: activo,
|
|
presetActual: PresetEcualizador.flat,
|
|
l10n: l10n,
|
|
);
|
|
final iconos = controles.map((c) => c.androidIcon).toSet();
|
|
|
|
expect(
|
|
iconos.length,
|
|
controles.length,
|
|
reason:
|
|
'every custom action must have a distinct androidIcon '
|
|
'(activo=$activo)',
|
|
);
|
|
}
|
|
},
|
|
);
|
|
});
|
|
|
|
group('action name constants (item 4 — collision-free with car-tree ids)', () {
|
|
test('names are non-empty, distinct, and do not collide with any '
|
|
'existing browse-tree media-id prefix', () {
|
|
expect(accionEqToggle, isNotEmpty);
|
|
expect(accionEqPresetSiguiente, isNotEmpty);
|
|
expect(accionEqToggle, isNot(equals(accionEqPresetSiguiente)));
|
|
});
|
|
});
|
|
|
|
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, ic_auto_eq_off and ic_auto_eq_preset all 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',
|
|
() {
|
|
for (final nombre in [
|
|
'ic_auto_eq_on',
|
|
'ic_auto_eq_off',
|
|
'ic_auto_eq_preset',
|
|
]) {
|
|
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/',
|
|
);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|