3e905db906
ExpansionTile per aggregated quarantine row — collapsed shows track,
artist · album, and "{N} reports: {top reason}". Expanded reveals the
nested per-user reports with their reasons + optional notes.
Three-dot menu offers Resolve / Delete file / Delete via Lidarr; both
deletes route through TypedConfirmSheet which requires the user to
type "DELETE" before the action button enables.
Adds AdminQuarantineController with optimistic-removal + rollback,
mirroring AdminRequestsController's pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
3.0 KiB
Dart
95 lines
3.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:minstrel/admin/admin_providers.dart';
|
|
import 'package:minstrel/admin/admin_quarantine_screen.dart';
|
|
import 'package:minstrel/auth/auth_provider.dart';
|
|
import 'package:minstrel/models/admin_quarantine_item.dart';
|
|
import 'package:minstrel/models/user.dart';
|
|
import 'package:minstrel/theme/theme_data.dart';
|
|
|
|
class _StubAuth extends AuthController {
|
|
@override
|
|
Future<User?> build() async =>
|
|
const User(id: 'u1', username: 'admin', isAdmin: true);
|
|
}
|
|
|
|
class _StubQuarantine extends AdminQuarantineController {
|
|
_StubQuarantine(this._initial);
|
|
final List<AdminQuarantineItem> _initial;
|
|
@override
|
|
Future<List<AdminQuarantineItem>> build() async => _initial;
|
|
}
|
|
|
|
const _item = AdminQuarantineItem(
|
|
trackId: 't1',
|
|
trackTitle: 'Bad Track',
|
|
artistName: 'Some Artist',
|
|
albumTitle: 'Some Album',
|
|
albumId: 'al1',
|
|
lidarrAlbumMbid: null,
|
|
reportCount: 2,
|
|
latestAt: '2026-05-08T00:00:00Z',
|
|
reasonCounts: {'wrong_tags': 1, 'bad_rip': 1},
|
|
reports: [
|
|
AdminQuarantineReport(
|
|
userId: 'u2',
|
|
username: 'alice',
|
|
reason: 'wrong_tags',
|
|
notes: null,
|
|
createdAt: '2026-05-08T00:00:00Z',
|
|
),
|
|
AdminQuarantineReport(
|
|
userId: 'u3',
|
|
username: 'bob',
|
|
reason: 'bad_rip',
|
|
notes: 'cracks at 2:13',
|
|
createdAt: '2026-05-08T00:01:00Z',
|
|
),
|
|
],
|
|
);
|
|
|
|
Widget _harness(List<AdminQuarantineItem> rows) => ProviderScope(
|
|
overrides: [
|
|
authControllerProvider.overrideWith(() => _StubAuth()),
|
|
adminQuarantineProvider.overrideWith(() => _StubQuarantine(rows)),
|
|
],
|
|
child: MaterialApp(
|
|
theme: buildThemeData(),
|
|
home: const AdminQuarantineScreen(),
|
|
),
|
|
);
|
|
|
|
void main() {
|
|
testWidgets('empty state', (t) async {
|
|
await t.pumpWidget(_harness(const []));
|
|
await t.pumpAndSettle();
|
|
expect(find.text('No quarantined tracks.'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('row renders aggregate summary + 3-dot menu has three actions',
|
|
(t) async {
|
|
await t.pumpWidget(_harness(const [_item]));
|
|
await t.pumpAndSettle();
|
|
expect(find.byKey(const Key('admin_quarantine_tile_t1')), findsOneWidget);
|
|
expect(find.text('Bad Track'), findsOneWidget);
|
|
expect(find.textContaining('2 reports'), findsOneWidget);
|
|
await t.tap(find.byKey(const Key('admin_quarantine_menu_t1')));
|
|
await t.pumpAndSettle();
|
|
expect(find.text('Resolve'), findsOneWidget);
|
|
expect(find.text('Delete file'), findsOneWidget);
|
|
expect(find.text('Delete via Lidarr'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('expanding tile reveals per-user reports', (t) async {
|
|
await t.pumpWidget(_harness(const [_item]));
|
|
await t.pumpAndSettle();
|
|
await t.tap(find.byKey(const Key('admin_quarantine_tile_t1')));
|
|
await t.pumpAndSettle();
|
|
expect(find.textContaining('alice — wrong_tags'), findsOneWidget);
|
|
expect(find.textContaining('bob — bad_rip'), findsOneWidget);
|
|
expect(find.text('cracks at 2:13'), findsOneWidget);
|
|
});
|
|
}
|