JJ_MKVMaker/test/explorer_more_flows_contrac...

228 lines
10 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/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_bars.dart';
import 'package:jj_mkvmaker/ui/explorer/explorer_body.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_layout.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/server_flows.dart';
import 'package:jj_mkvmaker/ui/explorer/toolbar.dart';
import 'package:jj_mkvmaker/ui/explorer_drag.dart' show DragLoad;
import 'package:jj_mkvmaker/ui/explorer_search.dart' show SearchResultsPage;
import 'package:jj_mkvmaker/ui/storage_page.dart' show StorageAnalysisPage;
import 'package:path/path.dart' as p;
/// E5e 약속 시험: explorer_body · search_flow · tools_flow · rsync_flow 맨 위 약속
/// (실제 시험 폴더 · 실제 화면 · 실제 설정 (저장하지 않음). rsync 실행은 rsync 실행 파일이 있어야 해 확인 창까지)
void main() {
late Directory tmp;
late String root, l, r;
setUp(() {
tmp = Directory.systemTemp.createTempSync('jj_more_flow_');
root = tmp.path;
l = p.join(root, 'L');
r = p.join(root, 'R');
File(p.join(l, 'doc.txt'))
..createSync(recursive: true)
..writeAsStringSync('hello');
Directory(r).createSync();
});
tearDown(() => tmp.deleteSync(recursive: true));
Future<void> settle(WidgetTester t, [bool Function()? until]) async {
for (var i = 0; i < 40 && !(until?.call() ?? false); i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await t.pump(const Duration(milliseconds: 100));
}
}
Future<_HostState> open(WidgetTester t, {bool rsync = false, Size size = const Size(1200, 800)}) async {
await t.binding.setSurfaceSize(size);
addTearDown(() => t.binding.setSurfaceSize(null));
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!;
host.make(c, rsync: rsync, root: root);
addTearDown(host.drop);
await t.runAsync(() => host.ctl.goTo(host.ctl.panes[0], l));
await t.runAsync(() => host.ctl.goTo(host.ctl.panes[1], r));
host.ctl.ready = true;
host.redraw();
await t.pump();
return host;
}
testWidgets('창 배치: 두 창 = 창 둘 + 버튼 줄 · 한 창 = 창 하나 · 폴더 + 파일 목록 = 트리 + 목록 · 좁은 Rsync = [왼쪽 | 오른쪽]', (t) async {
final h = await open(t);
expect([find.byType(ExplorerPaneView).evaluate().length, find.byType(ExplorerToolbar).evaluate().length], [2, 1]);
h.ctl.c.settings.explorer.layout = 'single';
h.redraw();
await t.pump();
expect(find.byType(ExplorerPaneView).evaluate().length, 1);
h.ctl.c.settings.explorer.layout = 'split';
h.redraw();
await t.pump();
expect([find.byType(ExplorerPaneView).evaluate().length, find.byType(ExplorerListView).evaluate().length], [1, 1]);
h.ctl.c.settings.explorer.toolbar = 'hidden';
h.redraw();
await t.pump();
expect(find.byType(ExplorerToolbar), findsNothing, reason: '버튼 줄 숨김');
final rs = await open(t, rsync: true, size: const Size(420, 800));
expect([find.byType(RsyncPaneSwitch).evaluate().length, find.byType(ExplorerPaneView).evaluate().length, rs.ctl.rsync], [1, 1, true]);
});
testWidgets('찾기: 처음엔 돌아갈 결과 없음 · 조건 창 [찾기] → 결과 화면에 찾은 것 · 그냥 닫으면 돌아갈 결과도 없음', (t) async {
final h = await open(t);
expect(h.flows.search.backBar(h.context), isNull);
unawaited(h.flows.search.open(h.context, h.ctl.panes[0]));
await t.pump();
await t.enterText(find.byType(TextField).first, 'doc');
await t.pump();
await t.tap(find.widgetWithText(FilledButton, '찾기'));
await settle(t, () => find.byType(SearchResultsPage).evaluate().isNotEmpty && find.text('doc.txt').evaluate().length > 1);
expect(find.byType(SearchResultsPage), findsOneWidget);
Navigator.of(t.element(find.byType(SearchResultsPage))).pop();
await settle(t);
expect(h.flows.search.backBar(h.context), isNull);
});
testWidgets('저장 공간: 원격은 먼저 묻고 [취소] 면 그대로 · 이 기기 폴더는 분석 화면 (고르지 않고 닫으면 그대로)', (t) async {
final h = await open(t);
final q = h.ctl.panes[0];
unawaited(h.flows.tools.openStorage(h.context, q, 'dav://srv/x/'));
await t.pump();
expect(find.textContaining('오래 걸릴 수 있습니다'), findsOneWidget);
await t.tap(find.text('취소'));
await t.pump();
expect(find.byType(StorageAnalysisPage), findsNothing);
var done = false;
unawaited(h.flows.tools.openStorage(h.context, q, l).then((_) => done = true));
await settle(t, () => find.byType(StorageAnalysisPage).evaluate().isNotEmpty);
expect(find.byType(StorageAnalysisPage), findsOneWidget);
Navigator.of(t.element(find.byType(StorageAnalysisPage))).pop();
await settle(t, () => done);
expect([done, q.current], [true, l]);
});
testWidgets('Rsync: 고르지 않으면 까닭 알림 · 두 폴더를 고르면 확인 창 → [취소] 면 실행 안 함', (t) async {
final h = await open(t, rsync: true);
await t.runAsync(() => h.flows.rsync.run(h.context, right: true, left: false));
await t.pump();
expect(find.text('왼쪽 · 오른쪽 창에서 폴더를 하나씩 고르세요.'), findsOneWidget);
await t.runAsync(() => h.sel.pick(h.ctl.panes[0], FileEntry(l, isDir: true, modified: DateTime(0))));
await t.runAsync(() => h.sel.pick(h.ctl.panes[1], FileEntry(r, isDir: true, modified: DateTime(0))));
var done = false;
unawaited(h.flows.rsync.run(h.context, right: true, left: false).then((_) => done = true));
await settle(t, () => find.text('취소').evaluate().isNotEmpty);
await t.tap(find.text('취소').last);
await settle(t, () => done);
expect([done, File(p.join(r, 'doc.txt')).existsSync(), h.flows.jobs.list.isEmpty], [true, false, true]);
});
}
class _Host extends StatefulWidget {
const _Host({super.key});
@override
State<_Host> createState() => _HostState();
}
/// 실제 화면: ExplorerHost · ExplorerCommands · ExplorerPaneParts 를 이 화면이 맡는다 (틀 · 안내 · 표시 막대는 단순하게)
class _HostState extends State<_Host> implements ExplorerHost, ExplorerCommands, ExplorerPaneParts {
late ExplorerController ctl;
late ExplorerSelect sel;
late ExplorerLayout layout;
late ExplorerFlows flows;
late ExplorerButtons buttons;
bool _made = false;
void make(AppController c, {required bool rsync, required String root}) {
ctl = ExplorerController(c, rsync: rsync, host: this)..local = [(root, 'T:')];
sel = ExplorerSelect(ctl, clock: DateTime.now, openFile: (_, _) async {});
layout = ExplorerLayout(ctl, context: () => context);
flows = ExplorerFlows(ctl, sel,
androidTrash: () => false, otherDirOf: (_) => null, otherDir: () => layout.other?.current, twoPanes: () => layout.dual || layout.split);
buttons = ExplorerButtons(ctl, sel, this, flows);
_made = true;
}
void drop() {
flows.dispose();
sel.dispose();
ctl.dispose();
}
void redraw() => 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
String? get otherDir => layout.other?.current;
@override
bool get dual => layout.dual;
@override
double get rowHeight => layout.rowHeight();
@override
double get listTextScale => layout.listTextScale;
@override
bool get narrowSplit => layout.narrowSplit;
@override
bool get alwaysMarks => layout.alwaysMarks;
@override
Widget frame(int i, List<Widget> children) => Column(children: children);
@override
Future<(int, int)?> diskSpace(String path) async => null;
@override
Widget header(ExplorerPane pane) => ExplorerPaneHeader(
ctl: ctl,
servers: ExplorerServers(ctl),
pane: pane,
android: false,
onBookmarks: () {},
onOpenFilter: () {},
onCloseFilter: () {},
onOpenRoot: () async {},
onOpenShizuku: () async {},
onReconnect: (_) async {},
);
@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) => flows.drops.dragPaths(pane, e);
@override
Future<void> onDrop(DragLoad load, int to, String dest) => flows.drops.drop(context, load, to, dest);
@override
void closeFilter(ExplorerPane pane) {}
@override
void filterChanged() => setState(() {});
@override
Widget build(BuildContext context) => Scaffold(
body: !_made ? const SizedBox() : ExplorerPanesBody(ctl: ctl, sel: sel, layout: layout, parts: this, buttons: buttons, flows: flows));
}