478 lines
21 KiB
Dart
478 lines
21 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
import 'package:flutter/gestures.dart' show kSecondaryButton;
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||
import 'package:jj_mkvmaker/app/settings.dart';
|
||
import 'package:jj_mkvmaker/core/playlist.dart';
|
||
import 'package:jj_mkvmaker/main.dart' show lastWorkText, wantsNewPlayerWindow;
|
||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||
import 'package:jj_mkvmaker/platform/windows/shell_integration.dart';
|
||
import 'package:jj_mkvmaker/platform/windows/single_instance.dart';
|
||
import 'package:jj_mkvmaker/services/media_player.dart';
|
||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||
import 'package:jj_mkvmaker/ui/exit_dialog.dart';
|
||
import 'package:jj_mkvmaker/ui/player_page.dart';
|
||
import 'package:path/path.dart' as p;
|
||
|
||
class _FakePlayer implements MediaPlayer {
|
||
@override
|
||
final ValueNotifier<PlayerState> state = ValueNotifier(const PlayerState(
|
||
duration: Duration(minutes: 2),
|
||
audioTracks: [TrackInfo('1', 'kor'), TrackInfo('2', 'eng')],
|
||
subtitleTracks: [TrackInfo('3', '한국어')],
|
||
width: 1920,
|
||
height: 1080,
|
||
));
|
||
final calls = <String>[];
|
||
List<String> _files = [];
|
||
|
||
void _set({bool? playing, int? index, String? sub, bool clearSub = false, double? volume}) {
|
||
final s = state.value;
|
||
state.value = PlayerState(
|
||
position: s.position,
|
||
duration: s.duration,
|
||
playing: playing ?? s.playing,
|
||
index: index ?? s.index,
|
||
volume: volume ?? s.volume,
|
||
width: s.width,
|
||
height: s.height,
|
||
audioTracks: s.audioTracks,
|
||
subtitleTracks: s.subtitleTracks,
|
||
subtitleId: clearSub ? null : (sub ?? s.subtitleId),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Stream<String> get errors => const Stream.empty();
|
||
@override
|
||
List<String> get playlist => _files;
|
||
@override
|
||
Future<void> open(List<String> files, {int start = 0}) async {
|
||
_files = List.of(files);
|
||
calls.add('open ${files.length} $start');
|
||
_set(index: start, playing: true);
|
||
}
|
||
|
||
@override
|
||
Future<void> add(List<String> files) async => _files.addAll(files);
|
||
@override
|
||
Future<void> jump(int index) async => _set(index: index);
|
||
@override
|
||
Future<void> next() async => _set(index: (state.value.index + 1).clamp(0, _files.length - 1));
|
||
@override
|
||
Future<void> previous() async => _set(index: (state.value.index - 1).clamp(0, _files.length - 1));
|
||
@override
|
||
Future<void> playOrPause() async => _set(playing: !state.value.playing);
|
||
@override
|
||
Future<void> play() async => _set(playing: true);
|
||
@override
|
||
Future<void> stop() async => calls.add('stop');
|
||
@override
|
||
Future<void> seek(Duration position) async => calls.add('seek ${position.inSeconds}');
|
||
@override
|
||
Future<void> setVolume(double volume) async => _set(volume: volume);
|
||
@override
|
||
Future<void> setRate(double rate) async => calls.add('rate $rate');
|
||
@override
|
||
Future<void> setAudioTrack(TrackInfo t) async => calls.add('audio ${t.id}');
|
||
@override
|
||
Future<void> setSubtitleTrack(TrackInfo? t) async =>
|
||
t == null ? _set(clearSub: true) : _set(sub: t.id);
|
||
@override
|
||
Widget buildView() => const ColoredBox(color: Colors.black);
|
||
@override
|
||
Future<void> dispose() async => calls.add('dispose');
|
||
}
|
||
|
||
void main() {
|
||
group('재생 목록 규칙', () {
|
||
const dir = r'D:\v';
|
||
final files = [
|
||
for (final n in [
|
||
'file_010.mp4', 'file_002.mp4', 'file_1.mp4', 'Show.S01E02.mkv', 'Show.S01E10.mkv',
|
||
'Show.S02E01.mkv', 'other.avi', 'notes.txt', 'Show.S01E01.srt',
|
||
])
|
||
p.join(dir, n),
|
||
];
|
||
|
||
test('시리즈 키 · 자연 정렬', () {
|
||
expect(seriesKey('file_001.mp4'), seriesKey('file_0002.mp4'));
|
||
expect(seriesKey('Show.S01E02.mkv'), seriesKey('Show.S02E10.mkv'));
|
||
expect(seriesKey('other.avi') == seriesKey('file_1.mp4'), isFalse);
|
||
final names = ['a10', 'a2', 'a1', 'b1'];
|
||
names.sort(naturalCompare);
|
||
expect(names, ['a1', 'a2', 'a10', 'b1']);
|
||
});
|
||
|
||
test('하나 · 시리즈 · 폴더', () {
|
||
final f = p.join(dir, 'file_002.mp4');
|
||
final (one, k) = buildPlaylist(f, files, PlaylistMode.single);
|
||
expect([one, k], [[f], 0]);
|
||
final (series, i) = buildPlaylist(f, files, PlaylistMode.series);
|
||
expect(series.map(p.basename), ['file_1.mp4', 'file_002.mp4', 'file_010.mp4']);
|
||
expect(i, 1);
|
||
final (show, j) = buildPlaylist(p.join(dir, 'Show.S01E10.mkv'), files, PlaylistMode.series);
|
||
expect(show.map(p.basename), ['Show.S01E02.mkv', 'Show.S01E10.mkv', 'Show.S02E01.mkv']);
|
||
expect(j, 1);
|
||
final (all, _) = buildPlaylist(f, files, PlaylistMode.folder);
|
||
expect(all, hasLength(7)); // 자막·텍스트 제외
|
||
});
|
||
|
||
test('끌어다 놓기: 동영상만, 폴더 안쪽, jj_ 출력 폴더 제외', () async {
|
||
final tree = {
|
||
r'D:\drop': [r'D:\drop\a.mp4', r'D:\drop\b.txt', r'D:\drop\sub', r'D:\drop\jj_mkv'],
|
||
r'D:\drop\sub': [r'D:\drop\sub\c.mkv'],
|
||
r'D:\drop\jj_mkv': [r'D:\drop\jj_mkv\a.mkv'],
|
||
};
|
||
final v = await collectVideos([r'D:\drop', r'D:\x.avi', r'D:\y.jpg'],
|
||
isDirectory: (x) async => tree.containsKey(x), listDir: (x) async => List.of(tree[x]!));
|
||
expect(v, [r'D:\drop\a.mp4', r'D:\drop\sub\c.mkv', r'D:\x.avi']);
|
||
});
|
||
|
||
test('실행 인수', () {
|
||
final r = LaunchRequest.parse(['--play', r'D:\a.mp4', r'D:\b.mp4']);
|
||
expect([r.action, r.files], [LaunchAction.play, [r'D:\a.mp4', r'D:\b.mp4']]);
|
||
expect(LaunchRequest.parse([r'D:\a.mp4']).action, LaunchAction.add);
|
||
expect(LaunchRequest.parse(['--subtitle', 'x']).toArgs(), ['--subtitle', 'x']);
|
||
});
|
||
|
||
test('탐색기에서 연 동영상: 새 재생 창 여부', () {
|
||
final open = LaunchRequest.parse([r'D:.mkv']);
|
||
final play = LaunchRequest.parse(['--play', r'D:.mkv']);
|
||
final sub = LaunchRequest.parse(['--subtitle', r'D:.mkv']);
|
||
final s = AppSettings();
|
||
expect([s.openFileAction, s.openFileWindow], ['play', 'same']);
|
||
expect(wantsNewPlayerWindow(open, s), isFalse); // 기본: 켜져 있는 창에서
|
||
s.openFileWindow = 'new';
|
||
expect(wantsNewPlayerWindow(open, s), isTrue);
|
||
expect(wantsNewPlayerWindow(play, s), isTrue);
|
||
expect(wantsNewPlayerWindow(sub, s), isFalse); // 자막 만들기는 원래 창
|
||
expect(wantsNewPlayerWindow(LaunchRequest.parse(const []), s), isFalse);
|
||
s.openFileAction = 'add';
|
||
expect(wantsNewPlayerWindow(open, s), isFalse); // 목록에 추가는 원래 창
|
||
expect(wantsNewPlayerWindow(play, s), isTrue);
|
||
final back = AppSettings.fromJson(s.toJson());
|
||
expect([back.openFileAction, back.openFileWindow], ['add', 'new']);
|
||
});
|
||
|
||
test('Android 화면 방향: 기본 자동, 가로 · 세로 고정 저장, 예전 기본 (가로 고정) 은 한 번 자동으로', () {
|
||
final s = AppSettings();
|
||
expect(s.display.orientation, 'auto');
|
||
for (final m in ['portrait', 'auto', 'landscape']) {
|
||
s.display.orientation = m;
|
||
expect(AppSettings.fromJson(s.toJson()).display.orientation, m);
|
||
}
|
||
expect(AppSettings.fromJson({'screenOrientation': 'sideways', 'orientationV2': true}).display.orientation, 'auto');
|
||
// 예전 설정 파일 (기본 가로 고정 시절) → 자동
|
||
expect(AppSettings.fromJson({'screenOrientation': 'landscape'}).display.orientation, 'auto');
|
||
});
|
||
});
|
||
|
||
group('중복 실행 방지 (228: 데이터 폴더마다 잠금 파일 · 포트 0 · 비밀값)', () {
|
||
late Directory d1, d2;
|
||
setUp(() {
|
||
d1 = Directory.systemTemp.createTempSync('jj_inst1_');
|
||
d2 = Directory.systemTemp.createTempSync('jj_inst2_');
|
||
});
|
||
tearDown(() {
|
||
d1.deleteSync(recursive: true);
|
||
d2.deleteSync(recursive: true);
|
||
});
|
||
|
||
test('나중 실행은 넘기고, 짧은 시간의 요청은 하나로 · 끝내면 instance.json 을 지운다', () async {
|
||
final first = SingleInstance(dataDir: d1.path, gather: const Duration(milliseconds: 300));
|
||
expect(await first.claim(const LaunchRequest(LaunchAction.play, ['a.mp4'])), ClaimResult.first);
|
||
final got = <LaunchRequest>[];
|
||
first.listen(got.add);
|
||
expect(File(p.join(d1.path, 'instance.json')).existsSync(), isTrue);
|
||
|
||
// 탐색기가 파일마다 프로그램을 실행한 상황
|
||
for (final f in ['b.mp4', 'c.mp4']) {
|
||
expect(await SingleInstance(dataDir: d1.path).claim(LaunchRequest(LaunchAction.play, [f])), ClaimResult.forwarded);
|
||
}
|
||
expect(await SingleInstance(dataDir: d1.path).claim(const LaunchRequest(LaunchAction.subtitle, ['d.mp4'])),
|
||
ClaimResult.forwarded);
|
||
// 새 재생 창으로 따로 뜰 때는 넘기지 않는다
|
||
expect(await SingleInstance(dataDir: d1.path).claim(const LaunchRequest(LaunchAction.play, ['e.mp4']), forward: false),
|
||
ClaimResult.running);
|
||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||
await first.close();
|
||
|
||
expect(got, hasLength(2));
|
||
final play = got.firstWhere((r) => r.action == LaunchAction.play);
|
||
expect(play.files, ['a.mp4', 'b.mp4', 'c.mp4']);
|
||
expect(got.firstWhere((r) => r.action == LaunchAction.subtitle).files, ['d.mp4']);
|
||
expect(File(p.join(d1.path, 'instance.json')).existsSync(), isFalse);
|
||
// 끝낸 뒤에는 새 실행이 첫 실행
|
||
final again = SingleInstance(dataDir: d1.path);
|
||
expect(await again.claim(const LaunchRequest(LaunchAction.play, [])), ClaimResult.first);
|
||
await again.close();
|
||
}, skip: !Platform.isWindows);
|
||
|
||
test('데이터 폴더가 다르면 따로 뜬다 (시험판 · 다른 데이터끼리 넘기지 않음)', () async {
|
||
final a = SingleInstance(dataDir: d1.path);
|
||
final b = SingleInstance(dataDir: d2.path);
|
||
expect(a.lockPath, isNot(b.lockPath));
|
||
expect(await a.claim(const LaunchRequest(LaunchAction.play, [])), ClaimResult.first);
|
||
expect(await b.claim(const LaunchRequest(LaunchAction.play, [])), ClaimResult.first);
|
||
await a.close();
|
||
await b.close();
|
||
}, skip: !Platform.isWindows);
|
||
|
||
test('비밀값이 틀린 요청은 받지 않는다 (같은 PC 의 다른 프로그램)', () async {
|
||
final first = SingleInstance(dataDir: d1.path, gather: const Duration(milliseconds: 100));
|
||
expect(await first.claim(const LaunchRequest(LaunchAction.play, [])), ClaimResult.first);
|
||
final got = <LaunchRequest>[];
|
||
first.listen(got.add);
|
||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||
got.clear();
|
||
final port = (jsonDecode(File(p.join(d1.path, 'instance.json')).readAsStringSync()) as Map)['port'] as int;
|
||
for (final line in ['JJMKVMAKER2 wrong ["--play","x.mp4"]', 'JJMKVMAKER1 ["--play","x.mp4"]']) {
|
||
final s = await Socket.connect(InternetAddress.loopbackIPv4, port);
|
||
s.write('$line\n');
|
||
await s.flush();
|
||
final reply = await utf8.decoder.bind(s).join();
|
||
expect(reply, isNot(startsWith('OK')));
|
||
s.destroy();
|
||
}
|
||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||
expect(got, isEmpty);
|
||
await first.close();
|
||
}, skip: !Platform.isWindows);
|
||
|
||
test('첫 창이 응답하지 않으면 unresponsive (묻기 - 같은 데이터 위에 창 두 개를 막는다)', () async {
|
||
final holder = SingleInstance(dataDir: d1.path);
|
||
expect(await holder.claim(const LaunchRequest(LaunchAction.play, [])), ClaimResult.first);
|
||
// 응답하지 않는 창: 받는 곳을 닫았다
|
||
await holder.debugStopListening();
|
||
expect(await SingleInstance(dataDir: d1.path).claim(const LaunchRequest(LaunchAction.play, [])),
|
||
ClaimResult.unresponsive);
|
||
await holder.close();
|
||
}, skip: !Platform.isWindows);
|
||
|
||
test('잠금을 쥔 다른 프로세스가 죽으면 풀려서 다음 실행이 첫 실행', () async {
|
||
// 다른 프로세스가 instance.lock 을 잠근 채 응답하지 않는다 (instance.json 없음)
|
||
final lock = p.join(d1.path, 'instance.lock');
|
||
final holder = await Process.start('powershell', [
|
||
'-NoProfile',
|
||
'-Command',
|
||
"\$f = [IO.File]::Open('$lock', 'OpenOrCreate', 'ReadWrite', 'ReadWrite'); \$f.Lock(0, 1); "
|
||
"[Console]::Out.WriteLine('locked'); [Console]::Out.Flush(); Start-Sleep 60",
|
||
]);
|
||
await holder.stdout.transform(utf8.decoder).firstWhere((l) => l.contains('locked'));
|
||
expect(await SingleInstance(dataDir: d1.path).claim(const LaunchRequest(LaunchAction.play, [])),
|
||
ClaimResult.unresponsive);
|
||
holder.kill();
|
||
await holder.exitCode;
|
||
final next = SingleInstance(dataDir: d1.path);
|
||
expect(await next.claim(const LaunchRequest(LaunchAction.play, [])), ClaimResult.first);
|
||
await next.close();
|
||
}, skip: !Platform.isWindows, timeout: const Timeout(Duration(minutes: 1)));
|
||
});
|
||
|
||
test('탐색기 메뉴 등록 · 해제 (테스트 전용 확장자)', () async {
|
||
if (!Platform.isWindows) return markTestSkipped('Windows 전용');
|
||
final s = ShellIntegration(exePath: r'C:\JJ Test\jj_mkvmaker.exe', extensions: ['jjtestvid']);
|
||
await s.unregister();
|
||
expect(await s.isRegistered(), isFalse);
|
||
expect(await s.register(), isTrue);
|
||
expect(await s.isRegistered(), isTrue);
|
||
final q = await Process.run('reg', [
|
||
'query', r'HKCU\Software\Classes\SystemFileAssociations\.jjtestvid\shell\JJMKVMaker.Play\command', '/ve'
|
||
]);
|
||
expect('${q.stdout}', contains(r'"C:\JJ Test\jj_mkvmaker.exe" --play "%1"'));
|
||
final q2 = await Process.run('reg', [
|
||
'query', r'HKCU\Software\Classes\SystemFileAssociations\.jjtestvid\shell\JJMKVMaker.Subtitle\command', '/ve'
|
||
]);
|
||
expect('${q2.stdout}', contains('--subtitle "%1"'));
|
||
await s.unregister();
|
||
expect(await s.isRegistered(), isFalse);
|
||
});
|
||
|
||
testWidgets('플레이어 화면: 조작 · 단축키 · 오른쪽 메뉴 · 재생 목록', (tester) async {
|
||
tester.view.physicalSize = const Size(1400, 900);
|
||
tester.view.devicePixelRatio = 1;
|
||
addTearDown(tester.view.reset);
|
||
|
||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||
final pl = _FakePlayer();
|
||
await tester.pumpWidget(MaterialApp(
|
||
home: PlayerPage(c: c, player: pl, files: const [r'D:\v\ep1.mp4', r'D:\v\ep2.mp4'], start: 1),
|
||
));
|
||
await tester.pump();
|
||
expect(pl.calls.first, 'open 2 1');
|
||
expect(find.text('ep2.mp4 (2/2)'), findsOneWidget);
|
||
|
||
// Space: 일시정지
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||
await tester.pump();
|
||
expect(pl.state.value.playing, isFalse);
|
||
// → 10초 앞으로, P 이전 파일
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.keyP);
|
||
await tester.pump();
|
||
expect(pl.calls, contains('seek 10'));
|
||
expect(find.text('ep1.mp4 (1/2)'), findsOneWidget);
|
||
// S: 자막 켜기 → 첫 트랙
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
|
||
await tester.pump();
|
||
expect(pl.state.value.subtitleId, '3');
|
||
|
||
// 재생 목록 (L) → 항목 누르면 이동
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.keyL);
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('재생 목록 2개'), findsOneWidget);
|
||
await tester.tap(find.text('ep2.mp4'));
|
||
await tester.pump();
|
||
expect(pl.state.value.index, 1);
|
||
|
||
// 오른쪽 클릭 메뉴
|
||
await tester.tap(find.byType(ColoredBox).first, buttons: kSecondaryButton);
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('전체 화면'), findsOneWidget);
|
||
expect(find.text('팝업 보기'), findsOneWidget);
|
||
expect(find.text('자막 보기'), findsOneWidget);
|
||
expect(find.text('화면 크기'), findsOneWidget);
|
||
expect(find.text('음성 선택'), findsOneWidget); // 음성 트랙 2개
|
||
expect(find.text('자막 크기'), findsOneWidget);
|
||
await tester.tap(find.text('자막 보기'));
|
||
await tester.pumpAndSettle();
|
||
expect(pl.state.value.subtitleId, isNull);
|
||
|
||
// 터치 화면: 길게 누르면 같은 메뉴
|
||
await tester.longPress(find.byType(ColoredBox).first);
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('전체 화면'), findsOneWidget);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.pumpWidget(const SizedBox());
|
||
expect(pl.calls.last, 'dispose');
|
||
});
|
||
|
||
test('동영상 목록: 저장 → 다시 켜면 그대로, 창 두 개가 서로 맞춤 (추가 · 삭제)', () async {
|
||
final dir = Directory.systemTemp.createTempSync('jj_list_');
|
||
addTearDown(() => dir.deleteSync(recursive: true));
|
||
final list = p.join(dir.path, 'videos.json');
|
||
Directory(p.join(dir.path, 'jj_mkv')).createSync();
|
||
final a = p.join(dir.path, 'a.mp4'), b = p.join(dir.path, 'jj_mkv', 'b.mkv');
|
||
for (final f in [a, b]) {
|
||
File(f).writeAsStringSync('x');
|
||
}
|
||
AppController make() =>
|
||
AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||
|
||
final w1 = make();
|
||
await w1.shareVideoList(list);
|
||
await w1.addVideos([a]);
|
||
// 탐색기에서 직접 연 파일은 jj_mkv 안이어도 들어간다 (폴더째 넣을 때는 제외)
|
||
await w1.addVideos([b]);
|
||
expect(w1.videos, hasLength(1));
|
||
await w1.addVideos([b], allowOutputFolder: true);
|
||
expect(w1.videos.map((v) => v.fileName), ['a.mp4', 'b.mkv']);
|
||
|
||
// 새 창: 같은 목록으로 시작
|
||
final w2 = make();
|
||
await w2.shareVideoList(list);
|
||
expect(w2.videos.map((v) => v.fileName), ['a.mp4', 'b.mkv']);
|
||
|
||
// 새 창에서 삭제 → 원래 창에서도 삭제
|
||
w2.removeVideo(w2.videos.first);
|
||
await w1.syncVideoList();
|
||
expect(w1.videos.map((v) => v.fileName), ['b.mkv']);
|
||
// 원래 창에서 추가 → 새 창에도
|
||
await w1.addVideos([a]);
|
||
await w2.syncVideoList();
|
||
expect(w2.videos.map((v) => v.fileName), ['b.mkv', 'a.mp4']);
|
||
|
||
// 없어진 파일은 다시 켤 때 빠진다
|
||
File(a).deleteSync();
|
||
final w3 = make();
|
||
await w3.shareVideoList(list);
|
||
expect(w3.videos.map((v) => v.fileName), ['b.mkv']);
|
||
for (final w in [w1, w2, w3]) {
|
||
w.stopVideoListShare();
|
||
}
|
||
});
|
||
|
||
testWidgets('종료 진행 창: 종료 중입니다 → 단계 → Have You Good Time', (tester) async {
|
||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||
expect(lastWorkText(c), '없음');
|
||
final ran = <String>[];
|
||
final gate = Completer<void>();
|
||
late BuildContext ctx;
|
||
await tester.pumpWidget(MaterialApp(home: Builder(builder: (x) {
|
||
ctx = x;
|
||
return const SizedBox();
|
||
})));
|
||
var closed = false;
|
||
unawaited(showExitProgress(ctx, lastWork: 'MKV 만들기', steps: [
|
||
ExitStep('다운로드 정리', () async => ran.add('dl')),
|
||
ExitStep('쓰레드 확인', () async {
|
||
await gate.future;
|
||
ran.add('thread');
|
||
}),
|
||
ExitStep('실패해도 계속', () async => throw StateError('x')),
|
||
ExitStep('창 종료', () async => ran.add('close')),
|
||
]).then((_) => closed = true));
|
||
await tester.pump();
|
||
expect(find.text('종료 중입니다.'), findsOneWidget);
|
||
expect(find.text('바로 전 작업: MKV 만들기'), findsOneWidget);
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
expect(ran, ['dl']); // 쓰레드 확인에서 기다리는 중
|
||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||
gate.complete();
|
||
for (var i = 0; i < 5; i++) {
|
||
await tester.pump(const Duration(milliseconds: 100));
|
||
}
|
||
expect(ran, ['dl', 'thread', 'close']);
|
||
expect(find.text('Have You Good Time'), findsOneWidget);
|
||
expect(closed, isFalse);
|
||
await tester.pump(const Duration(milliseconds: 800));
|
||
await tester.pump();
|
||
expect(closed, isTrue);
|
||
});
|
||
|
||
testWidgets('종료를 누르면: 설정 기본값 · 고르기 창 (백그라운드로 / 모두 종료 / 묻지 않기)', (tester) async {
|
||
expect(AppSettings().run.closeAction, 'background');
|
||
expect(AppSettings.fromJson((AppSettings()..run.closeAction = 'quit').toJson()).run.closeAction, 'quit');
|
||
expect(AppSettings.fromJson({'closeAction': 'x'}).run.closeAction, 'background');
|
||
|
||
late BuildContext ctx;
|
||
await tester.pumpWidget(MaterialApp(home: Builder(builder: (x) {
|
||
ctx = x;
|
||
return const SizedBox();
|
||
})));
|
||
Future<(String, bool)?> open() => showCloseChoice(ctx, running: '다운로드 2개', hotkey: 'Ctrl+Shift+X');
|
||
|
||
var r = open();
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('지금: 다운로드 2개'), findsOneWidget);
|
||
expect(find.textContaining('Ctrl+Shift+X'), findsOneWidget);
|
||
await tester.tap(find.text('백그라운드로'));
|
||
await tester.pumpAndSettle();
|
||
expect(await r, ('background', false));
|
||
|
||
r = open();
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byType(Checkbox));
|
||
await tester.pump();
|
||
await tester.tap(find.text('모두 종료'));
|
||
await tester.pumpAndSettle();
|
||
expect(await r, ('quit', true));
|
||
|
||
r = open();
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('취소'));
|
||
await tester.pumpAndSettle();
|
||
expect(await r, isNull);
|
||
});
|
||
}
|