next10: hwp_pdf (ced0ae9) + rsync_narrow (dfed3fa: 412 Rsync 화면 오른쪽 창 · 옵션 이름 줄바꿈) - 시험판 986
합침 (d3): copy_sync_settings 의 import 한 줄 (optionProblemText · keepOptionsTogether 둘 다). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>next10 v2026.10.18_986
commit
56c632d372
|
|
@ -0,0 +1,98 @@
|
|||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/i18n_controller.dart';
|
||||
import 'package:jj_mkvmaker/l10n/tr.dart';
|
||||
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_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/temp_dir.dart';
|
||||
|
||||
/// 28 확인 (10/20): 좁은 Rsync 화면의 [오른쪽] 탭을 처음 볼 때 그 창의 지금 폴더가 보이는지 · 확인 창의 체크 제목
|
||||
/// "(--remove-source-files)" 가 "(--" 에서 갈리지 않는지를 412 · 960, ko · en · ja 로 찍는다 (시험 폴더 안에서만, [취소]).
|
||||
/// 실행: flutter test integration_test\windows_rsync_narrow_shot_test.dart -d windows (JJ_SHOT_DIR 에 PNG)
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_rsync_narrow');
|
||||
for (final lang in ['ko', 'en', 'ja']) {
|
||||
for (final (w, h) in [(412, 900), (960, 600)]) {
|
||||
testWidgets('좁은 Rsync 화면 $lang ${w}x$h', (t) async {
|
||||
t.view.physicalSize = Size(w.toDouble(), h.toDouble());
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||
addTearDown(() => i18n.apply('ko', save: false));
|
||||
final tmp = testTempDir('jj_rsnarrow_shot_');
|
||||
// 맨 위에 폴더가 많고 (C: 처럼) 두 창의 지금 폴더는 깊은 곳
|
||||
for (var i = 0; i < 60; i++) {
|
||||
Directory(p.join(tmp.path, 'a${i.toString().padLeft(2, '0')}')).createSync();
|
||||
}
|
||||
final l = p.join(tmp.path, 'zz', 'deep', 'L'), r = p.join(tmp.path, 'zz', 'deep', 'R');
|
||||
File(p.join(l, 'sub', 'a.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('a');
|
||||
Directory(p.join(r, 'rsub')).createSync(recursive: true);
|
||||
for (var i = 0; i < 20; i++) {
|
||||
Directory(p.join(tmp.path, 'zz${i.toString().padLeft(2, '0')}')).createSync();
|
||||
}
|
||||
ExplorerPage.debugVolumes = () => [(tmp.path, 'T:')];
|
||||
addTearDown(() => ExplorerPage.debugVolumes = null);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerOrientation = 'side'
|
||||
..rsyncPaths = [l, r];
|
||||
final shotKey = GlobalKey();
|
||||
await t.pumpWidget(MaterialApp(
|
||||
theme: buildTheme(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: (_, child) => RepaintBoundary(key: shotKey, child: child),
|
||||
home: ExplorerPage(c: c, rsync: true),
|
||||
));
|
||||
Future<void> settle([int n = 10]) async {
|
||||
for (var i = 0; i < n; i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 60)));
|
||||
await t.pump(const Duration(milliseconds: 60));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> shot(String name) async {
|
||||
await settle(3);
|
||||
expect(t.takeException(), isNull, reason: '$name 넘침');
|
||||
final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||
final img = await t.runAsync(() => b.toImage());
|
||||
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
||||
Directory(out).createSync(recursive: true);
|
||||
File(p.join(out, 'rsync_narrow_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
await settle(20);
|
||||
final narrow = find.text('${tr('오른쪽')} · R').evaluate().isNotEmpty;
|
||||
await t.tap(find.text('sub').hitTestable());
|
||||
await settle();
|
||||
if (narrow) {
|
||||
await t.tap(find.text('${tr('오른쪽')} · R'));
|
||||
await settle(15);
|
||||
expect(find.text('rsub').hitTestable(), findsOneWidget, reason: '[오른쪽] 탭을 처음 보면 지금 폴더');
|
||||
await shot('1_right_tab');
|
||||
}
|
||||
await t.tap(find.text('rsub').hitTestable());
|
||||
await settle();
|
||||
await t.tap(find.text(tr('좌 → 우')).first);
|
||||
await settle(25);
|
||||
await shot('2_confirm');
|
||||
await t.tap(find.text(tr('취소')).last);
|
||||
await settle();
|
||||
expect(File(p.join(l, 'sub', 'a.txt')).existsSync(), isTrue, reason: '찍기만 - 실행하지 않음');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import 'rsync_setup.dart';
|
|||
import 'schedule_editor.dart';
|
||||
import 'theme.dart';
|
||||
import 'setting_tile.dart';
|
||||
import 'sync_preview_view.dart' show OptionTextFormatter, optionProblemText;
|
||||
import 'sync_preview_view.dart' show OptionTextFormatter, keepOptionsTogether, optionProblemText;
|
||||
|
||||
/// 환경 설정의 복사 · 동기화 부분.
|
||||
/// - 파일 탐색기 ([rsync] false): 파일 / 폴더 복사 방법 (현재 방식 · robocopy), robocopy 옵션, 속도 제한
|
||||
|
|
@ -183,7 +183,7 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
value: c.settings.copySkipNewer,
|
||||
onChanged: (v) => c.updateSettings((x) => x.copySkipNewer = v),
|
||||
title: Text(tr('받는 쪽이 더 새것이면 건너뛰기')),
|
||||
subtitle: Text(tr('현재 방식 · robocopy · WebDAV 로 복사 · 이동할 때 (rsync 의 -u, robocopy 의 /XO 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다')),
|
||||
subtitle: Text(keepOptionsTogether(tr('현재 방식 · robocopy · WebDAV 로 복사 · 이동할 때 (rsync 의 -u, robocopy 의 /XO 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다'))),
|
||||
);
|
||||
|
||||
Widget _rsyncPart() {
|
||||
|
|
@ -342,7 +342,7 @@ class _LiveSyncTile extends StatelessWidget {
|
|||
value: delete,
|
||||
onChanged: (v) => set(() => delete = v ?? false),
|
||||
title: Text(tr('원본에 없는 것을 대상에서 지우기')),
|
||||
subtitle: Text(tr('rsync --delete · robocopy /PURGE. 끄면 대상에 더하기 · 바꾸기만')),
|
||||
subtitle: Text(keepOptionsTogether(tr('rsync --delete · robocopy /PURGE. 끄면 대상에 더하기 · 바꾸기만'))),
|
||||
),
|
||||
SettingTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
|
|
|
|||
|
|
@ -245,6 +245,9 @@ class _Pane extends ChangeNotifier {
|
|||
final back = <String>[];
|
||||
final forward = <String>[];
|
||||
bool traveling = false;
|
||||
|
||||
/// 이 창의 목록이 화면에 붙어 있는지 (새로 붙을 때 지금 폴더를 보이게 - [_ExplorerPageState._revealNewViews])
|
||||
bool onScreen = false;
|
||||
String? focused;
|
||||
final expanded = <String>{};
|
||||
final cache = <String, List<FileEntry>>{};
|
||||
|
|
@ -1058,8 +1061,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
}
|
||||
|
||||
/// 그 항목이 목록 위쪽 1/4 쯤에 보이게 스크롤 (깊은 폴더로 가면 위쪽 폴더들의 다른 항목에 밀려 안 보이므로)
|
||||
void _reveal(_Pane pane, String path) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
void _reveal(_Pane pane, String path) => WidgetsBinding.instance.addPostFrameCallback((_) => _revealNow(pane, path));
|
||||
|
||||
void _revealNow(_Pane pane, String path) {
|
||||
if (!mounted || !pane.scroll.hasClients) return;
|
||||
// 화면의 트리와 같은 줄로 센다 (폴더 + 파일 목록 배치 · Rsync 화면은 폴더만)
|
||||
final i = _rows(pane, foldersOnly: _split || widget.rsync).indexWhere((r) => r.kind == 0 && samePath(r.entry.path, path));
|
||||
|
|
@ -1068,9 +1072,19 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// 180: 끝 (maxScrollExtent) 으로 자르지 않는다 - 줄이 막 늘어난 때는 끝이 아직 앞의 것이라 보던 폴더로 못 간다.
|
||||
// 넘치면 다음 배치 때 실제 끝으로 맞춰진다
|
||||
pane.scroll.jumpTo((i * _look.rowHeightOf(context) - pos.viewportDimension / 4).clamp(0.0, double.infinity));
|
||||
});
|
||||
}
|
||||
|
||||
/// 28 확인 (10/20): 창의 목록이 화면에 새로 붙으면 (좁은 Rsync 화면의 [오른쪽] 탭을 처음 볼 때 · 폭이 바뀌어 두 창이 될 때)
|
||||
/// 그 창의 지금 폴더를 보이게 한다. 켤 때 그려지지 않은 창은 [_goTo] 의 스크롤을 건너뛰어, 나중에 보면 맨 위 (저장 장치) 에 있었다
|
||||
void _revealNewViews() => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
for (final pane in _panes) {
|
||||
final on = pane.scroll.hasClients;
|
||||
if (on && !pane.onScreen && pane.current.isNotEmpty) _revealNow(pane, pane.current);
|
||||
pane.onScreen = on;
|
||||
}
|
||||
});
|
||||
|
||||
/// 75: 창마다 뒤로 · 앞으로 (가 본 폴더로)
|
||||
Future<void> _travel(_Pane pane, {required bool forward}) async {
|
||||
// 앞의 이동이 끝나기 전에 또 불리면 (빨리 두 번 · 같은 누름이 두 번 들어옴) 무시 - 내역에서 두 칸이 빠지지 않게
|
||||
|
|
@ -3507,7 +3521,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
|
||||
// 1280×720 등 낮은 화면에서도 보이게: 원본 지우기 체크 · 경고는 비교 목록보다 위에 (178)
|
||||
if (both) ...[
|
||||
Text(tr('양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).'),
|
||||
Text(keepOptionsTogether(tr('양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).')),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (optionMove)
|
||||
Text(tr('양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).'),
|
||||
|
|
@ -3522,7 +3536,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
preview = null;
|
||||
previewError = null;
|
||||
}),
|
||||
title: Text(tr('원본 파일 지우기 (--remove-source-files)')),
|
||||
title: Text(keepOptionsTogether(tr('원본 파일 지우기 (--remove-source-files)'))),
|
||||
subtitle: Text(tr('대상으로 옮긴 파일을 원본에서 지웁니다 (이동)')),
|
||||
),
|
||||
if (removeSource) ...[
|
||||
|
|
@ -4453,6 +4467,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
final dual = _dual;
|
||||
final split = _split;
|
||||
return LayoutBuilder(builder: (context, box) {
|
||||
// 폭이 바뀌면 (좁은 한 창 ↔ 두 창) 이 builder 만 다시 불린다 - 그때도 새로 붙은 창을 보이게
|
||||
if (_ready) _revealNewViews();
|
||||
final side = !(dual || split) ||
|
||||
switch (s.explorerOrientation) {
|
||||
'side' => true,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import 'path_label.dart';
|
|||
import 'rsync_setup.dart';
|
||||
import 'schedule_editor.dart';
|
||||
import '../core/sync_preview.dart' show optionsRemoveSource;
|
||||
import 'sync_preview_view.dart' show FilterLimitNote, OptionProblemNote, OptionTextFormatter, confirmDeletingRun, effectiveRunText, unbreakableOption;
|
||||
import 'sync_preview_view.dart' show FilterLimitNote, OptionProblemNote, OptionTextFormatter, confirmDeletingRun, effectiveRunText, keepOptionsTogether, unbreakableOption;
|
||||
import 'theme.dart';
|
||||
|
||||
/// 모니터링 (파일 탐색기 > 모니터링): 복사 · rsync 와 lsync (실시간 동기화) 를 따로 보여 준다.
|
||||
|
|
@ -135,9 +135,9 @@ Future<bool?> _liveDeleteDialog(BuildContext context, LiveSync live, LiveSyncPai
|
|||
if (plain.length > 500) Text(trf('… 외 {0}개', [plain.length - 500])),
|
||||
if (filtered.isNotEmpty) ...[
|
||||
if (plain.isNotEmpty) const SizedBox(height: 10),
|
||||
Text(trf('거른 것 {0}개도 지워집니다 (--delete-excluded)', [filtered.length]),
|
||||
Text(keepOptionsTogether(trf('거른 것 {0}개도 지워집니다 (--delete-excluded)', [filtered.length])),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: JjColors.danger)),
|
||||
Text(tr('rsync 옵션에서 뺀 것 (--exclude 등) 이지만 --delete-excluded 가 있어 대상에서 지웁니다. 지키려면 옵션에서 --delete-excluded 를 빼세요.'),
|
||||
Text(keepOptionsTogether(tr('rsync 옵션에서 뺀 것 (--exclude 등) 이지만 --delete-excluded 가 있어 대상에서 지웁니다. 지키려면 옵션에서 --delete-excluded 를 빼세요.')),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 4),
|
||||
for (final n in filtered.take(500)) Text(n, style: const TextStyle(fontSize: 12, fontFamily: 'Consolas', color: JjColors.danger)),
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
|
|||
IconButton(tooltip: tr('다시 비교'), icon: const Icon(Icons.refresh, size: 18), onPressed: _run),
|
||||
]),
|
||||
if (dels > 0)
|
||||
Text(trf('받는 쪽에서 {0}개가 지워집니다 (--delete, 되돌릴 수 없음)', [dels]),
|
||||
Text(keepOptionsTogether(trf('받는 쪽에서 {0}개가 지워집니다 (--delete, 되돌릴 수 없음)', [dels])),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)),
|
||||
// 15 검토 F4: 받는 쪽에만 있는 받다 만 파일 (*.jjpart 등) 도 지워진다는 것을 따로
|
||||
if (partials > 0)
|
||||
|
|
@ -316,6 +316,12 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
|
|||
/// 빈칸은 줄바꿈 없는 빈칸, - · / 뒤에는 줄바꿈을 막는 글자 (U+2060, 보이지 않음)
|
||||
String unbreakableOption(String s) => s.replaceAll(' ', '\u00A0').replaceAllMapped(RegExp(r'[-/]'), (m) => '${m[0]}\u2060');
|
||||
|
||||
/// 28 확인 (10/20): 글 안의 옵션 이름만 묶는다 - 괄호째 ("(--remove-source-files)" · "(--delete, 되돌릴 수 없음)") 와 따로 쓴
|
||||
/// "--delete-excluded" · "-u" · "/PURGE". 예전에는 en 412 에서 "(--" 에서 갈렸다. 나머지 글은 그대로 줄바꿈
|
||||
String keepOptionsTogether(String s) => s
|
||||
.replaceAllMapped(RegExp(r'\([-/][^()]*\)'), (m) => unbreakableOption(m[0]!))
|
||||
.replaceAllMapped(RegExp(r'(?<![\w\-\u2060/])(?:--?[A-Za-z][\w-]*(?:=\S*)?|/[A-Z]{2,}\b)'), (m) => unbreakableOption(m[0]!));
|
||||
|
||||
/// 옵션 입력 칸: 붙여 넣거나 친 글의 U+00A0 · U+2060 등을 고쳐 받는다 ([cleanOptionText], 관리자 지시 10/20).
|
||||
/// 화면의 묶은 옵션 글 ([unbreakableOption]) 이나 다른 곳의 글을 붙여 넣어도 rsync 가 다르게 읽지 않게
|
||||
class OptionTextFormatter extends TextInputFormatter {
|
||||
|
|
|
|||
|
|
@ -365,7 +365,7 @@ void main() {
|
|||
expect(find.text('rsync'), findsWidgets);
|
||||
// 222: 원본이 대상 안 - 복사면 "복사됩니다 · 원본 폴더 그대로", 이동이면 "옮겨집니다 · 원본 파일은 지워짐"
|
||||
expect(find.textContaining('바로 아래에 복사됩니다'), findsOneWidget);
|
||||
await tester.tap(find.text('원본 파일 지우기 (--remove-source-files)'));
|
||||
await tester.tap(find.text(keepOptionsTogether('원본 파일 지우기 (--remove-source-files)')));
|
||||
await tester.pump();
|
||||
expect(find.textContaining('바로 아래로 옮겨집니다 (원본 파일은 지워지고'), findsOneWidget);
|
||||
expect(find.textContaining('바로 아래에 복사됩니다'), findsNothing);
|
||||
|
|
|
|||
|
|
@ -94,6 +94,47 @@ void main() {
|
|||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('28 확인 (10/20): 좁은 Rsync 화면의 [오른쪽] 탭 · 넓게 바꾼 두 창도 그 창의 지금 폴더가 보인다 (저장 장치 맨 위에 머물지 않음)', (tester) async {
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_rsync_reveal_');
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
// 맨 위에 폴더가 많고 (C: 처럼) 두 창의 지금 폴더는 깊은 곳 - 펼쳐도 스크롤하지 않으면 화면 밖
|
||||
for (var i = 0; i < 80; i++) {
|
||||
Directory(p.join(tmp.path, 'a${i.toString().padLeft(2, '0')}')).createSync();
|
||||
}
|
||||
final l = p.join(tmp.path, 'zz', 'deep', 'L'), r = p.join(tmp.path, 'zz', 'deep', 'R');
|
||||
Directory(p.join(l, 'sub')).createSync(recursive: true);
|
||||
Directory(p.join(r, 'rsub')).createSync(recursive: true);
|
||||
ExplorerPage.debugVolumes = () => [(tmp.path, 'T:')];
|
||||
addTearDown(() => ExplorerPage.debugVolumes = null);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerOrientation = 'side'
|
||||
..rsyncPaths = [l, r];
|
||||
tester.view.physicalSize = const Size(412, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true))));
|
||||
await settle(tester, () => find.text('sub').hitTestable().evaluate().isNotEmpty);
|
||||
expect(find.text('sub').hitTestable(), findsOneWidget, reason: '왼쪽은 처음부터 지금 폴더');
|
||||
// [오른쪽] 탭을 처음 보면 그 창의 지금 폴더 (R) 로 - 예전에는 맨 위 (a00 …) 에서 시작
|
||||
await tester.tap(find.text('오른쪽 · R'));
|
||||
await settle(tester, () => find.text('rsub').hitTestable().evaluate().isNotEmpty, rounds: 40);
|
||||
expect(find.text('rsub').hitTestable(), findsOneWidget);
|
||||
// 넓게 바꾸면 두 창 - 둘 다 지금 폴더가 보임
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
await settle(tester, () => find.text('sub').hitTestable().evaluate().isNotEmpty && find.text('rsub').hitTestable().evaluate().isNotEmpty,
|
||||
rounds: 40);
|
||||
expect(find.text('sub').hitTestable(), findsOneWidget);
|
||||
expect(find.text('rsub').hitTestable(), findsOneWidget);
|
||||
// 다시 좁게 → [왼쪽] 탭 (지금 보이는 창) 도 지금 폴더
|
||||
tester.view.physicalSize = const Size(412, 900);
|
||||
await settle(tester, () => find.text('왼쪽 · L').evaluate().isNotEmpty);
|
||||
await tester.tap(find.text('왼쪽 · L'));
|
||||
await settle(tester, () => find.text('sub').hitTestable().evaluate().isNotEmpty, rounds: 40);
|
||||
expect(find.text('sub').hitTestable(), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('208: 폰 가로 (낮은 화면) Rsync 화면에서도 방향 실행 버튼 · 모니터링은 늘 보인다 ([더 보기] 로 가는 것은 뒤쪽)', (tester) async {
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_rsync_low_');
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import 'package:jj_mkvmaker/ui/path_label.dart';
|
|||
import 'package:path/path.dart' as p;
|
||||
import 'support/fake_recycle_bin.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart' show recycleBinForTest;
|
||||
import 'package:jj_mkvmaker/ui/sync_preview_view.dart' show keepOptionsTogether;
|
||||
|
||||
/// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게)
|
||||
class _Shell extends NoopShell {
|
||||
|
|
@ -1164,7 +1165,7 @@ void main() {
|
|||
await tester.tap(find.text('취소'));
|
||||
await calm(tester);
|
||||
expect(c.settings.copyTasks, isEmpty);
|
||||
expect(find.text('원본 파일 지우기 (--remove-source-files)'), findsNothing); // 양쪽에는 없음
|
||||
expect(find.text(keepOptionsTogether('원본 파일 지우기 (--remove-source-files)')), findsNothing); // 양쪽에는 없음
|
||||
// → 는 한 방향 · -u 없이 · 원본 파일 지우기를 고를 수 있다 (고르면 원본 폴더 남김 / 지움)
|
||||
await act(tester, () => tester.tap(find.text('좌 → 우')));
|
||||
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 400);
|
||||
|
|
@ -1173,7 +1174,7 @@ void main() {
|
|||
// 옵션 낱말 -u 만 (폴더 이름 vscode-stable-user-x64 같은 것은 아님 - 시험 임시 폴더 위가 %TEMP% 일 때)
|
||||
expect(find.textContaining(RegExp(r'(^|\s)-u(\s|$)')), findsNothing);
|
||||
expect(find.text('빈 폴더 지움 · 원본 폴더는 남김'), findsNothing);
|
||||
await tester.tap(find.text('원본 파일 지우기 (--remove-source-files)'));
|
||||
await tester.tap(find.text(keepOptionsTogether('원본 파일 지우기 (--remove-source-files)')));
|
||||
await calm(tester);
|
||||
expect(find.textContaining('-avPog --remove-source-files'), findsOneWidget);
|
||||
expect(find.text('빈 폴더 지움 · 원본 폴더는 남김'), findsOneWidget);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import 'package:jj_mkvmaker/ui/monitor_page.dart';
|
|||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/rsync_scenes.dart';
|
||||
import 'package:jj_mkvmaker/ui/sync_preview_view.dart' show keepOptionsTogether;
|
||||
|
||||
/// 관리자 지시 (10/20): 실시간 동기화 (로컬 rsync 쌍) 를 28 의 장면 · 옵션으로 - 결과 트리 = 실제 rsync,
|
||||
/// 지울지 묻는 목록 = rsync 가 지우는 것, 맞춘 뒤 "맞출 것" 이 남지 않음 (거른 것을 기다리는 것으로 세지 않음)
|
||||
|
|
@ -106,9 +107,9 @@ void main() {
|
|||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.text('대상에서 3개를 지울까요?'), findsOneWidget);
|
||||
expect(find.text('거른 것 2개도 지워집니다 (--delete-excluded)'), findsOneWidget);
|
||||
expect(find.text(keepOptionsTogether('거른 것 2개도 지워집니다 (--delete-excluded)')), findsOneWidget);
|
||||
// 순서: 그냥 지울 것 → 묶음 제목 → 거른 것
|
||||
final y = {for (final n in ['only.txt', '거른 것 2개도 지워집니다 (--delete-excluded)', 'skip.tmp', 'cache']) n: tester.getTopLeft(find.text(n)).dy};
|
||||
final y = {for (final n in ['only.txt', '거른 것 2개도 지워집니다 (--delete-excluded)', 'skip.tmp', 'cache']) n: tester.getTopLeft(find.text(keepOptionsTogether(n))).dy};
|
||||
expect(y['only.txt']! < y['거른 것 2개도 지워집니다 (--delete-excluded)']!, isTrue);
|
||||
expect(y['거른 것 2개도 지워집니다 (--delete-excluded)']! < y['skip.tmp']!, isTrue);
|
||||
expect(y['거른 것 2개도 지워집니다 (--delete-excluded)']! < y['cache']!, isTrue);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'package:path/path.dart' as p;
|
|||
|
||||
import 'explorer_test.dart' show act, settle;
|
||||
import 'support/dav_server.dart';
|
||||
import 'package:jj_mkvmaker/ui/sync_preview_view.dart' show keepOptionsTogether;
|
||||
|
||||
/// 95 · 96 · 97 · 100: Rsync 화면의 지우기 - 확인 창 · 미리 보기 · 실행이 같고, ⇄ 는 차례로 (← 는 지우지 않음)
|
||||
void main() {
|
||||
|
|
@ -78,7 +79,7 @@ void main() {
|
|||
expect(find.textContaining('-avPog -u --delete'), findsNothing);
|
||||
expect(find.textContaining('rsync -avPog -u'), findsWidgets);
|
||||
expect(runButton(tester).onPressed, isNotNull);
|
||||
expect(find.text('받는 쪽에서 1개가 지워집니다 (--delete, 되돌릴 수 없음)'), findsOneWidget);
|
||||
expect(find.text(keepOptionsTogether('받는 쪽에서 1개가 지워집니다 (--delete, 되돌릴 수 없음)')), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '실행')));
|
||||
await settle(tester, () => find.byType(LinearProgressIndicator).evaluate().isEmpty && find.textContaining('rsync 끝').evaluate().isNotEmpty,
|
||||
rounds: 400);
|
||||
|
|
@ -101,12 +102,12 @@ void main() {
|
|||
expect(find.textContaining('rsync -avPog --delete'), findsNothing);
|
||||
expect(find.textContaining('− b.txt'), findsNothing);
|
||||
// 원본 파일 지우기 (이동 작업) → 그 작업의 옵션 (--delete) 으로 다시 보이고 다시 비교한다
|
||||
await tester.tap(find.text('원본 파일 지우기 (--remove-source-files)'));
|
||||
await tester.tap(find.text(keepOptionsTogether('원본 파일 지우기 (--remove-source-files)')));
|
||||
await tester.pump();
|
||||
expect(find.textContaining('rsync -avPog --delete --remove-source-files'), findsOneWidget);
|
||||
await waitCompare(tester);
|
||||
expect(find.textContaining('− b.txt'), findsOneWidget);
|
||||
expect(find.text('받는 쪽에서 1개가 지워집니다 (--delete, 되돌릴 수 없음)'), findsOneWidget);
|
||||
expect(find.text(keepOptionsTogether('받는 쪽에서 1개가 지워집니다 (--delete, 되돌릴 수 없음)')), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, '이동'), findsOneWidget);
|
||||
await tester.tap(find.text('취소'));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -118,7 +119,7 @@ void main() {
|
|||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('좌 → 우')));
|
||||
await waitCompare(tester);
|
||||
expect(find.text('받는 쪽에서 1개가 지워집니다 (--delete, 되돌릴 수 없음)'), findsOneWidget); // --del 도 지우기로 알아본다
|
||||
expect(find.text(keepOptionsTogether('받는 쪽에서 1개가 지워집니다 (--delete, 되돌릴 수 없음)')), findsOneWidget); // --del 도 지우기로 알아본다
|
||||
await tester.tap(find.byTooltip('다시 비교'));
|
||||
await tester.pump();
|
||||
expect(runButton(tester).onPressed, isNull);
|
||||
|
|
|
|||
|
|
@ -50,4 +50,16 @@ void main() {
|
|||
expect(find.textContaining('(더 새 파일을 덮어씀)'), findsOneWidget);
|
||||
expect(find.textContaining('(받는 쪽이 더 새것 - 건너뜀)'), findsOneWidget);
|
||||
});
|
||||
|
||||
test('28 확인 (10/20): keepOptionsTogether - 글 안의 옵션 덩어리만 묶고 (괄호째 · 따로 쓴 것) 나머지는 그대로', () {
|
||||
const wj = '\u2060', nb = '\u00A0';
|
||||
expect(keepOptionsTogether('원본 파일 지우기 (--remove-source-files)'), '원본 파일 지우기 (-$wj-${wj}remove-${wj}source-${wj}files)');
|
||||
expect(keepOptionsTogether('Delete source files (--remove-source-files)'), 'Delete source files (-$wj-${wj}remove-${wj}source-${wj}files)',
|
||||
reason: '앞 글의 빈칸은 그대로 (거기서는 줄을 바꿀 수 있음)');
|
||||
expect(keepOptionsTogether('지워집니다 (--delete, 되돌릴 수 없음)'), '지워집니다 (-$wj-${wj}delete,$nb되돌릴$nb수$nb없음)');
|
||||
expect(keepOptionsTogether('rsync --delete · robocopy /PURGE. 끄면'), 'rsync -$wj-${wj}delete · robocopy /${wj}PURGE. 끄면');
|
||||
expect(keepOptionsTogether('(rsync 의 -u, robocopy 의 /XO 와 같음)'), '(rsync 의 -${wj}u, robocopy 의 /${wj}XO 와 같음)');
|
||||
// 낱말 가운데의 - · / 는 그대로 (real-time · a/b)
|
||||
expect(keepOptionsTogether('real-time 동기화 · a/b · WebDAV'), 'real-time 동기화 · a/b · WebDAV');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import 'package:jj_mkvmaker/ui/monitor_page.dart';
|
|||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'explorer_test.dart' show act, calm, settle;
|
||||
import 'package:jj_mkvmaker/ui/sync_preview_view.dart' show keepOptionsTogether;
|
||||
|
||||
/// 68 · 70 · 71
|
||||
void main() {
|
||||
|
|
@ -204,7 +205,7 @@ void main() {
|
|||
expect(tester.widget<FilledButton>(find.widgetWithText(FilledButton, '실행')).onPressed, isNull);
|
||||
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 400);
|
||||
// 끝나면: 지울 목록 (빨강) · [실행 (1개 지움)] 켜짐
|
||||
expect(find.text('받는 쪽에서 1개가 지워집니다 (--delete, 되돌릴 수 없음)'), findsOneWidget);
|
||||
expect(find.text(keepOptionsTogether('받는 쪽에서 1개가 지워집니다 (--delete, 되돌릴 수 없음)')), findsOneWidget);
|
||||
expect(find.textContaining('− only_r.txt'), findsOneWidget);
|
||||
expect(tester.widget<FilledButton>(find.widgetWithText(FilledButton, '실행')).onPressed, isNotNull);
|
||||
await tester.tap(find.text('취소'));
|
||||
|
|
|
|||
Loading…
Reference in New Issue