5159bcd3f4
AsyncNotifier wrapping /api/quarantine/mine with optimistic flag / unflag mutations and an isHidden(trackId) convenience for menu state. Mirrors the LikedIdsController pattern: optimistic update, rollback on server error. Used by the next slice's TrackActionsSheet to power Hide / Unhide. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
3.8 KiB
Dart
124 lines
3.8 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:minstrel/api/endpoints/quarantine.dart';
|
|
import 'package:minstrel/models/quarantine_mine.dart';
|
|
import 'package:minstrel/models/track.dart';
|
|
import 'package:minstrel/quarantine/quarantine_provider.dart';
|
|
|
|
class _StubQuarantineApi implements QuarantineApi {
|
|
_StubQuarantineApi({this.shouldThrow = false});
|
|
bool shouldThrow;
|
|
int flagCalls = 0;
|
|
int unflagCalls = 0;
|
|
|
|
@override
|
|
Future<void> flag(String trackId, String reason, {String notes = ''}) async {
|
|
flagCalls++;
|
|
if (shouldThrow) throw Exception('simulated server failure');
|
|
}
|
|
|
|
@override
|
|
Future<void> unflag(String trackId) async {
|
|
unflagCalls++;
|
|
if (shouldThrow) throw Exception('simulated server failure');
|
|
}
|
|
}
|
|
|
|
class _StubController extends MyQuarantineController {
|
|
_StubController(this._initial);
|
|
final List<QuarantineMineRow> _initial;
|
|
@override
|
|
Future<List<QuarantineMineRow>> build() async => _initial;
|
|
}
|
|
|
|
const _track = TrackRef(
|
|
id: 't1',
|
|
title: 'Roygbiv',
|
|
albumId: 'a1',
|
|
albumTitle: 'Geogaddi',
|
|
artistId: 'ar1',
|
|
artistName: 'Boards of Canada',
|
|
durationSec: 137,
|
|
trackNumber: 4,
|
|
streamUrl: '',
|
|
);
|
|
|
|
const _existing = QuarantineMineRow(
|
|
trackId: 't1',
|
|
reason: 'bad_rip',
|
|
notes: null,
|
|
createdAt: '2026-05-01T00:00:00Z',
|
|
trackTitle: 'Roygbiv',
|
|
trackDurationMs: 137000,
|
|
albumId: 'a1',
|
|
albumTitle: 'Geogaddi',
|
|
artistId: 'ar1',
|
|
artistName: 'Boards of Canada',
|
|
);
|
|
|
|
void main() {
|
|
test('flag prepends synthetic row and calls server', () async {
|
|
final api = _StubQuarantineApi();
|
|
final container = ProviderContainer(overrides: [
|
|
quarantineApiProvider.overrideWith((ref) async => api),
|
|
myQuarantineProvider.overrideWith(() => _StubController(const [])),
|
|
]);
|
|
addTearDown(container.dispose);
|
|
|
|
await container.read(myQuarantineProvider.future);
|
|
await container.read(myQuarantineProvider.notifier).flag(_track, 'bad_rip', '');
|
|
|
|
expect(api.flagCalls, 1);
|
|
final list = container.read(myQuarantineProvider).value!;
|
|
expect(list, hasLength(1));
|
|
expect(list.first.trackId, 't1');
|
|
expect(list.first.reason, 'bad_rip');
|
|
});
|
|
|
|
test('flag rolls back on server failure', () async {
|
|
final api = _StubQuarantineApi(shouldThrow: true);
|
|
final container = ProviderContainer(overrides: [
|
|
quarantineApiProvider.overrideWith((ref) async => api),
|
|
myQuarantineProvider.overrideWith(() => _StubController(const [])),
|
|
]);
|
|
addTearDown(container.dispose);
|
|
|
|
await container.read(myQuarantineProvider.future);
|
|
await expectLater(
|
|
() => container
|
|
.read(myQuarantineProvider.notifier)
|
|
.flag(_track, 'bad_rip', ''),
|
|
throwsException,
|
|
);
|
|
expect(container.read(myQuarantineProvider).value, isEmpty);
|
|
});
|
|
|
|
test('unflag removes the row and calls server', () async {
|
|
final api = _StubQuarantineApi();
|
|
final container = ProviderContainer(overrides: [
|
|
quarantineApiProvider.overrideWith((ref) async => api),
|
|
myQuarantineProvider.overrideWith(() => _StubController(const [_existing])),
|
|
]);
|
|
addTearDown(container.dispose);
|
|
|
|
await container.read(myQuarantineProvider.future);
|
|
await container.read(myQuarantineProvider.notifier).unflag('t1');
|
|
|
|
expect(api.unflagCalls, 1);
|
|
expect(container.read(myQuarantineProvider).value, isEmpty);
|
|
});
|
|
|
|
test('isHidden reflects current state', () async {
|
|
final container = ProviderContainer(overrides: [
|
|
myQuarantineProvider.overrideWith(() => _StubController(const [_existing])),
|
|
]);
|
|
addTearDown(container.dispose);
|
|
|
|
await container.read(myQuarantineProvider.future);
|
|
final ctrl = container.read(myQuarantineProvider.notifier);
|
|
expect(ctrl.isHidden('t1'), isTrue);
|
|
expect(ctrl.isHidden('t2'), isFalse);
|
|
});
|
|
}
|