mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-28 20:32:15 +02:00
713 lines
25 KiB
Dart
713 lines
25 KiB
Dart
import 'dart:ui' as ui;
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:liquid_glass_easy/liquid_glass_easy.dart';
|
|
import 'package:spotiflac_android/l10n/l10n.dart';
|
|
import 'package:spotiflac_android/providers/music_player_provider.dart';
|
|
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
|
|
import 'package:spotiflac_android/screens/settings/settings_tab.dart';
|
|
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
|
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
|
|
import 'package:spotiflac_android/widgets/album_detail_header.dart';
|
|
import 'package:spotiflac_android/widgets/collection_scaffold.dart';
|
|
import 'package:spotiflac_android/widgets/mini_player.dart';
|
|
import 'package:spotiflac_android/widgets/mornye_artist_header.dart';
|
|
import 'package:spotiflac_android/widgets/mornye_bottom_bar.dart';
|
|
import 'package:spotiflac_android/widgets/mornye_chrome.dart';
|
|
|
|
void main() {
|
|
late MornyeChromeController chrome;
|
|
late ScrollController scroll;
|
|
late int searches;
|
|
final capture = GlobalKey();
|
|
|
|
setUp(() {
|
|
chrome = MornyeChromeController();
|
|
scroll = ScrollController();
|
|
searches = 0;
|
|
});
|
|
tearDown(() {
|
|
chrome.dispose();
|
|
scroll.dispose();
|
|
});
|
|
|
|
Future<void> pumpShell(
|
|
WidgetTester tester, {
|
|
bool withPlayer = true,
|
|
bool blur = false,
|
|
Brightness brightness = Brightness.light,
|
|
Color? backdrop,
|
|
Color? chromeSurface,
|
|
Widget? body,
|
|
ValueNotifier<int>? activeTab,
|
|
}) async {
|
|
tester.view.physicalSize = const Size(393, 760);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
currentMediaItemProvider.overrideWith(
|
|
(ref) => Stream.value(
|
|
withPlayer
|
|
? const MediaItem(
|
|
id: 'track',
|
|
title: 'A song',
|
|
artist: 'Artist',
|
|
)
|
|
: null,
|
|
),
|
|
),
|
|
playbackStateProvider.overrideWith((ref) => const Stream.empty()),
|
|
lowEndDeviceProvider.overrideWithValue(!blur),
|
|
backdropBlurEnabledProvider.overrideWithValue(blur),
|
|
],
|
|
child: MaterialApp(
|
|
theme: MornyeTheme.build(brightness, chromeSurface: chromeSurface),
|
|
builder: (_, child) => RepaintBoundary(key: capture, child: child),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(
|
|
backgroundColor: backdrop,
|
|
extendBody: true,
|
|
body: NotificationListener<ScrollNotification>(
|
|
onNotification: chrome.handleScroll,
|
|
child:
|
|
body ??
|
|
ListView.builder(
|
|
controller: scroll,
|
|
itemExtent: 60,
|
|
itemCount: 50,
|
|
itemBuilder: (_, index) => Text('Row $index'),
|
|
),
|
|
),
|
|
bottomNavigationBar: ListenableBuilder(
|
|
listenable: Listenable.merge([chrome, ?activeTab]),
|
|
builder: (_, _) => MornyeBottomBar(
|
|
collapsed: chrome.value,
|
|
destinations: const [
|
|
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.music_note),
|
|
label: 'Library',
|
|
),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.grid_view),
|
|
label: 'Repo',
|
|
),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.search),
|
|
label: 'Search',
|
|
),
|
|
],
|
|
selectedIndex: activeTab?.value ?? 0,
|
|
onSelected: (index) {
|
|
activeTab?.value = index;
|
|
chrome.expand();
|
|
},
|
|
onHome: () {
|
|
activeTab?.value = 0;
|
|
chrome.expand();
|
|
},
|
|
onSearch: () {
|
|
searches++;
|
|
activeTab?.value = 3;
|
|
},
|
|
blurEnabled: blur,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
const albumBlue = Color(0xff464566);
|
|
|
|
for (final blur in [false, true]) {
|
|
testWidgets('active edge icons and tabs stay synchronized (glass: $blur)', (
|
|
tester,
|
|
) async {
|
|
final activeTab = ValueNotifier(0);
|
|
addTearDown(activeTab.dispose);
|
|
await pumpShell(
|
|
tester,
|
|
blur: blur,
|
|
activeTab: activeTab,
|
|
brightness: Brightness.dark,
|
|
);
|
|
final scheme = MornyeTheme.build(Brightness.dark).colorScheme;
|
|
final primary = scheme.primary;
|
|
Color? iconColor(String key, IconData data) {
|
|
final icon = find.descendant(
|
|
of: find.byKey(ValueKey(key)),
|
|
matching: find.byIcon(data),
|
|
);
|
|
return IconTheme.of(tester.element(icon)).color;
|
|
}
|
|
|
|
for (final (index, label) in [
|
|
(1, 'Library'),
|
|
(2, 'Repo'),
|
|
(3, 'Search'),
|
|
(0, 'Home'),
|
|
]) {
|
|
await tester.tapAt(
|
|
tester.getCenter(
|
|
find
|
|
.descendant(
|
|
of: find.byType(MornyeTabBar),
|
|
matching: find.text(label),
|
|
)
|
|
.first,
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(activeTab.value, index);
|
|
final tabIcons = find.descendant(
|
|
of: find.byType(MornyeTabBar),
|
|
matching: find.byIcon(Icons.home),
|
|
);
|
|
// Edge icons stay in the glass bar's selected/unselected layers at
|
|
// rest, so the pill can reveal their red state during a held drag.
|
|
for (final icon in tabIcons.evaluate()) {
|
|
for (final opacity in tester.widgetList<Opacity>(
|
|
find.ancestor(
|
|
of: find.byElementPredicate((e) => e == icon),
|
|
matching: find.byType(Opacity),
|
|
),
|
|
)) {
|
|
expect(opacity.opacity, 1);
|
|
}
|
|
}
|
|
expect(
|
|
iconColor('mornye-compact-leading', switch (index) {
|
|
1 => Icons.music_note,
|
|
2 => Icons.grid_view,
|
|
_ => Icons.home,
|
|
}),
|
|
index == 3 ? scheme.onSurface : primary,
|
|
);
|
|
expect(
|
|
iconColor('mornye-compact-search', Icons.search),
|
|
index == 3 ? primary : scheme.onSurface,
|
|
);
|
|
final movingIcon = switch (index) {
|
|
1 => Icons.music_note,
|
|
2 => Icons.grid_view,
|
|
_ => Icons.home,
|
|
};
|
|
final leading = find.byKey(const ValueKey('mornye-compact-leading'));
|
|
final origin = tester.getCenter(
|
|
find.descendant(of: leading, matching: find.byIcon(movingIcon)),
|
|
);
|
|
final originalTabIcon = find
|
|
.descendant(
|
|
of: find.byType(MornyeTabBar),
|
|
matching: find.byIcon(movingIcon),
|
|
)
|
|
.first;
|
|
expect(
|
|
(origin - tester.getCenter(originalTabIcon)).distance,
|
|
lessThan(0.5),
|
|
);
|
|
chrome.value = true;
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(
|
|
tester
|
|
.widget<MornyeTabBar>(find.byType(MornyeTabBar))
|
|
.hiddenIconIndices,
|
|
{index == 3 ? 0 : index, 3},
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
iconColor('mornye-compact-leading', movingIcon),
|
|
index == 3 ? scheme.onSurface : primary,
|
|
);
|
|
expect(
|
|
iconColor('mornye-compact-search', Icons.search),
|
|
index == 3 ? primary : scheme.onSurface,
|
|
);
|
|
expect(
|
|
tester.widget<IconButton>(leading).tooltip,
|
|
index == 3 ? 'Home' : label,
|
|
);
|
|
await tester.tap(leading);
|
|
await tester.pumpAndSettle();
|
|
expect(activeTab.value, index == 3 ? 0 : index);
|
|
expect(chrome.value, isFalse);
|
|
activeTab.value = index;
|
|
chrome.expand();
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
tester.widget<MornyeTabBar>(find.byType(MornyeTabBar)).selectedIndex,
|
|
index,
|
|
);
|
|
}
|
|
});
|
|
|
|
testWidgets(
|
|
'edge icons travel continuously without fading (glass: $blur)',
|
|
(tester) async {
|
|
await pumpShell(tester, blur: blur);
|
|
final homeButton = find.byKey(const ValueKey('mornye-compact-leading'));
|
|
final icon = find.descendant(
|
|
of: homeButton,
|
|
matching: find.byIcon(Icons.home),
|
|
);
|
|
final element = tester.element(icon);
|
|
final tabIcon = find
|
|
.descendant(
|
|
of: find.byType(MornyeTabBar),
|
|
matching: find.byIcon(Icons.home),
|
|
)
|
|
.first;
|
|
final start = tester.getCenter(icon);
|
|
expect((start - tester.getCenter(tabIcon)).distance, lessThan(0.5));
|
|
chrome.value = true;
|
|
await tester.pump();
|
|
var previous = start;
|
|
for (var frame = 0; frame < 12; frame++) {
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
expect(tester.element(icon), same(element));
|
|
for (final opacity in tester.widgetList<Opacity>(
|
|
find.ancestor(of: icon, matching: find.byType(Opacity)),
|
|
)) {
|
|
expect(opacity.opacity, 1);
|
|
}
|
|
final center = tester.getCenter(icon);
|
|
expect((center - previous).distance, lessThan(8));
|
|
previous = center;
|
|
}
|
|
expect(previous.dx, lessThan(start.dx));
|
|
chrome.expand();
|
|
await tester.pumpAndSettle();
|
|
expect(tester.element(icon), same(element));
|
|
expect((tester.getCenter(icon) - start).distance, lessThan(0.5));
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Search joins full tabs and separates when compact (glass: $blur)',
|
|
(tester) async {
|
|
await pumpShell(tester, blur: blur);
|
|
final tabs = find.byType(MornyeTabBar);
|
|
final search = find.byKey(const ValueKey('mornye-compact-search'));
|
|
final repo = find
|
|
.descendant(of: tabs, matching: find.text('Repo'))
|
|
.first;
|
|
expect(tester.widget<MornyeTabBar>(tabs).destinations, hasLength(4));
|
|
final searchLabel = find
|
|
.descendant(of: tabs, matching: find.text('Search'))
|
|
.first;
|
|
expect(
|
|
find.descendant(of: tabs, matching: find.text('Search')),
|
|
findsWidgets,
|
|
);
|
|
expect(
|
|
find.descendant(of: tabs, matching: find.text('Settings')),
|
|
findsNothing,
|
|
);
|
|
expect(
|
|
tester.getCenter(search).dx,
|
|
greaterThan(tester.getCenter(repo).dx),
|
|
);
|
|
expect(
|
|
tester.getSize(tabs).width,
|
|
tester.getSize(find.byType(MornyeBottomBar)).width,
|
|
);
|
|
final searchIcon = find
|
|
.descendant(of: tabs, matching: find.byIcon(Icons.search))
|
|
.first;
|
|
expect(
|
|
(tester.getCenter(search) - tester.getCenter(searchIcon)).distance,
|
|
lessThan(0.5),
|
|
);
|
|
// The glass renderer paints the labels under a gesture overlay.
|
|
await tester.tapAt(tester.getCenter(searchLabel));
|
|
await tester.pumpAndSettle();
|
|
expect(searches, 1);
|
|
chrome.value = true;
|
|
await tester.pumpAndSettle();
|
|
expect(tester.getSize(search), const Size(52, 52));
|
|
await tester.tap(search);
|
|
expect(searches, 2);
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
}
|
|
|
|
for (final withPlayer in [false, true]) {
|
|
testWidgets('compact transition has no height jump (player: $withPlayer)', (
|
|
tester,
|
|
) async {
|
|
await pumpShell(tester, withPlayer: withPlayer);
|
|
final bar = find.byType(MornyeBottomBar);
|
|
var previousHeight = tester.getSize(bar).height;
|
|
final playerState = withPlayer
|
|
? tester.state(find.byType(MiniPlayer))
|
|
: null;
|
|
chrome.value = true;
|
|
await tester.pump();
|
|
Offset? firstHomeCenter;
|
|
for (var frame = 0; frame < 25; frame++) {
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
final height = tester.getSize(bar).height;
|
|
expect(height, lessThanOrEqualTo(previousHeight + 0.01));
|
|
previousHeight = height;
|
|
firstHomeCenter ??= tester.getCenter(
|
|
find.byKey(const ValueKey('mornye-compact-leading')),
|
|
);
|
|
}
|
|
final homeCenter = tester.getCenter(
|
|
find.byKey(const ValueKey('mornye-compact-leading')),
|
|
);
|
|
expect((homeCenter.dy - firstHomeCenter!.dy).abs(), lessThan(20));
|
|
expect(find.byTooltip('Home').hitTestable(), findsOneWidget);
|
|
expect(find.byTooltip('Search').hitTestable(), findsOneWidget);
|
|
if (withPlayer) {
|
|
expect(tester.state(find.byType(MiniPlayer)), same(playerState));
|
|
}
|
|
chrome.expand();
|
|
await tester.pump();
|
|
for (var frame = 0; frame < 25; frame++) {
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
final height = tester.getSize(bar).height;
|
|
expect(height, greaterThanOrEqualTo(previousHeight - 0.01));
|
|
previousHeight = height;
|
|
}
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|
|
|
|
for (final brightness in Brightness.values) {
|
|
for (final backdrop in [
|
|
Colors.white,
|
|
Colors.black,
|
|
if (brightness == Brightness.dark) albumBlue,
|
|
]) {
|
|
testWidgets(
|
|
'floating glass stays legible in $brightness over $backdrop',
|
|
(tester) async {
|
|
await pumpShell(
|
|
tester,
|
|
blur: true,
|
|
brightness: brightness,
|
|
backdrop: backdrop,
|
|
chromeSurface: backdrop == albumBlue ? albumBlue : null,
|
|
);
|
|
final artist = find.text('Artist');
|
|
// The glass renderer also builds a copy for the refractive pill.
|
|
final libraryIcon = find
|
|
.descendant(
|
|
of: find.byType(MornyeTabBar),
|
|
matching: find.byIcon(Icons.music_note),
|
|
)
|
|
.first;
|
|
final artistRect = tester.getRect(artist);
|
|
final iconRect = tester.getRect(libraryIcon);
|
|
final foregrounds = [
|
|
tester.widget<Text>(artist).style!.color!,
|
|
IconTheme.of(tester.element(libraryIcon)).color!,
|
|
];
|
|
final samples = [
|
|
Offset(artistRect.left + 8, artistRect.bottom + 2),
|
|
Offset(iconRect.right + 8, iconRect.center.dy),
|
|
];
|
|
final backgrounds = await tester.runAsync(() async {
|
|
final boundary = tester.renderObject<RenderRepaintBoundary>(
|
|
find.byKey(capture),
|
|
);
|
|
final image = await boundary.toImage();
|
|
final bytes = (await image.toByteData(
|
|
format: ui.ImageByteFormat.rawRgba,
|
|
))!;
|
|
final colors = <Color>[];
|
|
for (final point in samples) {
|
|
final offset =
|
|
(point.dy.floor() * image.width + point.dx.floor()) * 4;
|
|
colors.add(
|
|
Color.fromARGB(
|
|
bytes.getUint8(offset + 3),
|
|
bytes.getUint8(offset),
|
|
bytes.getUint8(offset + 1),
|
|
bytes.getUint8(offset + 2),
|
|
),
|
|
);
|
|
}
|
|
image.dispose();
|
|
return colors;
|
|
});
|
|
for (var index = 0; index < foregrounds.length; index++) {
|
|
if (backdrop == albumBlue) {
|
|
final glass = HSLColor.fromColor(backgrounds![index]);
|
|
final page = HSLColor.fromColor(albumBlue);
|
|
expect(glass.hue, closeTo(page.hue, 8));
|
|
expect(glass.saturation, greaterThan(page.saturation * 0.7));
|
|
}
|
|
final luminances = [
|
|
foregrounds[index].computeLuminance(),
|
|
backgrounds![index].computeLuminance(),
|
|
]..sort();
|
|
final contrast =
|
|
(luminances.last + 0.05) / (luminances.first + 0.05);
|
|
expect(
|
|
contrast,
|
|
greaterThanOrEqualTo(index == 0 ? 4.5 : 3),
|
|
reason: index == 0 ? 'Mini-player artist' : 'Inactive tab icon',
|
|
);
|
|
}
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
testWidgets('downward scroll folds the tabs without replacing the player', (
|
|
tester,
|
|
) async {
|
|
await pumpShell(tester);
|
|
final playerState = tester.state(find.byType(MiniPlayer));
|
|
final expandedHeight = tester.getSize(find.byType(MornyeBottomBar)).height;
|
|
await tester.drag(find.byType(ListView), const Offset(0, -240));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isTrue);
|
|
expect(tester.state(find.byType(MiniPlayer)), same(playerState));
|
|
expect(tester.widget<MiniPlayer>(find.byType(MiniPlayer)).compact, isTrue);
|
|
expect(
|
|
tester.getSize(find.byType(MornyeBottomBar)).height,
|
|
lessThan(expandedHeight - 60),
|
|
);
|
|
expect(find.byTooltip('Next track'), findsNothing);
|
|
await tester.tap(find.byTooltip('Home'));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isFalse);
|
|
expect(tester.getSize(find.byType(MornyeBottomBar)).height, expandedHeight);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets(
|
|
'collapse keeps the player and tab contents out of frame rebuilds',
|
|
(tester) async {
|
|
await pumpShell(tester);
|
|
var tabBuilds = 0;
|
|
var playerBuilds = 0;
|
|
final previous = debugOnRebuildDirtyWidget;
|
|
debugOnRebuildDirtyWidget = (element, builtOnce) {
|
|
previous?.call(element, builtOnce);
|
|
if (element.widget is MornyeTabBar) tabBuilds++;
|
|
if (element.widget is MiniPlayer) playerBuilds++;
|
|
};
|
|
addTearDown(() => debugOnRebuildDirtyWidget = previous);
|
|
chrome.value = true;
|
|
await tester.pump();
|
|
for (var frame = 0; frame < 25; frame++) {
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
}
|
|
expect(
|
|
tabBuilds,
|
|
lessThanOrEqualTo(2),
|
|
reason: 'Only the wrapper geometry animates each frame',
|
|
);
|
|
expect(
|
|
playerBuilds,
|
|
lessThanOrEqualTo(2),
|
|
reason:
|
|
'The player rebuilds for compact mode, not for every animation tick',
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
|
|
testWidgets('folding chrome updates the spacer without rebuilding Settings', (
|
|
tester,
|
|
) async {
|
|
await pumpShell(tester, body: const SettingsTab(), blur: true);
|
|
var settingsBuilds = 0;
|
|
var spacerBuilds = 0;
|
|
final previous = debugOnRebuildDirtyWidget;
|
|
debugOnRebuildDirtyWidget = (element, builtOnce) {
|
|
previous?.call(element, builtOnce);
|
|
if (element.widget is SettingsTab) settingsBuilds++;
|
|
if (element.widget is NavBarSliverSpacer) spacerBuilds++;
|
|
};
|
|
addTearDown(() => debugOnRebuildDirtyWidget = previous);
|
|
|
|
for (final collapsed in [true, false]) {
|
|
chrome.value = collapsed;
|
|
await tester.pump();
|
|
for (var frame = 0; frame < 25; frame++) {
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
}
|
|
}
|
|
|
|
expect(settingsBuilds, 0);
|
|
expect(spacerBuilds, greaterThan(20));
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
for (final artist in [true, false]) {
|
|
testWidgets('folding chrome isolates header insets (artist: $artist)', (
|
|
tester,
|
|
) async {
|
|
var pageBuilds = 0;
|
|
await pumpShell(
|
|
tester,
|
|
body: Builder(
|
|
builder: (context) {
|
|
pageBuilds++;
|
|
final tracks = SliverFixedExtentList.builder(
|
|
itemExtent: 60,
|
|
itemCount: 50,
|
|
itemBuilder: (_, index) => Text('Track $index'),
|
|
);
|
|
if (artist) {
|
|
return CustomScrollView(
|
|
controller: scroll,
|
|
slivers: [
|
|
...const MornyeArtistHeader(
|
|
name: 'Artist',
|
|
showTitle: false,
|
|
artwork: ColoredBox(color: Colors.blue),
|
|
actions: [],
|
|
).buildSlivers(context),
|
|
tracks,
|
|
const NavBarSliverSpacer(),
|
|
],
|
|
);
|
|
}
|
|
return CollectionScaffold(
|
|
scrollController: scroll,
|
|
isSelectionMode: false,
|
|
onExitSelectionMode: () {},
|
|
appBar: const AlbumDetailHeader(
|
|
title: 'Album',
|
|
expandedHeight: 400,
|
|
immersive: true,
|
|
showTitleInAppBar: false,
|
|
background: ColoredBox(color: Colors.blue),
|
|
),
|
|
slivers: [tracks],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
pageBuilds = 0;
|
|
var collectionBuilds = 0;
|
|
final previous = debugOnRebuildDirtyWidget;
|
|
debugOnRebuildDirtyWidget = (element, builtOnce) {
|
|
previous?.call(element, builtOnce);
|
|
if (element.widget is CollectionScaffold) collectionBuilds++;
|
|
};
|
|
addTearDown(() => debugOnRebuildDirtyWidget = previous);
|
|
for (final collapsed in [true, false]) {
|
|
chrome.value = collapsed;
|
|
await tester.pump();
|
|
for (var frame = 0; frame < 25; frame++) {
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
}
|
|
}
|
|
expect(pageBuilds, 0);
|
|
expect(collectionBuilds, 0);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|
|
|
|
testWidgets(
|
|
'compact tabs survive upward scrolling until the page returns to the top',
|
|
(tester) async {
|
|
await pumpShell(tester);
|
|
scroll.jumpTo(500);
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isFalse);
|
|
await tester.drag(find.byType(ListView), const Offset(-120, 0));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isFalse);
|
|
await tester.drag(find.byType(ListView), const Offset(0, -160));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isTrue);
|
|
await tester.drag(find.byType(ListView), const Offset(0, 90));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isTrue);
|
|
await tester.drag(find.byType(ListView), const Offset(0, 900));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isFalse);
|
|
},
|
|
);
|
|
|
|
testWidgets('Library keeps compact tabs until the outer header returns', (
|
|
tester,
|
|
) async {
|
|
await pumpShell(
|
|
tester,
|
|
body: NestedScrollView(
|
|
headerSliverBuilder: (_, _) => const [
|
|
SliverAppBar(expandedHeight: 180, title: Text('Library')),
|
|
],
|
|
body: PageView(
|
|
children: [
|
|
ListView.builder(
|
|
itemExtent: 60,
|
|
itemCount: 60,
|
|
itemBuilder: (_, index) => Text('Track $index'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.drag(find.byType(ListView), const Offset(0, -600));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isTrue);
|
|
await tester.drag(find.byType(ListView), const Offset(0, 100));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isTrue);
|
|
await tester.drag(find.byType(ListView), const Offset(0, 900));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isFalse);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets(
|
|
'glass bar stays mounted and pauses its tickers while collapsed',
|
|
(tester) async {
|
|
await pumpShell(tester, blur: true);
|
|
final glass = tester.element(find.byType(LiquidGlassTabBar));
|
|
final player = tester.state(find.byType(MiniPlayer));
|
|
expect(TickerMode.valuesOf(glass).enabled, isTrue);
|
|
chrome.value = true;
|
|
await tester.pumpAndSettle();
|
|
expect(tester.element(find.byType(LiquidGlassTabBar)), same(glass));
|
|
expect(TickerMode.valuesOf(glass).enabled, isFalse);
|
|
chrome.expand();
|
|
await tester.pumpAndSettle();
|
|
expect(tester.element(find.byType(LiquidGlassTabBar)), same(glass));
|
|
expect(tester.state(find.byType(MiniPlayer)), same(player));
|
|
expect(TickerMode.valuesOf(glass).enabled, isTrue);
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
|
|
testWidgets('compact navigation works without a playing track', (
|
|
tester,
|
|
) async {
|
|
await pumpShell(tester, withPlayer: false);
|
|
await tester.drag(find.byType(ListView), const Offset(0, -240));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isTrue);
|
|
await tester.tap(find.byTooltip('Search'));
|
|
expect(searches, 1);
|
|
await tester.tap(find.byTooltip('Home'));
|
|
await tester.pumpAndSettle();
|
|
expect(chrome.value, isFalse);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|