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.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 = []; Future 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()); }