JJ_MKVMaker/test/explorer_buttons_contract_t...

194 lines
8.4 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/transfer_job.dart';
import 'package:jj_mkvmaker/core/file_ops.dart' show samePath;
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/explorer/explorer_buttons.dart';
import 'package:jj_mkvmaker/ui/explorer/explorer_controller.dart';
import 'package:jj_mkvmaker/ui/explorer/explorer_flows.dart';
import 'package:jj_mkvmaker/ui/explorer/explorer_host.dart';
import 'package:jj_mkvmaker/ui/explorer/explorer_pane.dart';
import 'package:jj_mkvmaker/ui/explorer/explorer_select.dart';
import 'package:jj_mkvmaker/ui/explorer/marks_bar.dart';
import 'package:jj_mkvmaker/ui/explorer/toolbar.dart';
import 'package:jj_mkvmaker/ui/explorer/transfer_panel.dart';
import 'package:path/path.dart' as p;
/// E5d-4 약속 시험: explorer_buttons · toolbar · marks_bar · transfer_panel · job_view 맨 위 약속
/// (실제 시험 폴더 · 실제 화면 · 실제 복사 작업. 화면의 일 [ExplorerCommands] 은 이 시험 화면이 받아 적는다)
void main() {
late Directory tmp;
late String root, show, ep1;
setUp(() {
tmp = Directory.systemTemp.createTempSync('jj_btn_ui_');
root = tmp.path;
show = p.join(root, 'Show');
Directory(show).createSync();
ep1 = p.join(show, 'ep1.mkv');
File(ep1).writeAsStringSync('1');
File(p.join(show, 'ep2.mkv')).writeAsStringSync('2');
});
tearDown(() => tmp.deleteSync(recursive: true));
Future<(ExplorerButtons, ExplorerController, ExplorerSelect, _HostState)> open(WidgetTester t) async {
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
final key = GlobalKey<_HostState>();
await t.pumpWidget(MaterialApp(home: _Host(key: key)));
final host = key.currentState!;
final ctl = ExplorerController(c, rsync: false, host: host)..local = [(root, 'T:')];
final sel = ExplorerSelect(ctl, clock: DateTime.now, openFile: (_, _) async {});
addTearDown(() {
sel.dispose();
ctl.dispose();
});
await t.runAsync(() => ctl.goTo(ctl.panes[0], show));
final flows = ExplorerFlows(ctl, sel, androidTrash: () => false, otherDirOf: (_) => null, otherDir: () => null, twoPanes: () => false);
addTearDown(flows.dispose);
final b = ExplorerButtons(ctl, sel, host, flows);
return (b, ctl, sel, host);
}
testWidgets('버튼: 상위 폴더 · 선택 · 새 폴더 (실제로 만들고 그 줄로) · 한 창 복사 = 담기 · 길게 = 짝 동작 (영구 삭제 창)', (t) async {
final (b, ctl, sel, _) = await open(t);
final q = ctl.pane;
await t.runAsync(() => b.press(ExplorerButton.select));
expect(sel.selecting, isTrue);
var made0 = false;
b.press(ExplorerButton.newFolder).then((_) => made0 = true);
await t.pump();
await t.enterText(find.byType(TextField), 'New');
await t.tap(find.text('확인'));
for (var i = 0; i < 20 && !made0; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await t.pump(const Duration(milliseconds: 100));
}
final made = p.join(show, 'New');
expect([Directory(made).existsSync(), samePath(q.focused!, made)], [true, true]);
await t.runAsync(() => sel.toggleMark(q, sel.shown(q, show).firstWhere((e) => samePath(e.path, ep1))));
await t.runAsync(() => b.press(ExplorerButton.copy));
expect([b.flows.transfers.clip.items, b.flows.transfers.clip.move], [
[ep1],
false
]);
await t.runAsync(() => b.longPress(ExplorerButton.copy));
expect(b.flows.transfers.clip.move, isTrue, reason: '복사 길게 = 이동');
unawaited(b.longPress(ExplorerButton.delete));
await t.pump();
expect([find.text('영구 삭제').evaluate().isNotEmpty, b.flows.deletes.shiftDelete], [true, false]);
await t.tap(find.text('취소'));
await t.pump();
expect(File(ep1).existsSync(), isTrue, reason: '취소하면 그대로');
await t.runAsync(() => b.longPress(ExplorerButton.refresh));
await t.runAsync(() => b.press(ExplorerButton.up));
expect(samePath(q.current, root), isTrue);
await t.runAsync(() => b.press(ExplorerButton.up));
expect(samePath(q.current, root), isTrue, reason: '맨 위에서는 그대로');
expect([ExplorerButtons.longPressLabel(ExplorerButton.copy), ExplorerButtons.longPressLabel(ExplorerButton.up)], ['이동', null]);
});
testWidgets('버튼 줄: 누르기 · 못 누르는 버튼 · 넘치면 [더 보기] (고정한 것은 줄에)', (t) async {
final pressed = <ExplorerButton>[];
Future<void> bar(double height) async {
await t.pumpWidget(MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: SizedBox(
height: height,
child: ExplorerToolbar(
buttons: const [ExplorerButton.copy, ExplorerButton.move, ExplorerButton.paste, ExplorerButton.delete, ExplorerButton.orient],
vertical: true,
onPressed: pressed.add,
enabled: (b) => b != ExplorerButton.paste,
selected: (_) => false,
))))));
}
await bar(600);
await t.tap(find.text('복사'));
await t.tap(find.text('붙여넣기'));
expect([pressed, find.text('더 보기').evaluate().isEmpty], [
[ExplorerButton.copy],
true
]);
await bar(200);
expect([find.text('더 보기').evaluate().length, find.byIcon(Icons.swap_horiz).evaluate().length], [1, 1]);
});
testWidgets('표시 막대: 수 · 모두 선택 · 표시 지우기 · 선택 끝', (t) async {
final (_, ctl, sel, host) = await open(t);
final q = ctl.pane;
sel.setSelecting(true);
await t.runAsync(() => sel.toggleMark(q, sel.shown(q, show).first));
host.show(() => ExplorerMarksBar(sel: sel, pane: q), sel);
await t.pump();
expect(find.textContaining('1개 표시함'), findsOneWidget);
await t.tap(find.text('모두 선택'));
await t.pump();
expect(find.textContaining('2개 표시함'), findsOneWidget);
await t.tap(find.text('표시 지우기'));
await t.pump();
expect(q.sel.isEmpty, isTrue);
await t.tap(find.text('선택 끝'));
expect(sel.selecting, isFalse);
});
testWidgets('진행 패널: 실제 복사 작업 · 끝나면 완료 · 숨기면 작은 표시', (t) async {
final (_, ctl, _, host) = await open(t);
final dest = p.join(root, 'Out');
Directory(dest).createSync();
final job = TransferJob(sources: [ep1], dest: dest, move: false);
await t.runAsync(job.run);
expect(File(p.join(dest, 'ep1.mkv')).existsSync(), isTrue);
host.show(() => ExplorerTransferPanel(c: ctl.c, jobs: [job], rsync: false), ctl.c);
await t.pump();
expect(find.textContaining('완료'), findsOneWidget);
await t.runAsync(() => ctl.c.updateSettings((s) => s.explorer.progressHidden = true));
await t.pump();
expect([find.byType(TransferProgressChip).evaluate().length, find.textContaining('완료').evaluate().length], [1, 0]);
});
}
class _Host extends StatefulWidget {
const _Host({super.key});
@override
State<_Host> createState() => _HostState();
}
/// 실제 화면: ExplorerHost · ExplorerCommands 를 이 화면이 맡는다
class _HostState extends State<_Host> implements ExplorerHost, ExplorerCommands {
Widget Function()? child;
/// [build] 로 그리고, [changes] 가 바뀔 때마다 다시 그린다
void show(Widget Function() build, Listenable changes) {
changes.addListener(() {
if (mounted) setState(() {});
});
setState(() => child = build);
}
@override
bool get alive => mounted;
@override
void snack(String text) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
@override
void reveal(ExplorerPane pane, String path) {}
@override
void afterFrame(VoidCallback f) => WidgetsBinding.instance.addPostFrameCallback((_) => f());
@override
String? get otherDir => null;
@override
bool get dual => false;
@override
Widget build(BuildContext context) => Scaffold(body: child?.call() ?? const SizedBox());
}