209 lines
8.6 KiB
Dart
209 lines
8.6 KiB
Dart
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/file_ops.dart' show FileEntry, 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_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:jj_mkvmaker/ui/explorer/pane_header.dart';
|
|
import 'package:jj_mkvmaker/ui/explorer/pane_view.dart';
|
|
import 'package:jj_mkvmaker/ui/explorer/path_dialog.dart';
|
|
import 'package:jj_mkvmaker/ui/explorer/server_flows.dart';
|
|
import 'package:jj_mkvmaker/ui/explorer_drag.dart' show DragLoad;
|
|
import 'package:jj_mkvmaker/ui/explorer_look.dart' show rowSpacingFactor;
|
|
import 'package:path/path.dart' as p;
|
|
|
|
/// E5d-3 약속 시험: path_dialog · server_flows · pane_header · pane_view 맨 위 약속 (실제 시험 폴더 · 실제 화면 · 실제 창)
|
|
void main() {
|
|
late Directory tmp;
|
|
late String root, show;
|
|
setUp(() {
|
|
tmp = Directory.systemTemp.createTempSync('jj_pane_ui_');
|
|
root = tmp.path;
|
|
show = p.join(root, 'Show');
|
|
Directory(show).createSync();
|
|
File(p.join(show, 'ep1.mkv')).writeAsStringSync('1');
|
|
File(p.join(root, 'a.txt')).writeAsStringSync('a');
|
|
});
|
|
tearDown(() => tmp.deleteSync(recursive: true));
|
|
|
|
/// 실제 I/O 를 기다린 뒤 그린다
|
|
Future<void> settle(WidgetTester t) async {
|
|
for (var i = 0; i < 10; i++) {
|
|
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
|
await t.pump(const Duration(milliseconds: 100));
|
|
}
|
|
}
|
|
|
|
test('경로 확인: 빈 칸 · 있는 폴더 · 없는 폴더 · Android 의 SMB', () async {
|
|
expect(await checkEnteredPath(' ', android: false), (null, null));
|
|
expect(await checkEnteredPath('"$show"', android: false), (show, null));
|
|
final (miss, why) = await checkEnteredPath(p.join(root, 'none'), android: false);
|
|
expect([miss, why!.isNotEmpty], [null, true]);
|
|
expect(await checkEnteredPath('smb://nas/v', android: true), (null, smbNotSupported));
|
|
});
|
|
|
|
Future<(ExplorerController, GlobalKey<_HostState>)> open(WidgetTester t, {double width = 800}) async {
|
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
|
final key = GlobalKey<_HostState>();
|
|
await t.binding.setSurfaceSize(Size(width, 600));
|
|
addTearDown(() => t.binding.setSurfaceSize(null));
|
|
await t.pumpWidget(MaterialApp(home: _Host(key: key)));
|
|
final ctl = ExplorerController(c, rsync: false, host: key.currentState!)..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], root));
|
|
key.currentState!.attach(ctl, sel);
|
|
await t.pump();
|
|
return (ctl, key);
|
|
}
|
|
|
|
testWidgets('경로 입력 창: 없는 폴더는 창을 닫지 않고 이유 · 있는 폴더는 창을 옮김 · Android SMB 는 서버 추가로', (t) async {
|
|
final (ctl, key) = await open(t);
|
|
final q = ctl.panes[0];
|
|
final servers = ExplorerServers(ctl);
|
|
var done = false;
|
|
servers.enterPath(key.currentContext!, q, android: false).then((_) => done = true);
|
|
await t.pump();
|
|
await t.enterText(find.byType(TextField).last, p.join(root, 'none'));
|
|
await t.tap(find.text('확인'));
|
|
await settle(t);
|
|
expect([find.byType(PathDialog).evaluate().length, find.text('폴더가 없거나 열 수 없습니다').evaluate().length], [1, 1]);
|
|
await t.enterText(find.byType(TextField).last, show);
|
|
await t.tap(find.text('확인'));
|
|
await settle(t);
|
|
expect([done, find.byType(PathDialog).evaluate().isEmpty, samePath(q.current, show)], [true, true, true]);
|
|
|
|
String? smb;
|
|
showDialog<String>(context: key.currentContext!, builder: (_) => const PathDialog(initial: '', android: true)).then((v) => smb = v);
|
|
await t.pump();
|
|
await t.enterText(find.byType(TextField).last, 'smb://nas/v');
|
|
await t.tap(find.text('확인'));
|
|
await settle(t);
|
|
await t.tap(find.text('SMB 서버 추가'));
|
|
await settle(t);
|
|
expect(smb, '${PathDialog.addSmb}smb://nas/v');
|
|
});
|
|
|
|
testWidgets('창 보기: 머리 · 줄 · 빠른 필터 수 · 좁은 창은 [⋮]', (t) async {
|
|
final (ctl, key) = await open(t);
|
|
final q = ctl.panes[0];
|
|
expect(find.text('a.txt'), findsOneWidget);
|
|
expect(find.byIcon(Icons.more_vert), findsNothing, reason: '넓은 창은 끝 버튼을 펼침');
|
|
// 뒤로 · 앞으로: 처음은 갈 곳 없음 → 폴더를 옮기면 뒤로가 켜짐
|
|
expect(t.widget<IconButton>(find.widgetWithIcon(IconButton, Icons.arrow_back)).onPressed, isNull);
|
|
await t.runAsync(() => ctl.goTo(q, show));
|
|
await t.pump();
|
|
expect(t.widget<IconButton>(find.widgetWithIcon(IconButton, Icons.arrow_back)).onPressed, isNotNull);
|
|
await t.runAsync(() => ctl.goTo(q, root));
|
|
await t.pump();
|
|
// 빠른 필터: 머리 단추 → 화면 콜백 → 칸 · 보이는 수
|
|
await t.tap(find.widgetWithIcon(IconButton, Icons.filter_alt_outlined));
|
|
await t.pump();
|
|
expect(key.currentState!.calls, ['openFilter']);
|
|
await t.enterText(find.byType(TextField), 'a.t');
|
|
await t.pump();
|
|
expect(find.text('1개 보임'), findsOneWidget);
|
|
await t.tap(find.widgetWithIcon(IconButton, Icons.close));
|
|
await t.pump();
|
|
expect(key.currentState!.calls, ['openFilter', 'filterChanged', 'closeFilter']);
|
|
await t.binding.setSurfaceSize(const Size(400, 600));
|
|
await t.pump();
|
|
expect(find.byIcon(Icons.more_vert), findsOneWidget, reason: '좁은 창 ($paneHeaderWideWidth 미만) 은 [⋮] 하나');
|
|
});
|
|
}
|
|
|
|
class _Host extends StatefulWidget {
|
|
const _Host({super.key});
|
|
@override
|
|
State<_Host> createState() => _HostState();
|
|
}
|
|
|
|
/// 실제 화면: ExplorerHost · ExplorerPaneParts 를 이 화면이 맡는다 (틀 · 안내 · 표시 막대는 비움, 부른 것은 [calls] 에)
|
|
class _HostState extends State<_Host> implements ExplorerHost, ExplorerPaneParts {
|
|
ExplorerController? ctl;
|
|
ExplorerSelect? sel;
|
|
final calls = <String>[];
|
|
|
|
void attach(ExplorerController c, ExplorerSelect s) {
|
|
ctl = c..addListener(_redraw);
|
|
sel = s;
|
|
for (final q in c.panes) {
|
|
q.addListener(_redraw);
|
|
}
|
|
setState(() {});
|
|
}
|
|
|
|
void _redraw() {
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
@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
|
|
double get rowHeight => ctl!.look.rowHeightOf(context, textScale: 1, spacing: rowSpacingFactor(''));
|
|
@override
|
|
double get listTextScale => 1;
|
|
@override
|
|
bool get narrowSplit => false;
|
|
@override
|
|
bool get alwaysMarks => false;
|
|
@override
|
|
Widget frame(int i, List<Widget> children) => Column(children: children);
|
|
@override
|
|
Widget header(ExplorerPane pane) => ExplorerPaneHeader(
|
|
ctl: ctl!,
|
|
servers: ExplorerServers(ctl!),
|
|
pane: pane,
|
|
android: false,
|
|
onBookmarks: () => calls.add('bookmarks'),
|
|
onOpenFilter: () => setState(() {
|
|
calls.add('openFilter');
|
|
pane.filterOpen = true;
|
|
}),
|
|
onCloseFilter: () => closeFilter(pane),
|
|
onOpenRoot: () async => calls.add('root'),
|
|
onOpenShizuku: () async => calls.add('shizuku'),
|
|
onReconnect: (_) async => calls.add('reconnect'),
|
|
);
|
|
@override
|
|
List<Widget?> notices(ExplorerPane pane, String dir) => const [];
|
|
@override
|
|
Widget marksBar(ExplorerPane pane, {String? listDir}) => const SizedBox();
|
|
@override
|
|
void onMenu(ExplorerPane pane, FileEntry e, Offset at) {}
|
|
@override
|
|
List<String> dragPaths(ExplorerPane pane, FileEntry e) => [e.path];
|
|
@override
|
|
Future<void> onDrop(DragLoad load, int to, String dest) async {}
|
|
@override
|
|
void closeFilter(ExplorerPane pane) => setState(() {
|
|
calls.add('closeFilter');
|
|
pane
|
|
..filterOpen = false
|
|
..filterCtl.clear();
|
|
});
|
|
@override
|
|
void filterChanged() => setState(() => calls.add('filterChanged'));
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Scaffold(
|
|
body: ctl == null ? const SizedBox() : ExplorerPaneView(ctl: ctl!, sel: sel!, parts: this, index: 0));
|
|
}
|