Files
SpotiFLAC-Mobile/test/selection_overlay_lifecycle_test.dart
zarzet ba44bafa92 fix: sync lyric romanization and clear stale selection menus
Preserve romanization and translation metadata through Rust exports and cache, match rounded line times, and retain word endings. Highlight romanization with the main lyric clock in Material and Mornye.

Clear selection overlay ownership safely when collection screens are disposed. Add parser, rendering, cache and overlay lifecycle regression coverage.
2026-09-23 12:42:08 +07:00

180 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/theme/app_theme.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/collection_scaffold.dart';
import 'package:spotiflac_android/widgets/selection_bottom_bar.dart';
Widget _app(ThemeData theme, GlobalKey<NavigatorState> navigator) {
return ProviderScope(
child: MaterialApp(
theme: theme,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: SelectionOverlayHost(
child: Navigator(
key: navigator,
onGenerateRoute: (_) => MaterialPageRoute<void>(
builder: (_) => const Scaffold(body: Text('Library')),
),
),
),
),
);
}
void main() {
for (final mornye in [false, true]) {
final name = mornye ? 'Mornye' : 'Material';
final theme = mornye
? MornyeTheme.build(Brightness.light)
: AppTheme.light();
testWidgets('$name: leaving a collection removes its selection bar', (
tester,
) async {
final navigator = GlobalKey<NavigatorState>();
final scroll = ScrollController();
addTearDown(scroll.dispose);
await tester.pumpWidget(_app(theme, navigator));
navigator.currentState!.push(
MaterialPageRoute<void>(
builder: (_) => CollectionScaffold(
scrollController: scroll,
isSelectionMode: true,
onExitSelectionMode: () {},
appBar: const SliverAppBar(title: Text('Album')),
slivers: const [],
selectionBar: const Text('Delete 1 track'),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Delete 1 track'), findsOneWidget);
navigator.currentState!.pop();
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
expect(find.text('Library'), findsOneWidget);
expect(find.text('Delete 1 track'), findsNothing);
});
testWidgets('$name: an empty album removes its old selection bar', (
tester,
) async {
final navigator = GlobalKey<NavigatorState>();
final scroll = ScrollController();
final hasTracks = ValueNotifier(true);
addTearDown(scroll.dispose);
addTearDown(hasTracks.dispose);
await tester.pumpWidget(_app(theme, navigator));
navigator.currentState!.push(
MaterialPageRoute<void>(
builder: (_) => ValueListenableBuilder<bool>(
valueListenable: hasTracks,
builder: (_, hasTracks, _) => hasTracks
? CollectionScaffold(
scrollController: scroll,
isSelectionMode: true,
onExitSelectionMode: () {},
appBar: const SliverAppBar(title: Text('Album')),
slivers: const [],
selectionBar: const Text('Delete 1 track'),
)
: const Scaffold(body: Text('No tracks found for this album')),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Delete 1 track'), findsOneWidget);
hasTracks.value = false;
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
expect(find.text('No tracks found for this album'), findsOneWidget);
expect(find.text('Delete 1 track'), findsNothing);
navigator.currentState!.pop();
await tester.pumpAndSettle();
expect(find.text('Library'), findsOneWidget);
});
testWidgets(
'$name: leaving Songs clears the bar and allows selecting again',
(tester) async {
final navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(_app(theme, navigator));
for (var visit = 0; visit < 2; visit++) {
navigator.currentState!.push(
MaterialPageRoute<void>(
builder: (_) => const _SongsSelectionPage(),
),
);
await tester.pumpAndSettle();
await tester.longPress(find.text('Song'));
await tester.pumpAndSettle();
expect(find.byType(SelectionBottomBar), findsOneWidget);
if (visit == 1) {
await tester.tap(find.byTooltip('Close'));
await tester.pumpAndSettle();
expect(find.byType(SelectionBottomBar), findsNothing);
}
await tester.tap(find.byTooltip('Back'));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
expect(find.text('Library'), findsOneWidget);
expect(find.byType(SelectionBottomBar), findsNothing);
}
},
);
}
}
// Songs owns a controller directly and hides it in dispose, whereas album
// screens delegate that lifecycle to CollectionScaffold.
class _SongsSelectionPage extends StatefulWidget {
const _SongsSelectionPage();
@override
State<_SongsSelectionPage> createState() => _SongsSelectionPageState();
}
class _SongsSelectionPageState extends State<_SongsSelectionPage> {
final _selectionOverlay = SelectionOverlayController();
@override
void dispose() {
_selectionOverlay.hide();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Songs'),
leading: BackButton(onPressed: () => Navigator.pop(context)),
),
body: ListTile(
title: const Text('Song'),
onLongPress: () => _selectionOverlay.show(
context,
(_) => SelectionBottomBar(
selectedCount: 1,
allSelected: true,
onClose: _selectionOverlay.hide,
onToggleSelectAll: _selectionOverlay.hide,
bottomPadding: 0,
children: const [Text('Delete 1 track')],
),
),
),
);
}
}