feat(flutter/admin): quarantine screen + typed-confirm sheet
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>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user