mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 23:11:45 +02:00
The Material theme now draws its main surfaces with rounded Material Symbols: navigation, player, mini player, queue, settings, sheets, dialogs and shared controls. AppIcon maps legacy Icons to their symbol and keeps outlined glyphs unfilled. Mornye keeps its original icons. Icon buttons, text and filled buttons, dialog actions, sliders, the setup FAB and indeterminate spinners use the Material 3 Expressive components. Each wrapper falls back to the standard Flutter widget for Mornye, reduced motion and custom sizing, so existing layouts and hit areas stay intact.
252 lines
7.6 KiB
Dart
252 lines
7.6 KiB
Dart
import 'package:audio_service/audio_service.dart';
|
|
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:spotiflac_android/l10n/l10n.dart';
|
|
import 'package:spotiflac_android/models/settings.dart';
|
|
import 'package:spotiflac_android/models/track.dart';
|
|
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
|
import 'package:spotiflac_android/providers/music_player_provider.dart';
|
|
import 'package:spotiflac_android/providers/preview_player_provider.dart';
|
|
import 'package:spotiflac_android/providers/settings_provider.dart';
|
|
import 'package:spotiflac_android/widgets/preview_button.dart';
|
|
import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart';
|
|
|
|
import 'support/app_icon_finder.dart';
|
|
|
|
void main() {
|
|
const track = Track(
|
|
id: 'track-1',
|
|
name: 'Track',
|
|
artistName: 'Artist',
|
|
albumName: 'Album',
|
|
previewUrl: 'https://example.com/preview.mp3',
|
|
duration: 180,
|
|
);
|
|
const sparseSearchTrack = Track(
|
|
id: 'search-track',
|
|
name: 'Search Track',
|
|
artistName: 'Artist',
|
|
albumName: '',
|
|
duration: 180,
|
|
source: 'metadata-extension',
|
|
);
|
|
|
|
Future<void> pumpButton(WidgetTester tester, {MediaItem? currentItem}) async {
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
currentMediaItemProvider.overrideWith(
|
|
(ref) => Stream.value(currentItem),
|
|
),
|
|
playbackPlayingProvider.overrideWith((ref) => false),
|
|
],
|
|
child: const MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(body: PreviewButton(track: track)),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
}
|
|
|
|
void expectCenteredHitbox(WidgetTester tester, IconData iconData) {
|
|
final button = find.byWidgetPredicate(
|
|
(widget) => widget is IconButton || widget is M3EIconButton,
|
|
);
|
|
final icon = findAppIcon(iconData);
|
|
final hitbox = tester.getRect(button);
|
|
|
|
expect(hitbox.width, greaterThanOrEqualTo(48));
|
|
expect(hitbox.height, greaterThanOrEqualTo(48));
|
|
expect(tester.getCenter(icon), hitbox.center);
|
|
}
|
|
|
|
testWidgets('preview icon is centered inside its 48dp hitbox', (
|
|
tester,
|
|
) async {
|
|
await pumpButton(tester);
|
|
expectCenteredHitbox(tester, Icons.play_circle_outline_rounded);
|
|
});
|
|
|
|
testWidgets('current-track play icon uses the same centered hitbox', (
|
|
tester,
|
|
) async {
|
|
await pumpButton(
|
|
tester,
|
|
currentItem: const MediaItem(
|
|
id: 'track-1',
|
|
title: 'Track',
|
|
artist: 'Artist',
|
|
),
|
|
);
|
|
expectCenteredHitbox(tester, Icons.play_circle_fill_rounded);
|
|
});
|
|
|
|
testWidgets('preview progress does not rebuild track buttons', (
|
|
tester,
|
|
) async {
|
|
final player = _TestPreviewPlayer();
|
|
var builds = 0;
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
currentMediaItemProvider.overrideWith((ref) => Stream.value(null)),
|
|
previewPlayerProvider.overrideWith(() => player),
|
|
],
|
|
child: MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(
|
|
body: _CountingPreviewButton(track: track, onBuild: () => builds++),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
player.emit(
|
|
PreviewPlayerState(
|
|
activeUrl: track.previewUrl,
|
|
status: PreviewStatus.playing,
|
|
),
|
|
);
|
|
await tester.pump();
|
|
expect(findAppIcon(Icons.pause_circle_filled_rounded), findsOneWidget);
|
|
final playingBuilds = builds;
|
|
for (var second = 1; second <= 3; second++) {
|
|
player.emit(
|
|
PreviewPlayerState(
|
|
activeUrl: track.previewUrl,
|
|
status: PreviewStatus.playing,
|
|
position: Duration(seconds: second),
|
|
duration: const Duration(seconds: 30),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
}
|
|
expect(builds, playingBuilds);
|
|
|
|
player.emit(
|
|
PreviewPlayerState(
|
|
activeUrl: track.previewUrl,
|
|
status: PreviewStatus.paused,
|
|
),
|
|
);
|
|
await tester.pump();
|
|
expect(findAppIcon(Icons.play_circle_fill_rounded), findsOneWidget);
|
|
expect(builds, playingBuilds + 1);
|
|
|
|
player.emit(
|
|
const PreviewPlayerState(
|
|
activeUrl: 'https://example.com/another-preview.mp3',
|
|
status: PreviewStatus.playing,
|
|
),
|
|
);
|
|
await tester.pump();
|
|
expect(findAppIcon(Icons.play_circle_outline_rounded), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('overflow menu icon is centered in its adjacent hitbox', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
const ProviderScope(
|
|
child: MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(body: TrackCollectionQuickActions(track: track)),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expectCenteredHitbox(tester, Icons.more_vert);
|
|
});
|
|
|
|
testWidgets('track options expose Go to Album for album tracks', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
settingsProvider.overrideWith(_TestSettingsNotifier.new),
|
|
libraryCollectionsProvider.overrideWith(
|
|
_TestLibraryCollectionsNotifier.new,
|
|
),
|
|
],
|
|
child: const MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(body: TrackCollectionQuickActions(track: track)),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(findAppIcon(Icons.more_vert));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Go to Album'), findsOneWidget);
|
|
expect(findAppIcon(Icons.album_outlined), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'search track options keep Go to Album when album data is sparse',
|
|
(tester) async {
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
settingsProvider.overrideWith(_TestSettingsNotifier.new),
|
|
libraryCollectionsProvider.overrideWith(
|
|
_TestLibraryCollectionsNotifier.new,
|
|
),
|
|
],
|
|
child: const MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(
|
|
body: TrackCollectionQuickActions(track: sparseSearchTrack),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(findAppIcon(Icons.more_vert));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Go to Album'), findsOneWidget);
|
|
expect(findAppIcon(Icons.album_outlined), findsOneWidget);
|
|
},
|
|
);
|
|
}
|
|
|
|
class _TestSettingsNotifier extends SettingsNotifier {
|
|
@override
|
|
AppSettings build() => const AppSettings();
|
|
}
|
|
|
|
class _TestPreviewPlayer extends PreviewPlayerController {
|
|
@override
|
|
PreviewPlayerState build() => const PreviewPlayerState();
|
|
|
|
void emit(PreviewPlayerState next) => state = next;
|
|
}
|
|
|
|
class _CountingPreviewButton extends PreviewButton {
|
|
const _CountingPreviewButton({required super.track, required this.onBuild});
|
|
|
|
final VoidCallback onBuild;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
onBuild();
|
|
return super.build(context, ref);
|
|
}
|
|
}
|
|
|
|
class _TestLibraryCollectionsNotifier extends LibraryCollectionsNotifier {
|
|
@override
|
|
LibraryCollectionsState build() => LibraryCollectionsState(isLoaded: true);
|
|
}
|