diff --git a/integration_test/rsync_page_test.dart b/integration_test/rsync_page_test.dart index 2592ad1..e21611a 100644 --- a/integration_test/rsync_page_test.dart +++ b/integration_test/rsync_page_test.dart @@ -11,6 +11,7 @@ import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:jj_mkvmaker/ui/monitor_page.dart'; +import 'package:jj_mkvmaker/ui/sync_preview_view.dart' show keepOptionsTogether; import 'package:jj_mkvmaker/ui/theme.dart'; import 'support/temp_dir.dart'; import 'package:path/path.dart' as p; @@ -227,7 +228,8 @@ void main() { await tap(find.text('srcA').first); await tap(find.text('dstB').last); await tap(find.text('좌 → 우')); - await tap(find.text('원본 파일 지우기 (--remove-source-files)')); + // 옵션 이름은 줄바꿈되지 않게 묶여 그려진다 (rsync_narrow dfed3fa) - 화면에 그려지는 글 그대로 찾는다 + await tap(find.text(keepOptionsTogether('원본 파일 지우기 (--remove-source-files)'))); await tap(find.text('빈 폴더 지움 · 원본 폴더도 지움')); await shot('rsync_move_confirm'); await tap(find.widgetWithText(FilledButton, '이동')); diff --git a/integration_test/shell_test.dart b/integration_test/shell_test.dart index 71f18de..fdd803d 100644 --- a/integration_test/shell_test.dart +++ b/integration_test/shell_test.dart @@ -14,6 +14,10 @@ void main() { final steps = File(p.join(Directory.systemTemp.path, 'jj_shell_steps.txt')); if (steps.existsSync()) steps.deleteSync(); void mark(String s) => steps.writeAsStringSync('$s\n', mode: FileMode.append, flush: true); + // 관리자 지시 (10/20): 끝까지 돌면 단계 기록을 지운다 (앱이 도중에 꺼졌을 때만 남아 어디까지 갔는지 보인다) + tearDownAll(() { + if (steps.existsSync()) steps.deleteSync(); + }); testWidgets('DesktopShell', (tester) async { mark('start'); diff --git a/integration_test/video_adjust_e2e_test.dart b/integration_test/video_adjust_e2e_test.dart index 29219f6..0d91ee4 100644 --- a/integration_test/video_adjust_e2e_test.dart +++ b/integration_test/video_adjust_e2e_test.dart @@ -21,6 +21,7 @@ void main() { testWidgets('화면 · 색 보정: 미리보기와 세로 9:16 MKV', (tester) async { final dir = testTempDir('jj_adj_e2e_'); + appTempInto(dir); // 앱 임시 폴더 (jj_mkvmaker) 도 이 아래로 - 끝에 함께 지움 (관리자 지시 10/20) final video = p.join(dir.path, '가로 영상.mp4'); final ffmpeg = p.join(Directory.current.path, 'third_party', 'ffmpeg', 'windows', 'ffmpeg.exe'); final r = await Process.run(ffmpeg, [ diff --git a/integration_test/windows_b6_input_test.dart b/integration_test/windows_b6_input_test.dart index 1b74e68..a59b761 100644 --- a/integration_test/windows_b6_input_test.dart +++ b/integration_test/windows_b6_input_test.dart @@ -1,7 +1,9 @@ import 'dart:io'; +import 'dart:ui' as ui; import 'package:flutter/gestures.dart' show kBackMouseButton, kForwardMouseButton, kPrimaryButton, PointerDeviceKind; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -24,6 +26,7 @@ void main() { late Directory tmp; late String left, right; late AppController c; + final shotKey = GlobalKey(); setUp(() async { await i18n.apply('ko', save: false); @@ -70,7 +73,11 @@ void main() { t.view.physicalSize = const Size(1600, 900); t.view.devicePixelRatio = 1; addTearDown(t.view.reset); - await t.pumpWidget(MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: ExplorerPage(c: c))); + await t.pumpWidget(MaterialApp( + theme: buildTheme(), + debugShowCheckedModeBanner: false, + builder: (_, child) => RepaintBoundary(key: shotKey, child: child), + home: ExplorerPage(c: c))); await until(t, () => find.text('doc.txt').evaluate().isNotEmpty && find.text('r1').evaluate().isNotEmpty); expect(find.text('doc.txt'), findsOneWidget); } @@ -104,6 +111,61 @@ void main() { int feedback() => find.byIcon(Icons.drive_file_move_outline).evaluate().length; + /// 관리자 지시 (10/20): 실패 때의 화면 캡처 (JJ_SHOT_DIR) 와 그때 열린 창 · 단추 · 글 목록 + Future dump(WidgetTester t, String what) async { + final texts = [for (final e in find.byType(Text).evaluate()) (e.widget as Text).data ?? (e.widget as Text).textSpan?.toPlainText() ?? '']; + final buttons = [ + for (final e in find.byWidgetPredicate((w) => w is ButtonStyleButton || w is SnackBarAction).evaluate()) + '${e.widget.runtimeType}: ${[for (final x in find.descendant(of: find.byWidget(e.widget), matching: find.byType(Text)).evaluate()) (x.widget as Text).data].join(' ')}' + ]; + // ignore: avoid_print + print('== $what 실패 때: 창 ${find.byType(Dialog).evaluate().length} · 스낵바 ${find.byType(SnackBar).evaluate().length}\n단추: $buttons\n글: $texts'); + final dir = Platform.environment['JJ_SHOT_DIR']; + final b = shotKey.currentContext?.findRenderObject(); + if (dir == null || dir.isEmpty || b is! RenderRepaintBoundary) return; + final img = await t.runAsync(() => b.toImage()); + final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); + File(p.join(dir, 'b6_input_fail_${what.hashCode.toRadixString(16)}.png')) + ..createSync(recursive: true) + ..writeAsBytesSync(bytes!.buffer.asUint8List()); + } + + /// 관리자 지시 (10/20): [f] 가 보이는 것만이 아니라 정말 눌리는 자리에 올 때까지 기다린 뒤 누른다. + /// 스낵바는 아래에서 올라오는 동안 (약 0.25초) 글은 이미 있지만 그 자리를 누르면 아무것도 맞지 않는다 - 예전에는 그때 눌러 [되돌리기] 가 빗나갔다. + /// 끝내 눌리지 않으면 캡처 · 목록을 남기고 실패 + Future tapReady(WidgetTester t, Finder f, String what, {int ms = 8000}) async { + final sw = Stopwatch()..start(); + int? seen; + bool within(Object? target, RenderObject box) { + for (RenderObject? x = target is RenderObject ? target : null; x != null; x = x.parent) { + if (identical(x, box)) return true; + } + return false; + } + + for (var i = 0; i < ms ~/ 50; i++) { + final e = f.evaluate(); + if (e.length == 1) { + seen ??= sw.elapsedMilliseconds; + final box = e.single.renderObject; + if (box is RenderBox && box.attached && box.hasSize) { + final at = box.localToGlobal(box.size.center(Offset.zero)); + final hit = HitTestResult(); + t.binding.hitTestInView(hit, at, t.view.viewId); + if (hit.path.any((x) => within(x.target, box))) { + // ignore: avoid_print + print('누를 수 있음: $what - 보인 뒤 ${sw.elapsedMilliseconds - seen}ms'); + await t.tapAt(at); + return; + } + } + } + await t.pump(const Duration(milliseconds: 50)); + } + await dump(t, what); + fail('$what 을 누를 수 없음 (보인 때: ${seen == null ? '안 보임' : '${seen}ms'})'); + } + testWidgets('82: 8px 문턱 · 놓을 곳 강조 · 같은 창은 빨간 테두리 · 묻기 창 [이동] · [되돌리기]', (t) async { await open(t); final doc = t.getCenter(find.text('doc.txt')); @@ -141,17 +203,15 @@ void main() { await until(t, () => find.text('1개 항목을 놓았습니다').evaluate().isNotEmpty); expect(find.text('1개 항목을 놓았습니다'), findsOneWidget); expect(find.widgetWithText(FilledButton, '복사'), findsOneWidget, reason: '처음 고른 단추는 [복사]'); - await t.tap(find.widgetWithText(OutlinedButton, '이동')); + await tapReady(t, find.widgetWithText(OutlinedButton, '이동'), '묻기 창 [이동]'); // 지금의 이동 확인 창 - await until(t, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty); - await t.tap(find.widgetWithText(FilledButton, '이동')); + await tapReady(t, find.widgetWithText(FilledButton, '이동'), '이동 확인 창 [이동]'); await until(t, () => find.text('되돌리기').evaluate().isNotEmpty); expect(File(p.join(right, 'doc.txt')).existsSync(), isTrue); expect(File(p.join(left, 'doc.txt')).existsSync(), isFalse); // 4) [되돌리기] → 원래 자리로 (지금의 이동 확인 창) - await t.tap(find.text('되돌리기')); - await until(t, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty); - await t.tap(find.widgetWithText(FilledButton, '이동')); + await tapReady(t, find.text('되돌리기'), '[되돌리기]'); + await tapReady(t, find.widgetWithText(FilledButton, '이동'), '되돌리기의 이동 확인 창 [이동]'); await until(t, () => File(p.join(left, 'doc.txt')).existsSync() && !File(p.join(right, 'doc.txt')).existsSync()); expect(File(p.join(left, 'doc.txt')).readAsStringSync(), 'hello'); expect(File(p.join(right, 'doc.txt')).existsSync(), isFalse); diff --git a/integration_test/windows_xplore_shot_test.dart b/integration_test/windows_xplore_shot_test.dart index 03b573e..f4d8384 100644 --- a/integration_test/windows_xplore_shot_test.dart +++ b/integration_test/windows_xplore_shot_test.dart @@ -33,6 +33,7 @@ void main() { await t.runAsync(() => i18n.apply(lang, save: false)); addTearDown(() => i18n.apply('ko', save: false)); final tmp = testTempDir('jj_xpshot_'); + appTempInto(tmp); // 앱 임시 폴더 (jj_mkvmaker) 도 이 아래로 - 끝에 함께 지움 (관리자 지시 10/20) final a = Directory(p.join(tmp.path, 'Videos'))..createSync(); final b = Directory(p.join(tmp.path, 'Backup'))..createSync(); for (final n in ['Show S01', 'Movies']) { @@ -74,6 +75,13 @@ void main() { } await settle(20); + // 관리자 지시 (10/20): 목록은 따로 읽혀 늦게 올 수 있다 (회귀에서 en 412 가 1.2초 안에 못 받아 실패) - 정해진 시간이 아니라 그 줄이 올 때까지 (최대 약 10초) + final load = Stopwatch()..start(); + for (var i = 0; i < 160 && find.text('Show S01').evaluate().isEmpty; i++) { + await settle(1); + } + // ignore: avoid_print + print('목록 받음: $lang $w - 1.2초 뒤 ${load.elapsedMilliseconds}ms 더'); // Show S01 펼치기 · 즐겨찾기 펼치기 await t.tap(find.text('Show S01').first); await settle();