feat(concerts): show artwork-themed event details in app

This commit is contained in:
zarzet
2026-09-25 12:00:28 +07:00
parent ab58cdbb0c
commit abb8dc4a42
18 changed files with 1047 additions and 23 deletions
+178
View File
@@ -0,0 +1,178 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/l10n/app_localizations.dart';
import 'package:spotiflac_android/models/artist_concert.dart';
import 'package:spotiflac_android/screens/artist_concerts_screen.dart';
import 'package:spotiflac_android/screens/concert_detail_screen.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const backend = MethodChannel('com.zarz.spotiflac/backend');
const calendar = MethodChannel('com.zarz.spotiflac/concert_calendar');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
ArtistConcert event(String id, {bool details = true}) =>
ArtistConcert.parseList([
{
'id': id,
'location': 'Example City',
'venue': 'Example Hall',
'start_at': '2026-10-07T01:00:00Z',
'time_zone': 'America/New_York',
if (details) 'detail_id': id,
'url': 'https://example.com/events/1',
},
]).single;
Widget app(Widget home, {Brightness brightness = Brightness.light}) =>
ProviderScope(
child: MaterialApp(
theme: MornyeTheme.build(brightness),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: home,
),
);
setUp(() => SharedPreferences.setMockInitialValues({}));
tearDown(() {
messenger.setMockMethodCallHandler(backend, null);
messenger.setMockMethodCallHandler(calendar, null);
});
for (final brightness in Brightness.values) {
testWidgets(
'schedule follows $brightness and a row opens an internal dark detail',
(tester) async {
await tester.pumpWidget(
app(
ArtistConcertsScreen(
artistName: 'Example Artist',
concerts: [event('basic-${brightness.name}', details: false)],
),
brightness: brightness,
),
);
await tester.pumpAndSettle();
expect(
Theme.of(
tester.element(find.text('All Upcoming Concerts')),
).brightness,
brightness,
);
await tester.tap(find.text('Example City'));
await tester.pumpAndSettle();
expect(find.byType(ConcertDetailScreen), findsOneWidget);
expect(
Theme.of(tester.element(find.text('Example Artist').last)).brightness,
Brightness.dark,
);
expect(find.textContaining('21:00'), findsOneWidget);
expect(find.text('Get Tickets'), findsNothing);
await tester.tap(find.byTooltip('Back').last);
await tester.pumpAndSettle();
expect(find.byType(ConcertDetailScreen), findsNothing);
},
);
}
testWidgets(
'details load on demand with skeleton, retry and calendar action',
(tester) async {
tester.view.physicalSize = const Size(390, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final pending = Completer<Object?>();
var requests = 0;
messenger.setMockMethodCallHandler(backend, (call) async {
if (call.method != 'getProviderMetadata') return null;
expect(call.arguments, containsPair('resource_type', 'concert'));
expect(call.arguments, containsPair('provider_id', 'example.metadata'));
requests++;
if (requests == 1) return pending.future;
return {
'concert': {
'id': 'load',
'address': '123 Example Street',
'start_at': '2026-10-07T01:00:00Z',
'end_at': '2026-10-07T04:00:00Z',
'ticket_url': 'https://example.com/tickets/1',
'set_list': {'id': 'list-1', 'name': 'Example Set List'},
},
};
});
final calendarCalls = <MethodCall>[];
messenger.setMockMethodCallHandler(calendar, (call) async {
calendarCalls.add(call);
return true;
});
await tester.pumpWidget(
app(
ConcertDetailScreen(
artistName: 'Example Artist',
providerId: 'example.metadata',
concert: event('load'),
),
),
);
await tester.pump();
expect(find.byType(ShimmerLoading), findsOneWidget);
pending.completeError(PlatformException(code: 'offline'));
await tester.pumpAndSettle();
expect(
find.text('Concert details are currently unavailable.'),
findsOneWidget,
);
await tester.tap(find.text('Retry'));
await tester.pumpAndSettle();
expect(find.text('Get Tickets'), findsOneWidget);
expect(find.text('Example Set List'), findsOneWidget);
expect(find.text('123 Example Street'), findsOneWidget);
await tester.tap(find.byTooltip('Add to Calendar'));
await tester.pumpAndSettle();
expect(
calendarCalls.single.arguments,
containsPair(
'start',
DateTime.utc(2026, 10, 7, 1).millisecondsSinceEpoch,
),
);
expect(
calendarCalls.single.arguments,
containsPair(
'end',
DateTime.utc(2026, 10, 7, 4).millisecondsSinceEpoch,
),
);
expect(tester.takeException(), isNull);
await tester.pump(const Duration(seconds: 1));
},
);
test(
'optional fields and unsafe links cannot create bogus detail actions',
() {
final detail = ConcertDetail.fromJson({
'ticket_url': 'javascript:alert(1)',
'map_url': 'file:///private',
'set_list': <Object?>[],
'start_at': 'bad date',
'artist_name': 42,
});
expect(detail.ticketUrl, isNull);
expect(detail.mapUrl, isNull);
expect(detail.start, isNull);
expect(detail.setListId, isNull);
expect(detail.artistName, isNull);
},
);
}