JJ_MKVMaker/test/batch_ui_test.dart

537 lines
26 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:convert';
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/app/bookmarks_controller.dart';
import 'package:jj_mkvmaker/app/download_manager.dart';
import 'package:jj_mkvmaker/app/settings.dart';
import 'package:jj_mkvmaker/core/models.dart';
import 'package:jj_mkvmaker/core/output_paths.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/downloader.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/app_actions.dart';
import 'package:jj_mkvmaker/ui/browser_page.dart';
import 'package:jj_mkvmaker/ui/downloads_page.dart';
import 'package:jj_mkvmaker/ui/home_page.dart';
import 'package:jj_mkvmaker/ui/settings_page.dart';
import 'package:path/path.dart' as p;
import 'support/temp_dirs.dart';
import 'support/batch_doubles.dart';
void main() {
deleteTempDirsAtEnd();
testWidgets('198: 원어를 자동 감지한 동영상은 세부 정보에 감지한 언어 · [다른 언어로 다시] (알림을 놓쳐도)', (tester) async {
tester.view.physicalSize = const Size(1600, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.videos.add(VideoItem(r'D:\v\a.mp4')..aiDetected = 'ko');
c.selected = c.videos.first;
await tester.pumpWidget(MaterialApp(home: HomePage(c: c)));
expect(find.text('감지한 언어: 한국어 (자동 감지)'), findsOneWidget);
expect(find.widgetWithText(TextButton, '다른 언어로 다시'), findsOneWidget);
// 원어를 직접 고르면 (null) 보이지 않는다
c.videos.first.aiDetected = null;
c.selected = c.videos.first;
await tester.pumpWidget(MaterialApp(key: UniqueKey(), home: HomePage(c: c)));
expect(find.textContaining('감지한 언어'), findsNothing);
});
for (final (name, size, targets) in [
('폰 세로', const Size(412, 900), 0),
('폰 세로 · 이동 버튼 3개', const Size(412, 900), 3),
('넓은 화면 · 이동 버튼 3개', const Size(1600, 520), 3),
]) {
testWidgets('206: MKV 자세히 칸의 [이동] 버튼이 칩 (음성 n개) 을 가리지 않는다 ($name)', (tester) async {
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings.mkv.moveTargets = [for (var i = 0; i < targets; i++) MoveTarget('보관$i', 'D:\\m$i')];
c.videos.add(VideoItem(r'D:\v\a.mp4')
..info = const MediaInfo(duration: Duration(minutes: 3), streams: [
StreamInfo(index: 0, type: 'video', codec: 'h264', width: 1920, height: 1080),
StreamInfo(index: 1, type: 'audio', codec: 'aac'),
]));
c.selected = c.videos.first;
await tester.pumpWidget(MaterialApp(home: HomePage(c: c)));
await tester.pump();
final chip = find.text('음성 1개');
// 끝까지 내려 본다 (넓은 화면은 떠 있는 버튼 - 끝에서도 가리지 않아야)
await tester.dragUntilVisible(chip, find.byType(ListView).last, const Offset(0, -80));
await tester.drag(find.byType(ListView).last, const Offset(0, -2000));
await tester.pumpAndSettle();
expect(chip, findsOneWidget);
final r = tester.getRect(chip);
for (final b in find.byType(FloatingActionButton).evaluate()) {
final br = tester.getRect(find.byWidget(b.widget));
expect(br.overlaps(r), isFalse, reason: '버튼 $br · 칩 $r');
}
});
}
testWidgets('MKV 화면: 체크 → "자막 만들기 (2)" · "MKV 만들기 (2)" 글, 전체 선택', (tester) async {
tester.view.physicalSize = const Size(1600, 900); // 위쪽 버튼 글이 다 보이는 너비
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final c = AppController(PlatformServices(
mediaTool: ProcessMediaTool('x', 'y'),
storage: DesktopStorageService(),
createRecognizer: TestRecognizer.new,
createTranslator: TestTranslator.new,
));
for (final n in ['a', 'b', 'c']) {
c.videos.add(VideoItem('D:\\v\\$n.mp4'));
}
c.selected = c.videos.first;
await tester.pumpWidget(MaterialApp(home: HomePage(c: c)));
expect(find.text('자막 만들기'), findsOneWidget);
expect(find.text('자막 만들기 & MKV 만들기'), findsOneWidget);
expect(find.text('동영상 3개 · 전체 선택'), findsOneWidget);
// 줄의 체크 상자 (0 번째는 전체 선택)
// (줄에 더블클릭 = 재생이 있어 탭은 더블클릭 대기 시간 뒤에 확정된다)
await tester.tap(find.byType(Checkbox).at(1));
await tester.pump(const Duration(milliseconds: 400));
await tester.tap(find.byType(Checkbox).at(3));
await tester.pump(const Duration(milliseconds: 400));
expect(c.checked, {c.videos[0], c.videos[2]});
expect(find.text('선택 2 / 3개'), findsOneWidget);
expect(find.text('자막 만들기 (2)'), findsOneWidget);
expect(find.text('자막 만들기 & MKV 만들기 (2)'), findsOneWidget);
expect(find.text('MKV 만들기 (2)'), findsOneWidget);
expect(find.byTooltip(RegExp('^파일 이름 순으로 정렬')), findsOneWidget);
expect(find.byTooltip(RegExp('^날짜 순으로 정렬')), findsOneWidget);
await tester.tap(find.text('선택 2 / 3개'));
await tester.pump();
expect(c.checked, hasLength(3));
await tester.tap(find.byTooltip('선택한 동영상을 목록에서 제거'));
await tester.pump();
expect(c.videos, isEmpty);
});
testWidgets('다운로드 화면: 다 받은 것을 골라 [동영상 추가] → MKV 목록', (tester) async {
tester.view.physicalSize = const Size(1500, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final dir = Directory.systemTemp.createTempSync('jj_dladd_');
addTearDown(() => dir.deleteSync(recursive: true));
final file = p.join(dir.path, 'movie.mp4');
File(file).writeAsStringSync('x');
final d = DownloadManager(
backends: [TestDownloadBackend()], settings: () => AppSettings()..download.root = dir.path, readClipboard: () async => null);
final added = <String>[];
d.addToEditList = (files) async {
added.addAll(files);
return files.length;
};
final done = d.addPage('https://vimeo.com/1')!
..title = '다 받은 영상'
..files.add(file)
..state = DownloadState.done;
d.addPage('https://vimeo.com/2')!.title = '받는 중인 영상';
await tester.pumpWidget(MaterialApp(home: DownloadsPage(d: d)));
OutlinedButton btn() => tester.widget<OutlinedButton>(
find.ancestor(of: find.text('동영상 추가'), matching: find.byWidgetPredicate((w) => w is OutlinedButton)));
expect(btn().onPressed, isNull); // 고른 것이 없음
await tester.tap(find.text('받는 중인 영상'));
await tester.pump();
expect(btn().onPressed, isNull); // 아직 다 받지 않음
await tester.tap(find.text('다 받은 영상'));
await tester.pump();
expect(d.selectedVideoFiles, [file]);
await tester.tap(find.text('동영상 추가'));
await tester.pump();
expect(added, [file]);
expect(find.textContaining('동영상 1개를 추가했습니다'), findsOneWidget);
expect(done.state, DownloadState.done);
d.dispose();
});
testWidgets('다운로드 화면: "완료 시 자동 동영상 추가" 체크 → 설정 변경, 목록에서 빼기', (tester) async {
tester.view.physicalSize = const Size(1500, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final s = AppSettings()..download.addFinished = false;
final d = DownloadManager(backends: [TestDownloadBackend()], settings: () => s, readClipboard: () async => null);
d.addToEditList = (files) async => files.length;
d.setAutoAdd = (on) async {
s.download.addFinished = on;
d.refresh();
};
final t = d.addPage('https://vimeo.com/1')!..title = '영상';
await tester.pumpWidget(MaterialApp(home: DownloadsPage(d: d)));
Checkbox box() => tester.widget<Checkbox>(
find.descendant(of: find.byTooltip(RegExp('MKV 만들기의 동영상 목록에 자동으로')), matching: find.byType(Checkbox)));
expect(box().value, isFalse);
await tester.tap(find.text('완료 시 자동 동영상 추가'));
await tester.pump();
expect(s.download.addFinished, isTrue);
expect(box().value, isTrue);
// 다 받으면 목록에서 뺀다 (앱이 onFinished 에서 부름)
d.dropFromList(t);
await tester.pump();
expect(d.tasks, isEmpty);
expect(find.text('영상'), findsNothing);
d.dispose();
});
// 246 · P11 확인 (921): 영어 · 일본어는 글이 길어 체크가 아예 안 보였다 - 언어 · 폭마다
for (final lang in ['ko', 'en', 'ja']) {
for (final w in [800.0, 1200.0]) {
testWidgets('246: 다운로드 화면 ($lang, 폭 $w) - 버튼 줄의 글이 잘리거나 "전체 선택" 과 겹치지 않는다', (tester) async {
Map<String, String> dict(String code) =>
Map<String, String>.from(jsonDecode(File('assets/l10n/$code.json').readAsStringSync()) as Map);
setTranslations(lang, lang == 'ko' ? const {} : dict(lang), lang == 'ko' ? const {} : dict('en'));
addTearDown(() => setTranslations('ko', const {}));
tester.view.physicalSize = Size(w, 411);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final s = AppSettings();
final d = DownloadManager(backends: [TestDownloadBackend()], settings: () => s, readClipboard: () async => null);
addTearDown(d.dispose);
d.addToEditList = (files) async => files.length;
d.setAutoAdd = (on) async => s.download.addFinished = on;
d.addPage('https://vimeo.com/1')!.title = '영상';
await tester.pumpWidget(MaterialApp(home: DownloadsPage(d: d)));
await tester.pump();
expect(tester.takeException(), isNull);
final all = tester.getRect(find.text(tr('전체 선택')));
final auto = tester.getRect(find.text(tr('완료 시 자동 동영상 추가')));
expect(auto.overlaps(all), isFalse, reason: '$auto · $all');
expect([auto.left >= 0, auto.right <= w], [true, true], reason: '화면 안에 다 보임 $auto');
for (final t in ['일시정지', '재개', '다운로드 취소', '삭제', '완료 정리']) {
final r = tester.getRect(find.text(tr(t)).first);
expect([r.left >= 0, r.right <= w], [true, true], reason: '$t $r');
}
});
}
}
testWidgets('환경 설정 · 종료 버튼: 모든 화면에서 같은 자리', (tester) async {
tester.view.physicalSize = const Size(1500, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final dir = Directory.systemTemp.createTempSync('jj_pos_');
addTearDown(() => dir.deleteSync(recursive: true));
final c = plainController();
final d = DownloadManager(backends: [TestDownloadBackend()], settings: () => c.settings, readClipboard: () async => null);
final bm = BookmarksController(p.join(dir.path, 'bm.json'));
var exits = 0;
Future<(Offset, Offset, Offset, Offset)> at(Widget page) async {
await tester.pumpWidget(MaterialApp(
key: UniqueKey(),
builder: (context, child) =>
AppScope(controller: c, onExit: () => exits++, downloads: d, bookmarks: bm, child: child!),
home: page,
));
await tester.pump();
return (
tester.getCenter(find.byTooltip('환경 설정')),
tester.getCenter(find.byTooltip('종료')),
// 왼쪽 공통 버튼: JJ (홈) · 다운로드 목록
tester.getCenter(find.byTooltip(RegExp('^홈 화면'))),
tester.getCenter(find.byTooltip(RegExp('^다운로드 목록'))),
);
}
final home = await at(HomePage(c: c, downloads: d, bookmarks: bm, onExit: () => exits++));
final pages = {
'다운로드': await at(DownloadsPage(d: d)),
'환경 설정': await at(SettingsPage(c: c)),
'브라우저': await at(BrowserPage(
c: c, bookmarks: bm, downloads: d, viewBuilder: (h, url) => const ColoredBox(color: Colors.white))),
};
for (final e in pages.entries) {
expect(e.value, home, reason: '${e.key} 화면의 버튼 자리가 MKV 화면과 다릅니다');
}
// 오른쪽 위 구석
expect(home.$2.dx, greaterThan(1500 - 60));
expect(home.$2.dy, 28);
// 왼쪽 위 구석: JJ 아이콘 (홈)
expect(home.$3.dx, lessThan(40));
expect(home.$3.dy, 28);
// 브라우저에서 종료 버튼 → 앱의 종료 동작
await tester.tap(find.byTooltip('종료'));
expect(exits, 1);
d.dispose();
});
testWidgets('MKV 만들기: 체크가 없으면 보고 있는 동영상만, 보고 있는 것도 없으면 "전체 만들기 / 취소" 를 묻는다', (tester) async {
tester.view.physicalSize = const Size(1500, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final c = plainController()..ffmpegVersion = 'test';
c.videos.addAll([VideoItem(r'D:\v\a.mp4'), VideoItem(r'D:\v\b.mp4')]);
c.selected = null;
await tester.pumpWidget(MaterialApp(home: HomePage(c: c)));
await tester.tap(find.text('MKV 만들기'));
await tester.pumpAndSettle();
expect(find.textContaining('선택한 동영상이 없습니다'), findsOneWidget);
expect(find.textContaining('목록 전체 2개'), findsOneWidget);
await tester.tap(find.text('취소'));
await tester.pumpAndSettle();
expect(c.busy, isFalse); // 취소하면 아무것도 하지 않음
// 하나라도 고르면 묻지 않는다 (버튼 글에 개수)
c.toggleChecked(c.videos.last);
await tester.pump();
expect(find.text('MKV 만들기 (1)'), findsOneWidget);
});
testWidgets('왼쪽 공통 버튼: JJ (홈) · MKV 화면으로 · 뒤로 · 다운로드 목록', (tester) async {
tester.view.physicalSize = const Size(1500, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final dir = tempDirUntilEnd('jj_nav_');
final c = plainController();
final d = DownloadManager(backends: [TestDownloadBackend()], settings: () => c.settings, readClipboard: () async => null);
final bm = BookmarksController(p.join(dir.path, 'bm.json'));
await tester.pumpWidget(MaterialApp(
builder: (context, child) => AppScope(controller: c, downloads: d, bookmarks: bm, child: child!),
home: HomePage(c: c, downloads: d, bookmarks: bm),
));
IconButton btn(String tip) =>
tester.widget<IconButton>(find.ancestor(of: find.byTooltip(RegExp('^$tip')), matching: find.byType(IconButton)).first);
// MKV 화면: 뒤로 · MKV 는 "지금 여기" 라 꺼짐, 다운로드 목록 열기
expect(btn('뒤로').onPressed, isNull);
expect(find.byTooltip('MKV 화면 (지금 여기)'), findsOneWidget);
// 지금 화면 버튼 아래 밑줄 하나 (그 버튼 아래에)
final line = find.byKey(const ValueKey('nav-here-underline'));
expect(line, findsOneWidget);
final mkv = tester.getRect(find.byTooltip('MKV 화면 (지금 여기)'));
expect(tester.getCenter(line).dx, closeTo(mkv.center.dx, 2));
await tester.tap(find.byTooltip('다운로드 목록'));
await tester.pumpAndSettle();
expect(find.textContaining('다운로드 ('), findsOneWidget);
expect(find.byTooltip('다운로드 목록 (지금 여기)'), findsOneWidget);
// 다운로드 → 환경 설정 → MKV 화면으로 (한 번에 처음까지)
await tester.tap(find.byTooltip('환경 설정').first);
await tester.pumpAndSettle();
expect(find.text('환경 설정'), findsWidgets);
await tester.tap(find.byTooltip('MKV 화면으로'));
await tester.pumpAndSettle();
expect(find.byTooltip('MKV 화면 (지금 여기)'), findsOneWidget);
// 뒤로: 한 단계만
await tester.tap(find.byTooltip('다운로드 목록'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('뒤로'));
await tester.pumpAndSettle();
expect(find.byTooltip('MKV 화면 (지금 여기)'), findsOneWidget);
// 홈 화면을 웹 브라우저로 정하면 JJ 아이콘이 브라우저를 연다 (MKV 버튼은 그대로 있음)
c.settings.display.startScreen = 'browser';
await tester.tap(find.byTooltip('다운로드 목록'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip(RegExp('^홈 화면 \\(웹 브라우저\\)')));
// 화면 전환 (닫히는 화면 · 새 화면) 이 끝날 때까지. 브라우저는 계속 움직여서 pumpAndSettle 은 쓰지 않는다.
for (var i = 0; i < 6; i++) {
await tester.pump(const Duration(milliseconds: 200));
}
expect(find.byType(BrowserPage), findsOneWidget);
expect(find.byTooltip('MKV 화면으로'), findsOneWidget);
await tester.pumpWidget(const SizedBox());
d.dispose();
});
testWidgets('262: 끝나는 순간 줄의 ■ 자리에 오는 ⟳ 는 지금 묶음이 끝날 때까지 눌리지 않는다 (✕ 도 그 자리로 밀려오지 않음)', (tester) async {
tester.view.physicalSize = const Size(1600, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final tool = EndTool();
final dir = Directory.systemTemp.createTempSync('jj_batch262w_');
addTearDown(() => dir.deleteSync(recursive: true));
final files = [for (final n in ['a', 'b']) (File(p.join(dir.path, '$n.mp4'))..writeAsBytesSync([0])).path];
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
late Future<void> building;
await tester.runAsync(() async {
await c.init();
c.settings.mkv.maxParallelJobs = 1;
await c.addVideos(files);
building = c.buildVideos([...c.videos]);
for (var i = 0; i < 200 && tool.runs.isEmpty; i++) {
await Future<void>.delayed(const Duration(milliseconds: 10));
}
});
final [a, b] = c.videos;
await tester.pumpWidget(MaterialApp(home: HomePage(c: c)));
await tester.pump();
final stopAt = tester.getCenter(find.byTooltip('이 동영상만 취소'));
// a 가 끝나고 b 가 시작
await tester.runAsync(() async {
tool.finish(a);
for (var i = 0; i < 200 && !tool.runs.containsKey(b); i++) {
await Future<void>.delayed(const Duration(milliseconds: 10));
}
});
await tester.pump();
expect(a.status, JobStatus.done);
// ■ 를 누르려던 자리를 누른다
await tester.tapAt(stopAt);
await tester.pump();
expect(a.status, JobStatus.done, reason: '⟳ 로 대기가 되지 않음');
expect(c.videos, contains(a), reason: '✕ 로 빠지지 않음');
await tester.runAsync(() async {
tool.finish(b);
await building;
});
await tester.pump();
expect(c.logs.join('\n'), contains('성공 2 · 취소 0 · 실패 0 / 전체 2'));
// 누른 자리의 풍선 도움말 타이머 정리
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(seconds: 2));
});
testWidgets('240: 여럿이 동시에 돌 때 ■ 는 묻는다 ([그대로 두기] · [모두 취소 (n개)]) · 목록 줄의 ■ 는 그 동영상만', (tester) async {
tester.view.physicalSize = const Size(1600, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final tool = HoldTool();
final dir = Directory.systemTemp.createTempSync('jj_batch240_');
addTearDown(() => dir.deleteSync(recursive: true));
final files = [
for (final n in ['a', 'b']) (File(p.join(dir.path, '$n.mp4'))..writeAsBytesSync([0])).path,
];
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
late Future<void> building;
await tester.runAsync(() async {
await c.init();
c.settings.mkv.maxParallelJobs = 5;
await c.addVideos(files);
building = c.buildVideos([...c.videos]);
for (var i = 0; i < 200 && tool.runs.length < 2; i++) {
await Future<void>.delayed(const Duration(milliseconds: 10));
}
});
final [a, b] = c.videos;
expect([a.status, b.status], [JobStatus.running, JobStatus.running]);
await tester.pumpWidget(MaterialApp(home: HomePage(c: c)));
await tester.pump();
// ■ (위 막대): 둘이 돌고 남은 것은 없음 → 묻는다
await tester.tap(find.byTooltip('작업 취소').first);
await tester.pump(const Duration(milliseconds: 400)); // 도는 동안은 진행 막대가 움직여 pumpAndSettle 이 끝나지 않는다
expect(find.text('모두 취소 (2개)'), findsOneWidget);
await tester.tap(find.text('그대로 두기'));
await tester.pump(const Duration(milliseconds: 400)); // 도는 동안은 진행 막대가 움직여 pumpAndSettle 이 끝나지 않는다
expect(tool.runs.length, 2, reason: '[그대로 두기] 는 아무것도 취소하지 않음');
// 목록 줄의 ■: 그 동영상만
expect(find.byTooltip('이 동영상만 취소'), findsNWidgets(2));
await tester.tap(find.byTooltip('이 동영상만 취소').first);
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
await tester.pump();
expect([a.status, b.status], [JobStatus.cancelled, JobStatus.running]);
// 하나만 남으면 ■ 는 묻지 않고 바로 취소
await tester.tap(find.byTooltip('작업 취소').first);
await tester.runAsync(() => building);
await tester.pump();
expect(b.status, JobStatus.cancelled);
expect(find.text('모두 취소 (2개)'), findsNothing);
});
testWidgets('242: 폰 가로 (노트 10+ - 내비게이션 막대를 뺀 약 800×411) 에서 작업 중이어도 위 막대 버튼이 화면 크기 "100%" 를 덮지 않는다', (tester) async {
final w = double.parse(Platform.environment['JJ_W'] ?? '800');
tester.view.physicalSize = Size(w, 411);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final tool = HoldTool();
final dir = Directory.systemTemp.createTempSync('jj_batch242_');
addTearDown(() => dir.deleteSync(recursive: true));
final file = (File(p.join(dir.path, 'a.mp4'))..writeAsBytesSync([0])).path;
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
late Future<void> building;
await tester.runAsync(() async {
await c.init();
await c.addVideos([file]);
c.selected = c.videos.first;
building = c.buildVideos([...c.videos]);
for (var i = 0; i < 200 && tool.runs.isEmpty; i++) {
await Future<void>.delayed(const Duration(milliseconds: 10));
}
});
expect(c.busy, isTrue);
await tester.pumpWidget(MaterialApp(home: HomePage(c: c)));
// 첫 그림에서 재어 보고 모자라면 다음 그림부터 두 줄 (첫 그림도 넘침 오류 없이 잘라 그림)
expect(tester.takeException(), isNull);
await tester.pump();
await tester.pump();
expect(tester.takeException(), isNull, reason: '두 줄로 바뀐 뒤에는 넘침 없음');
final pct = tester.getRect(find.text('${(c.settings.display.scale * 100).round()}%'));
for (final tip in ['작업 취소', '결과 폴더 열기: ${outputDirFor(file)}']) {
final f = find.byTooltip(tip);
expect(f, findsWidgets, reason: tip);
final r = tester.getRect(f.first);
expect(r.overlaps(pct), isFalse, reason: '$tip 버튼 $r 이 "100%" $pct 를 덮음');
}
c.cancel();
await tester.runAsync(() => building);
await tester.pump();
});
testWidgets('폰 세로 (노트 10+ 약 411×800): 여러 개를 만드는 동안 위 막대 진행은 "n/3 · 37%" 짧은 꼴 · [+ 동영상 추가] 를 가리지 않는다', (tester) async {
tester.view.physicalSize = const Size(411, 800);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final tool = HoldTool();
final dir = Directory.systemTemp.createTempSync('jj_batch_portrait_');
addTearDown(() => dir.deleteSync(recursive: true));
final files = [for (final n in ['a', 'b', 'c']) (File(p.join(dir.path, '$n.mp4'))..writeAsBytesSync([0])).path];
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
late Future<void> building;
await tester.runAsync(() async {
await c.init();
await c.addVideos(files);
c.selected = c.videos.first;
building = c.buildVideos([...c.videos]);
for (var i = 0; i < 200 && tool.runs.isEmpty; i++) {
await Future<void>.delayed(const Duration(milliseconds: 10));
}
});
expect(c.busy, isTrue);
expect(c.batchTotal, 3);
await tester.pumpWidget(MaterialApp(home: HomePage(c: c)));
await tester.pump();
await tester.pump();
// 위 막대를 잰 뒤에 진행률이 생긴다 (예전에는 이때 글이 넓어져 [+] 를 가렸다)
for (final v in c.videos.where((v) => v.status == JobStatus.running)) {
v.progress = 0.37;
}
await c.updateSettings((_) {});
await tester.pump();
expect(tester.takeException(), isNull);
final prog = find.textContaining(RegExp(r'^\d+/3 · 37%$'));
expect(prog, findsOneWidget);
final add = find.byTooltip(Platform.isAndroid ? '동영상 추가' : '동영상 추가 (탐색기에서 끌어다 놓아도 됩니다)');
expect(add, findsWidgets);
final r = tester.getRect(prog);
// [+] 는 옆으로 미는 줄 안에 있어 끝이 잘려 보일 수 있다 - 실제로 보이는 부분 (밀기 줄 안쪽) 이 진행 글과 겹치지 않아야 한다
for (final e in add.evaluate()) {
final a = tester.getRect(find.byWidget(e.widget).first);
final view = tester.getRect(find.ancestor(of: find.byWidget(e.widget).first, matching: find.byType(Scrollable)).first);
final seen = a.intersect(view);
expect(seen.width > 0 && seen.overlaps(r), isFalse, reason: '[+] 의 보이는 부분 $seen 이 진행 글 $r 와 겹침');
}
c.cancel();
await tester.runAsync(() => building);
await tester.pump();
});
}