feat(albums): show formatted editorial descriptions

This commit is contained in:
zarzet
2026-09-26 03:50:37 +07:00
parent 0e1881d837
commit 7d7716cd6f
13 changed files with 465 additions and 25 deletions
+145
View File
@@ -0,0 +1,145 @@
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/mornye_theme.dart';
import 'package:spotiflac_android/utils/editorial_notes.dart';
import 'package:spotiflac_android/widgets/album_description.dart';
void main() {
test(
'editorial notes prefer full text and fall back to a usable summary',
() {
expect(
albumDescriptionFromMetadata({
'editorial_notes': {
'standard': '<p>Full notes.</p>',
'short': 'Short notes.',
},
}),
'<p>Full notes.</p>',
);
expect(
albumDescriptionFromMetadata({
'editorial_notes': {
'standard': '<p>&nbsp;</p>',
'short': 'Short notes.',
},
}),
'Short notes.',
);
expect(
albumDescriptionFromMetadata({'description': 'Plain notes.'}),
'Plain notes.',
);
expect(albumDescriptionFromMetadata({'editorial_notes': null}), isNull);
expect(albumDescriptionFromMetadata({'editorial_notes': 42}), isNull);
expect(
albumDescriptionFromMetadata({
'editorial_notes': {'standard': '<script>hidden</script>'},
}),
isNull,
);
},
);
for (final style in ['mornye-light', 'mornye-dark', 'material']) {
testWidgets('album description opens a formatted reading sheet ($style)', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(393, 700));
addTearDown(() => tester.binding.setSurfaceSize(null));
final mornye = style.startsWith('mornye');
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
theme: mornye
? MornyeTheme.build(
style.endsWith('light')
? Brightness.light
: Brightness.dark,
)
: ThemeData(),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Navigator(
onGenerateRoute: (_) => MaterialPageRoute<void>(
builder: (_) => Scaffold(
body: AlbumDescription(
title: 'Example Album',
description:
'A <i>new direction</i> &amp; a <b>bold sound</b>.\n\n'
'A second paragraph.\nOne line break remains.\n\n'
'${List.filled(12, '<p>The artists explore melodies and new ideas in these songs.</p>').join()}'
'<p>Final paragraph.</p><script>hidden script</script>',
),
),
),
),
),
),
);
await tester.pumpAndSettle();
final preview = tester.widget<Text>(
find.byWidgetPredicate(
(widget) => widget is Text && widget.textSpan != null,
),
);
expect(preview.maxLines, 2);
expect(
preview.textSpan!.toPlainText(),
contains('new direction & a bold sound.\n\n'),
);
expect(preview.textSpan!.toPlainText(), isNot(contains('hidden script')));
expect(
preview.textSpan!.toPlainText(),
contains('sound.\n\nA second paragraph.\nOne line break remains.\n\n'),
);
Iterable<TextSpan> spans(InlineSpan span) sync* {
if (span is TextSpan) {
yield span;
for (final child in span.children ?? <InlineSpan>[]) {
yield* spans(child);
}
}
}
expect(
spans(preview.textSpan!).any(
(span) =>
span.style?.fontStyle == FontStyle.italic &&
span.toPlainText() == 'new direction',
),
isTrue,
);
await tester.tap(find.text(mornye ? 'MORE' : 'More'));
await tester.pumpAndSettle();
expect(find.text('Example Album'), findsOneWidget);
expect(find.byType(SelectionArea), findsOneWidget);
expect(
ModalRoute.of(tester.element(find.byType(SelectionArea)))!.navigator,
tester.state<NavigatorState>(find.byType(Navigator).first),
);
expect(find.byTooltip('Close').hitTestable(), findsOneWidget);
await tester.drag(
find.byType(SingleChildScrollView),
const Offset(0, -600),
);
await tester.pumpAndSettle();
final scroll = tester.state<ScrollableState>(
find.descendant(
of: find.byType(SingleChildScrollView),
matching: find.byType(Scrollable),
),
);
expect(scroll.position.pixels, greaterThan(0));
expect(find.byTooltip('Close').hitTestable(), findsOneWidget);
await tester.tap(find.byTooltip('Close'));
await tester.pumpAndSettle();
expect(find.byType(SelectionArea), findsNothing);
expect(tester.takeException(), isNull);
});
}
}
+35 -14
View File
@@ -11,6 +11,7 @@ import 'package:spotiflac_android/screens/album_screen.dart';
import 'package:spotiflac_android/screens/home_tab.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/album_description.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
import 'package:spotiflac_android/widgets/track_list_tile.dart';
@@ -54,6 +55,10 @@ void main() {
'name': fetchedName,
'artists': 'Example Artist',
'total_tracks': 1,
'editorial_notes': {
'standard': '<p>A <i>new direction</i> for the band.</p>',
'short': 'A new direction.',
},
};
final tracks = [
{
@@ -89,6 +94,10 @@ void main() {
final expected = fetchedName.trim().isEmpty ? 'Album' : fetchedName;
final album = tester.widget<AlbumScreen>(find.byType(AlbumScreen));
expect(album.albumName, expected);
expect(
album.description,
'<p>A <i>new direction</i> for the band.</p>',
);
expect(album.tracks!.single.albumName, expected);
expect(find.text(expected), findsWidgets);
expect(requests, 1);
@@ -124,7 +133,7 @@ void main() {
'audio_modes': 'DOLBY_ATMOS',
'explicit': true,
};
var albumRequested = false;
var albumRequests = 0;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (call) async {
if (call.method == 'getProviderMetadata') {
@@ -133,7 +142,7 @@ void main() {
'resource_type': 'album',
'resource_id': 'metadata-album',
});
albumRequested = true;
albumRequests++;
return jsonEncode({
'track_list': [payload],
'album_info': {
@@ -141,6 +150,7 @@ void main() {
'total_tracks': 1,
'album_type': 'album',
'audio_traits': ['lossless', 'dolby_atmos'],
'editorial_notes': {'standard': '<p>A new direction.</p>'},
},
});
}
@@ -156,22 +166,22 @@ void main() {
expect(searchTrack.label, 'Example Label');
expect(searchTrack.copyright, 'Example Copyright');
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
theme: MornyeTheme.build(Brightness.light),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const AlbumScreen(
albumId: 'metadata-album',
albumName: 'Example Album',
extensionId: 'example-metadata',
),
final page = ProviderScope(
child: MaterialApp(
theme: MornyeTheme.build(Brightness.light),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const AlbumScreen(
albumId: 'metadata-album',
albumName: 'Example Album',
extensionId: 'example-metadata',
),
),
);
await tester.pumpWidget(page);
await tester.pumpAndSettle();
expect(albumRequested, isTrue);
expect(albumRequests, 1);
expect(find.byType(AlbumDescription), findsOneWidget);
final albumTrack = tester
.widget<TrackListTile>(find.byType(TrackListTile).first)
.track;
@@ -193,5 +203,16 @@ void main() {
expect(find.byType(ExplicitBadge), findsOneWidget);
await tester.pumpWidget(const SizedBox());
await tester.pumpAndSettle();
await tester.pumpWidget(page);
await tester.pumpAndSettle();
expect(albumRequests, 1);
expect(
tester
.widget<AlbumDescription>(find.byType(AlbumDescription))
.description,
'<p>A new direction.</p>',
);
await tester.pumpWidget(const SizedBox());
await tester.pumpAndSettle();
});
}