mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 23:11:45 +02:00
235 lines
8.4 KiB
Dart
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());
|
|
},
|
|
);
|
|
}
|
|
}
|