255 lines
13 KiB
Dart
255 lines
13 KiB
Dart
import 'dart:io';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/gestures.dart' show kSecondaryMouseButton;
|
|
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';
|
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
|
import 'package:jj_mkvmaker/app/i18n_controller.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/job_view.dart' show TransferJobView;
|
|
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
|
import 'package:jj_mkvmaker/ui/notice.dart' show NoticeTiming;
|
|
import 'package:jj_mkvmaker/ui/theme.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import '../test/support/fake_recycle_bin.dart';
|
|
import 'support/temp_dir.dart';
|
|
import '../test/support/test_explorer_device.dart';
|
|
|
|
/// 탐색기 사용 점검 (관리자 c5 지시 10/22, 분석용 - 커밋하지 않음): 처음 설정 그대로 사용자처럼 써 보며 단계마다 PNG.
|
|
/// 저장 장치는 시험 폴더 둘 (TestExplorerDevice), 휴지통은 가짜 (사용자 휴지통에 흔적을 남기지 않게), 설정은 메모리에만.
|
|
void main() {
|
|
late TestExplorerDevice device;
|
|
setUp(() => device = TestExplorerDevice());
|
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_use');
|
|
|
|
for (final (w, h) in [(1920, 1040), (960, 600), (412, 900)]) {
|
|
testWidgets('탐색기 사용 ${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('ko', save: false));
|
|
useTestRecycleBin();
|
|
// 캡처 비교가 흔들리지 않게 시험 폴더 이름 · 파일 시각을 고정 (관리자 지시 10/22) - 끝나면 지움
|
|
final tmp = Directory(p.join(Directory.systemTemp.path, 'jj_usechk_fixed_$w'));
|
|
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
|
tmp.createSync();
|
|
addTearDown(() {
|
|
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
|
});
|
|
appTempInto(tmp);
|
|
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', 'Docs']) {
|
|
Directory(p.join(a.path, n)).createSync();
|
|
}
|
|
for (var i = 1; i <= 3; i++) {
|
|
File(p.join(a.path, 'Show S01', 'ep0$i.mkv')).writeAsBytesSync(List.filled(5000 * i, 0));
|
|
}
|
|
File(p.join(a.path, 'Movies', 'film.mp4')).writeAsBytesSync(List.filled(9000, 0));
|
|
File(p.join(a.path, 'notes.txt')).writeAsStringSync('memo');
|
|
File(p.join(a.path, 'report.pdf')).writeAsStringSync('pdf');
|
|
for (final f in tmp.listSync(recursive: true).whereType<File>()) {
|
|
f.setLastModifiedSync(DateTime(2026, 10, 22, 9));
|
|
}
|
|
device.volumes = () => [(a.path, 'C:'), (b.path, 'D:')];
|
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
|
final shotKey = GlobalKey();
|
|
await t.runAsync(() => t.pumpWidget(RepaintBoundary(
|
|
key: shotKey,
|
|
// 알림이 실제 시간으로 닫히지 않게 (정한 자리에서 noticeTimesOut 으로 닫는다)
|
|
child: NoticeTiming(
|
|
duration: const Duration(hours: 1),
|
|
child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: ExplorerPage(device: device, c: c)),
|
|
),
|
|
)));
|
|
Future<void> settle([int n = 12]) async {
|
|
for (var i = 0; i < n; i++) {
|
|
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 60)));
|
|
await t.pump();
|
|
}
|
|
}
|
|
|
|
var no = 0;
|
|
Future<void> shot(String name) async {
|
|
await settle(3);
|
|
final r = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
|
final img = await t.runAsync(() => r.toImage());
|
|
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
|
Directory(out).createSync(recursive: true);
|
|
no++;
|
|
File(p.join(out, 'use_${w}_${no.toString().padLeft(2, '0')}_$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
|
}
|
|
|
|
// 열린 창 (대화 상자 · 메뉴) 의 글을 기록 - 무엇이 떴는지
|
|
void dump(String step) {
|
|
final texts = <String>{};
|
|
for (final f in [find.byType(Dialog), find.byType(PopupMenuItem), find.byType(BottomSheet), find.byType(SnackBar)]) {
|
|
for (final e in find.descendant(of: f, matching: find.byType(Text)).evaluate()) {
|
|
final s = (e.widget as Text).data ?? (e.widget as Text).textSpan?.toPlainText() ?? '';
|
|
if (s.isNotEmpty) texts.add(s);
|
|
}
|
|
}
|
|
// ignore: avoid_print
|
|
print('[$w] $step: ${texts.isEmpty ? '(뜬 창 없음)' : texts.join(' | ')}');
|
|
}
|
|
|
|
// 복사 · 이동이 끝나 진행 패널이 다 내려갈 때까지 (실제 시간, 10초까지) - 캡처가 PC 빠르기에 따라 달라지지 않게
|
|
// 알림은 실제 시간 10초 (앱의 값) 로 닫히면 찍는 때와 겹쳐 장면이 실행마다 달라진다 (14 · 27) -
|
|
// 이 시험은 NoticeTiming 으로 저절로 닫히지 않게 두고, 정한 자리에서 "시간이 다 되어 닫힘" 으로 닫는다 (앱의 알림 시간은 그대로)
|
|
Future<void> noticeTimesOut() async {
|
|
t.state<ScaffoldMessengerState>(find.byType(ScaffoldMessenger).first).removeCurrentSnackBar();
|
|
await settle(3);
|
|
}
|
|
|
|
Future<void> jobsGone() async {
|
|
for (var i = 0; i < 100 && find.byType(TransferJobView).evaluate().isNotEmpty; i++) {
|
|
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
|
|
await t.pump(const Duration(milliseconds: 100));
|
|
}
|
|
await settle();
|
|
}
|
|
|
|
// 한 단계: 실패해도 다음 단계로 (못 찾은 것 자체가 점검 결과)
|
|
Future<void> step(String name, Future<void> Function() act) async {
|
|
try {
|
|
await act();
|
|
await settle();
|
|
dump(name);
|
|
} catch (e) {
|
|
// ignore: avoid_print
|
|
print('[$w] $name: 실패 - ${e.toString().split('\n').first}');
|
|
}
|
|
await shot(name);
|
|
}
|
|
|
|
// 두 창이면 왼쪽 창의 것만 (같은 폴더가 두 창에 보여 오른쪽 것을 누르지 않게)
|
|
Finder leftOf(Finder f) {
|
|
final half = w / 2; // 규칙 7: 폰 세로도 두 창
|
|
final el = f.evaluate().where((e) => t.getCenter(find.byWidget(e.widget).first).dx < half).toList();
|
|
if (el.isEmpty) throw StateError('왼쪽 창에 없음');
|
|
return find.byWidget(el.first.widget).first;
|
|
}
|
|
Finder txt(String name) => leftOf(find.text(name));
|
|
Finder row(String name) => find.ancestor(of: txt(name), matching: find.byType(InkWell)).first;
|
|
Finder tip(String s) => find.byWidgetPredicate((x) => x is Tooltip && (x.message ?? '').startsWith(s)).first;
|
|
// 줄 끝 표시 단추: 이름 글과 같은 높이의 것
|
|
Finder mark(String name) {
|
|
final y = t.getCenter(txt(name)).dy;
|
|
final half = w / 2; // 규칙 7: 폰 세로도 두 창
|
|
final all = find.byWidgetPredicate((x) => x is IconButton && (x.tooltip ?? '').startsWith('표시'));
|
|
final best = all.evaluate().where((e) => t.getCenter(find.byWidget(e.widget)).dx < half).toList()
|
|
..sort((m, n) => (t.getCenter(find.byWidget(m.widget)).dy - y).abs().compareTo((t.getCenter(find.byWidget(n.widget)).dy - y).abs()));
|
|
return find.byWidget(best.first.widget);
|
|
}
|
|
// 대화 상자의 확인 쪽 단추 (마지막 FilledButton)
|
|
Future<void> okDialog() => t.tap(find.descendant(of: find.byType(Dialog), matching: find.byType(FilledButton)).last);
|
|
void files(String when) {
|
|
final left = [
|
|
for (final e in tmp.listSync(recursive: true))
|
|
if (e is File) p.relative(e.path, from: tmp.path)
|
|
]..sort();
|
|
// ignore: avoid_print
|
|
print('[$w] 파일 ($when): ${left.join(', ')}');
|
|
}
|
|
Finder paneBtn(String s) => find.byWidgetPredicate((x) => x is IconButton && (x.tooltip ?? '').startsWith(s)).first;
|
|
|
|
await settle(30);
|
|
await shot('처음');
|
|
dump('처음');
|
|
await step('Show S01 누르기', () => t.tap(txt('Show S01')));
|
|
await step('Show S01 다시 누르기 (접히나)', () => t.tap(txt('Show S01')));
|
|
if (find.text('ep01.mkv').evaluate().isEmpty) await t.tap(txt('Show S01'));
|
|
await settle();
|
|
await step('ep01 한 번 누르기 (선택)', () => t.tap(txt('ep01.mkv')));
|
|
// 규칙 4 (E3): 표시는 선택 모드에서만 - [선택] 을 켜고 표시
|
|
Future<void> selectOn() async {
|
|
if (find.text('선택 끝').evaluate().isEmpty) await t.tap(tip('선택'));
|
|
}
|
|
await step('선택 켜기', selectOn);
|
|
await step('ep02 표시 단추', () => t.tap(mark('ep02.mkv')));
|
|
await step('ep03 표시 단추', () => t.tap(mark('ep03.mkv')));
|
|
await step('복사 단추', () => t.tap(tip('복사')));
|
|
await step('복사 확인 누르기', () async {
|
|
if (find.byType(Dialog).evaluate().isNotEmpty) {
|
|
await okDialog();
|
|
} else {
|
|
await t.tap(txt('D:'));
|
|
await settle();
|
|
await t.tap(tip('붙여넣기'));
|
|
await settle();
|
|
if (find.byType(Dialog).evaluate().isNotEmpty) await okDialog();
|
|
await settle();
|
|
await t.tap(txt('C:'));
|
|
}
|
|
await jobsGone();
|
|
});
|
|
await settle(20);
|
|
files('복사 뒤');
|
|
await step('선택 모드 켜기 (켜져 있으면 그대로)', selectOn);
|
|
await step('선택 모드 - Show S01 누르기', () => t.tap(txt('Show S01')));
|
|
await step('선택 모드 - 안의 ep01 누르기', () => t.tap(txt('ep01.mkv')));
|
|
await step('선택 모드 - Show S01 다시 누르기', () => t.tap(txt('Show S01')));
|
|
await step('선택 단추 (끄기)', () => t.tap(tip('선택')));
|
|
await noticeTimesOut(); // 복사 알림: 선택을 끈 뒤에는 닫혀 있음
|
|
await step('ep03 오른쪽 클릭', () => t.tap(row('ep03.mkv'), buttons: kSecondaryMouseButton));
|
|
await step('메뉴 이름 변경', () => t.tap(find.text('이름 변경').last));
|
|
await step('새 이름 넣고 확인', () async {
|
|
await t.enterText(find.descendant(of: find.byType(Dialog), matching: find.byType(TextField)), 'ep03_new.mkv');
|
|
await settle(2);
|
|
await okDialog();
|
|
});
|
|
files('이름 바꾼 뒤');
|
|
await step('ep01 오른쪽 클릭', () => t.tap(row('ep01.mkv'), buttons: kSecondaryMouseButton));
|
|
await step('메뉴 삭제', () => t.tap(find.text('삭제').last));
|
|
await step('삭제 확인 누르기', okDialog);
|
|
files('지운 뒤');
|
|
await step('Movies 누르기', () => t.tap(txt('Movies')));
|
|
await step('창 뒤로', () => t.tap(paneBtn('뒤로')));
|
|
await step('창 앞으로', () => t.tap(paneBtn('앞으로')));
|
|
await step('선택 켜기 2', selectOn);
|
|
await step('film 표시 단추', () => t.tap(mark('film.mp4')));
|
|
await step('이동 단추', () => t.tap(tip('이동')));
|
|
await noticeTimesOut(); // 휴지통 알림: 이동을 누르기 전에는 닫혀 있음 (이동 창 뒤에는 아직 보임)
|
|
await step('이동 확인 누르기', () async {
|
|
if (find.byType(Dialog).evaluate().isNotEmpty) {
|
|
await okDialog();
|
|
} else {
|
|
await t.tap(txt('D:'));
|
|
await settle();
|
|
await t.tap(tip('붙여넣기'));
|
|
}
|
|
await jobsGone();
|
|
});
|
|
await settle(20);
|
|
files('이동 뒤');
|
|
await step('notes 누르고 Space (TC 식 고르기)', () async {
|
|
await t.tap(txt('notes.txt'));
|
|
await settle(4);
|
|
await t.sendKeyEvent(LogicalKeyboardKey.space);
|
|
});
|
|
await step('Insert', () => t.sendKeyEvent(LogicalKeyboardKey.insert));
|
|
await step('report 누르고 Enter (열기 대신 무엇이)', () async {
|
|
await t.tap(txt('report.pdf'));
|
|
await settle(4);
|
|
});
|
|
// 끝 상태: 실제 파일
|
|
final left = [
|
|
for (final e in tmp.listSync(recursive: true))
|
|
if (e is File) p.relative(e.path, from: tmp.path)
|
|
]..sort();
|
|
// ignore: avoid_print
|
|
print('[$w] 끝 파일: ${left.join(', ')}');
|
|
await t.pumpWidget(const SizedBox());
|
|
await settle(4);
|
|
});
|
|
}
|
|
}
|