Files
SpotiFLAC-Mobile/test/equalizer_test.dart
T

235 lines
8.4 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_3_expressive/material_3_expressive.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/models/equalizer_settings.dart';
import 'package:spotiflac_android/providers/equalizer_provider.dart';
import 'package:spotiflac_android/screens/equalizer_screen.dart';
import 'package:spotiflac_android/services/playback_equalizer.dart';
import 'package:spotiflac_android/theme/app_theme.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/app_slider.dart';
import 'package:spotiflac_android/widgets/mornye_chrome.dart';
import 'package:spotiflac_android/widgets/mornye_level_slider.dart';
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
playbackEqualizer.value = const EqualizerSettings();
playbackEqualizerError.value = null;
});
test('band resampling preserves flat curves and center gains', () {
for (final count in EqualizerSettings.bandCounts) {
final settings = const EqualizerSettings().withBandCount(count);
expect(settings.gains, everyElement(0));
expect(settings.frequencies.length, count);
final raised = settings.copyWith(gains: List.filled(count, 6));
for (final hz in raised.frequencies) {
expect(raised.gainAt(hz), closeTo(6, 1e-9));
}
expect(raised.withBandCount(31).gains, everyElement(closeTo(6, 1e-9)));
}
expect(const EqualizerSettings().withBandCount(31).frequencies.take(3), [
20,
25,
31.5,
]);
expect(
() => const EqualizerSettings().withBandCount(32),
throwsFormatException,
);
});
test('tone controls and automatic headroom reach the native payload', () {
const settings = EqualizerSettings(
enabled: true,
bass: 6,
treble: -3,
preamp: -2,
);
final payload = settings.toNative();
expect(settings.effectiveGains.first, greaterThan(5));
expect(settings.effectiveGains.last, lessThan(-2));
expect(payload['inputGain'], lessThan(-7));
expect(payload['gains'], settings.effectiveGains);
expect(settings.copyWith(autoHeadroom: false).inputGain, -2);
});
test(
'malformed and nonfinite imports cannot create unsafe native settings',
() {
final json = const EqualizerSettings().toJson();
expect(
() => EqualizerSettings.fromJson({
...json,
'gains': [1, 2],
}),
throwsFormatException,
);
expect(
() => EqualizerSettings.fromJson({
...json,
'gains': List.filled(10, double.nan),
}),
throwsFormatException,
);
final bounded = EqualizerSettings.fromJson({
...json,
'gains': List.filled(10, 100),
'preamp': 1000,
'balance': -100,
'ratio': double.infinity,
});
expect(bounded.gains, everyElement(15));
expect(bounded.preamp, 12);
expect(bounded.balance, -1);
expect(bounded.ratio, 3);
},
);
testWidgets('presets survive fast edits during startup and reload', (
tester,
) async {
SharedPreferences.setMockInitialValues({
EqualizerController.storageKey: jsonEncode({
'settings': const EqualizerSettings(preamp: -4).toJson(),
'presets': [
const EqualizerPreset(
'Headphones',
EqualizerSettings(bass: 3),
).toJson(),
],
}),
});
final container = ProviderContainer();
container.read(equalizerProvider);
container
.read(equalizerProvider.notifier)
.update(const EqualizerSettings(enabled: true, treble: 2));
await tester.pump(const Duration(milliseconds: 500));
await tester.runAsync(() async {
await Future<void>.delayed(Duration.zero);
});
await tester.pump(const Duration(milliseconds: 500));
expect(container.read(equalizerProvider).settings.treble, 2);
expect(container.read(equalizerProvider).presets.single.name, 'Headphones');
final notifier = container.read(equalizerProvider.notifier);
notifier.savePreset('Desk');
notifier.update(const EqualizerSettings(enabled: true, preamp: -8));
final export = notifier.exportPreset('Portable');
notifier.importPreset(export);
expect(container.read(equalizerProvider).presets.map((v) => v.name), [
'Headphones',
'Desk',
'Portable',
]);
expect(container.read(equalizerProvider).settings.enabled, isTrue);
expect(() => notifier.importPreset('{}'), throwsFormatException);
expect(() => notifier.importPreset('x' * 65537), throwsFormatException);
await tester.pump(const Duration(milliseconds: 500));
await tester.runAsync(() async {
await Future<void>.delayed(Duration.zero);
});
container.dispose();
final restored = ProviderContainer();
addTearDown(restored.dispose);
restored.read(equalizerProvider);
await tester.runAsync(() async {
await Future<void>.delayed(Duration.zero);
});
await tester.pump();
expect(restored.read(equalizerProvider).settings.preamp, -8);
expect(restored.read(equalizerProvider).presets.length, 3);
});
for (final mornye in [false, true]) {
testWidgets(
'EQ editing and dialogs fit a narrow screen with large text ($mornye)',
(tester) async {
tester.view.physicalSize = const Size(320, 720);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final container = ProviderContainer();
addTearDown(container.dispose);
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: MaterialApp(
theme: mornye
? MornyeTheme.build(Brightness.dark)
: AppTheme.light(),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: const TextScaler.linear(1.8)),
child: child!,
),
home: const EqualizerScreen(),
),
),
);
await tester.pumpAndSettle();
final picker = find.byType(
mornye ? MornyeSegmentedControl : M3EButtonGroup,
);
await tester.scrollUntilVisible(
picker,
150,
scrollable: find.byType(Scrollable).first,
);
await tester.pumpAndSettle();
await tester.tap(
find.descendant(of: picker, matching: find.text('31')).first,
);
await tester.pumpAndSettle();
expect(container.read(equalizerProvider).settings.gains.length, 31);
expect(tester.takeException(), isNull);
// The first slider on the page is the lowest band's fader.
final onChanged = mornye
? tester
.widgetList<MornyeLevelSlider>(find.byType(MornyeLevelSlider))
.first
.onChanged
: tester
.widgetList<AppSlider>(find.byType(AppSlider))
.first
.onChanged;
onChanged!(5);
await tester.pump();
expect(container.read(equalizerProvider).settings.gains.first, 5);
// Drags over the vertical faders adjust gains instead of scrolling.
tester
.state<ScrollableState>(find.byType(Scrollable).first)
.position
.jumpTo(0);
await tester.pumpAndSettle();
final save = find.text('Save preset');
await tester.scrollUntilVisible(
save,
150,
scrollable: find.byType(Scrollable).first,
);
await tester.pumpAndSettle();
await tester.tap(save);
await tester.pumpAndSettle();
await tester.enterText(find.byType(EditableText), 'My headphones');
await tester.tap(find.text('Save').last);
await tester.pumpAndSettle();
expect(
container.read(equalizerProvider).presets.single.name,
'My headphones',
);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox.shrink());
},
);
}
}