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/core/explorer/tree_rows.dart'; import 'package:jj_mkvmaker/core/file_ops.dart' show FileEntry; 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_controller.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:path/path.dart' as p; /// E5d-2 약속 시험: lib/ui/explorer/explorer_select.dart 맨 위 약속 (실제 시험 폴더 · Host 는 실제 화면). void main() { late Directory tmp; late String root, show, ep1, ep2; setUp(() { tmp = Directory.systemTemp.createTempSync('jj_sel_ui_'); root = tmp.path; show = p.join(root, 'Show'); Directory(show).createSync(); ep1 = p.join(show, 'ep1.mkv'); ep2 = p.join(show, 'ep2.mkv'); File(ep1).writeAsStringSync('1'); File(ep2).writeAsStringSync('2'); }); tearDown(() => tmp.deleteSync(recursive: true)); var now = DateTime(2026, 10, 22, 9); Future<(ExplorerSelect, ExplorerPane, List)> open(WidgetTester t, {bool rsync = false}) async { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); final key = GlobalKey<_HostState>(); await t.pumpWidget(MaterialApp(home: _Host(key: key))); final ctl = ExplorerController(c, rsync: rsync, host: key.currentState!)..local = [(root, 'T:')]; final opened = []; final sel = ExplorerSelect(ctl, clock: () => now, openFile: (path, _) async => opened.add(path)); addTearDown(() { sel.dispose(); ctl.dispose(); }); final q = ctl.panes[0]; await t.runAsync(() => ctl.goTo(q, root)); return (sel, q, opened); } TreeRow rowOf(String path, {bool dir = false}) => TreeRow(FileEntry(path, isDir: dir, modified: DateTime(0)), 1); testWidgets('폴더 한 번 = 펼침 · 다시 = 접힘 (두 번 누르기의 두 번째는 아님) · 파일 한 번 = 지금 줄 · 두 번 = 열기', (t) async { final (sel, q, opened) = await open(t); await t.runAsync(() => sel.onTap(q, rowOf(show, dir: true))); expect(q.expanded.contains(show), isTrue); now = now.add(const Duration(milliseconds: 100)); await t.runAsync(() => sel.onTap(q, rowOf(show, dir: true))); expect(q.expanded.contains(show), isTrue, reason: '두 번 누르기의 두 번째'); now = now.add(const Duration(seconds: 1)); await t.runAsync(() => sel.onTap(q, rowOf(show, dir: true))); expect(q.expanded.contains(show), isFalse, reason: '규칙 3'); await t.runAsync(() => sel.onTap(q, rowOf(show, dir: true))); await t.runAsync(() => sel.onTap(q, rowOf(ep1))); expect([q.focused, opened], [ep1, []]); await t.runAsync(() => sel.onOpen(q, rowOf(ep1))); expect(opened, [ep1]); }); testWidgets('선택 모드: 파일 고름 · 폴더 = 자기 + 안 · 범위 · 뒤집기 · 끄면 모두 풀림', (t) async { final (sel, q, _) = await open(t); sel.setSelecting(true); await t.runAsync(() => sel.onTap(q, rowOf(show, dir: true))); expect([sel.isMarked(q, ep1), q.expanded.contains(show)], [true, true]); await t.runAsync(() => sel.onTap(q, rowOf(show, dir: true))); expect(q.sel.isEmpty, isTrue); await t.runAsync(() => sel.toggleMark(q, FileEntry(ep1, isDir: false, modified: DateTime(0)))); expect(sel.canRange(q, FileEntry(ep2, isDir: false, modified: DateTime(0))), isTrue); sel.markRange(q, FileEntry(ep2, isDir: false, modified: DateTime(0))); expect(q.sel.marked.length, 2); sel.invertMarks(q, show); expect(q.sel.isEmpty, isTrue); sel.markWhere(q, show, (e) => e.path.endsWith('2.mkv')); expect(q.sel.marked, [ep2]); sel.setSelecting(false); expect([sel.selecting, q.sel.isEmpty], [false, true]); }); testWidgets('Rsync: 폴더만 하나 · 고르면 펼침', (t) async { final (sel, q, _) = await open(t, rsync: true); await t.runAsync(() => sel.onTap(q, rowOf(ep1))); expect(q.sel.isEmpty, isTrue); await t.runAsync(() => sel.onTap(q, rowOf(show, dir: true))); expect([q.sel.marked, q.expanded.contains(show)], [ [show], true ]); }); } class _Host extends StatefulWidget { const _Host({super.key}); @override State<_Host> createState() => _HostState(); } class _HostState extends State<_Host> implements ExplorerHost { @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 Widget build(BuildContext context) => const Scaffold(body: SizedBox()); }