From 3d3ab5666faee5029ac6279e5bebe648aac2441a Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:08:44 +0900 Subject: [PATCH] =?UTF-8?q?=ED=86=B5=ED=95=A9=20=ED=9A=8C=EA=B7=80=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8:=20=EC=8B=9C=ED=97=98=EC=9D=B4=20=EC=98=9B?= =?UTF-8?q?=20=EA=B8=80=20=C2=B7=20=EC=A0=95=ED=95=B4=EC=A7=84=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=C2=B7=20=EB=B9=97=EB=82=98=EA=B0=84=20=EB=88=84?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=EB=A1=9C=20=EC=8B=A4=ED=8C=A8=ED=95=98?= =?UTF-8?q?=EB=8D=98=20=EA=B2=83=EA=B3=BC=20=EB=82=A8=EA=B8=B4=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=ED=8C=8C=EC=9D=BC=20(=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=20=EC=A7=80=EC=8B=9C=2010/20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rsync_page: rsync_narrow (dfed3fa) 부터 옵션 이름은 줄바꿈되지 않게 묶여 그려져 '원본 파일 지우기 (--remove-source-files)' 를 글 그대로 찾지 못함 → keepOptionsTogether 로 화면 글 그대로 찾기 - windows_b6_input (82 끌어 놓기 → [되돌리기]): 스낵바가 아래에서 올라오는 동안 (글은 이미 있음) 눌러 빗나감 (hit test 에 앱 위젯이 하나도 없음) → 되돌리기가 시작되지 않아 이동 확인 창이 안 나옴. 누르기 전에 정말 눌리는 자리에 올 때까지 기다림 (tapReady) - 실패하면 캡처 (JJ_SHOT_DIR) · 창 · 단추 · 글 목록. 측정: [되돌리기] 는 보인 뒤 1 ~ 97ms 지나야 눌림. 985 (f947935) 의 같은 시험도 3번 중 2번 똑같이 실패 → rsync_narrow 이전부터, 앱 쪽 바뀜 없음 (스낵바 · 되돌리기 코드는 985 와 같음). 사용자는 보이는 단추를 누르므로 앱은 고치지 않음 - windows_xplore_shot: 목록을 정해진 1.2초만 기다려 en 412 가 한 번 실패 → 'Show S01' 줄이 올 때까지 (최대 약 10초) - video_adjust_e2e · windows_xplore_shot: 앱 임시 폴더 jj_mkvmaker 를 시험 폴더로 (appTempInto) - shell: 끝까지 돌면 단계 기록 jj_shell_steps.txt 를 지움 (앱이 도중에 꺼졌을 때만 남음) Co-Authored-By: Claude Opus 5.5 --- integration_test/rsync_page_test.dart | 4 +- integration_test/shell_test.dart | 4 + integration_test/video_adjust_e2e_test.dart | 1 + integration_test/windows_b6_input_test.dart | 74 +++++++++++++++++-- .../windows_xplore_shot_test.dart | 8 ++ 5 files changed, 83 insertions(+), 8 deletions(-) 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();