Files
minstrel/flutter_client/test/admin/admin_quarantine_screen_test.dart
bvandeusen 3e905db906 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>
2026-05-08 21:56:00 -04:00

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);
});
}