JJ_MKVMaker/test/reader_more_test.dart

306 lines
14 KiB
Dart
Raw Permalink 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:async';
import 'dart:ui' as ui;
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/settings.dart';
import 'package:jj_mkvmaker/core/reader_sources.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/reader_page.dart';
/// 크기를 정한 그림들 (가로로 긴 것 = 두 쪽이 붙은 그림)
class _Sized extends ReaderSource {
final List<ui.Image> images;
final widths = <int, List<int>>{};
_Sized(this.images);
@override
String get title => 'comic';
@override
int get length => images.length;
@override
String pageName(int i) => 'p$i.png';
@override
Future<ReaderImage> load(int i, {required int maxWidth}) async {
(widths[i] ??= []).add(maxWidth);
return ReaderImage(image: images[i].clone());
}
}
/// 관리자 지시 10/20: WebDAV 처럼 느린 원본 - [hold] 의 장은 [release] 할 때까지 받는 중
class _Slow extends _Sized {
final int hold;
final release = Completer<void>();
_Slow(super.images, this.hold);
@override
Future<ReaderImage> load(int i, {required int maxWidth}) async {
if (i == hold) await release.future;
return super.load(i, maxWidth: maxWidth);
}
}
/// 223: 폴더의 그림들처럼 (자동 나누기는 다른 그림과 견주어 판단)
class _FolderSized extends ImageFilesSource {
final List<ui.Image> images;
_FolderSized(this.images) : super([for (var i = 0; i < images.length; i++) 'p$i.png'], tempDir: '.', title: 'photos');
@override
Future<ReaderImage> load(int i, {required int maxWidth}) async => ReaderImage(image: images[i].clone());
}
void main() {
late List<bool> screenOn;
setUp(() => screenOn = []);
// 설계 M4 WakeLock: 화면 켜 두기는 PlatformServices.keepScreenOn 으로 넘긴다
AppController controller() => AppController(PlatformServices(
mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), keepScreenOn: (on) async => screenOn.add(on)));
Future<_Sized> source(WidgetTester t, List<(int, int)> sizes) async {
final images = <ui.Image>[];
await t.runAsync(() async {
for (final (w, h) in sizes) {
images.add(await createTestImage(width: w, height: h));
}
});
return _Sized(images);
}
Future<void> open(WidgetTester t, AppController c, ReaderSource src, {double width = 900, int start = 0}) async {
t.view.physicalSize = Size(width, 1200);
t.view.devicePixelRatio = 1;
addTearDown(t.view.reset);
await t.pumpWidget(MaterialApp(home: ReaderPage(c: c, source: src, start: start)));
await t.pumpAndSettle();
}
Future<void> tapAt(WidgetTester t, double x) async {
await t.tapAt(Offset(x, 600));
await t.pumpAndSettle();
}
test('117 · 119 설정: 처음 값 3초 · 자동, 1 ~ 60 초', () {
final s = AppSettings.fromJson({});
expect((s.reader.autoSeconds, s.reader.split), (3, 'auto'));
final b = AppSettings.fromJson((AppSettings()
..reader.autoSeconds = 12
..reader.split = 'on')
.toJson());
expect((b.reader.autoSeconds, b.reader.split), (12, 'on'));
expect(AppSettings.fromJson({'readerAutoSeconds': 500}).reader.autoSeconds, 60);
expect(AppSettings.fromJson({'readerAutoSeconds': 0}).reader.autoSeconds, 1);
expect(AppSettings.fromJson({'readerSplit': 'x'}).reader.split, 'auto');
});
testWidgets('119: 자동이면 가로로 긴 그림만 반씩 (12-1 · 12-2), 표지 (세로) 는 통째 · 반쪽은 두 배 너비로 읽음', (t) async {
final c = controller();
final src = await source(t, [(100, 200), (200, 100), (100, 200)]);
await open(t, c, src);
expect(find.text('1 / 3'), findsOneWidget);
await tapAt(t, 850);
expect(find.text('2-1 / 3장'), findsOneWidget);
expect(src.widths[1], contains(1800), reason: '반쪽이 화면 가득이어도 흐리지 않게');
await tapAt(t, 850);
expect(find.text('2-2 / 3장'), findsOneWidget);
await tapAt(t, 850);
expect(find.text('3 / 3장'), findsOneWidget);
await tapAt(t, 50);
expect(find.text('2-2 / 3장'), findsOneWidget);
// 버튼: 자동 → 켜기 (모든 장) → 끄기 → 자동
await t.tap(find.byTooltip('두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)'));
await t.pumpAndSettle();
expect(c.settings.reader.split, 'on');
expect(find.text('2-2 / 3장'), findsOneWidget, reason: '보던 쪽 그대로');
await tapAt(t, 850);
expect(find.text('3-1 / 3장'), findsOneWidget);
await t.tap(find.byTooltip('두 쪽 나눠 보기: 켜기 (누르면 끄기)'));
await t.pumpAndSettle();
expect(c.settings.reader.split, 'off');
expect(find.text('3 / 3'), findsOneWidget);
await t.tap(find.byTooltip('두 쪽 나눠 보기: 끄기 (누르면 자동)'));
await t.pumpAndSettle();
expect(c.settings.reader.split, 'auto');
});
test('223: 자동 나누기 판단 - 묶음 (ZIP · CBZ · PDF) 의 가로 그림은 나눔, 폴더는 다른 그림 대부분이 세로일 때만', () {
const wide = Size(480, 320), tall = Size(320, 480);
expect(autoSplitsPage(wide, const [], bundle: true), isTrue);
expect(autoSplitsPage(tall, const [], bundle: true), isFalse);
// 사진 폴더: 가로 사진 한 장 · 가로 사진들 사이 → 나누지 않음
expect(autoSplitsPage(wide, const [], bundle: false), isFalse);
expect(autoSplitsPage(wide, const [wide, wide, tall], bundle: false), isFalse);
// 만화 폴더 (낱장 세로들 사이의 펼침 한 장) → 나눔
expect(autoSplitsPage(wide, const [tall, tall, tall, tall], bundle: false), isTrue);
expect(autoSplitsPage(wide, const [tall], bundle: false), isFalse, reason: '비교할 그림이 너무 적으면 나누지 않음');
});
testWidgets('223: 폴더의 평범한 가로 사진 (480×320) 은 자동이어도 반으로 자르지 않는다', (t) async {
final c = controller();
final images = <ui.Image>[];
await t.runAsync(() async => images.add(await createTestImage(width: 480, height: 320)));
await open(t, c, _FolderSized(images));
expect(find.text('1 / 1'), findsOneWidget, reason: '"1-1 / 1장" 처럼 나뉘지 않음');
expect(find.byTooltip('두 쪽 나눔 끄기 (한 장으로 보기)'), findsNothing);
});
testWidgets('223: 나눠 본 장에서는 위 막대의 [두 쪽 나눔 끄기] 로 바로 한 장으로', (t) async {
final c = controller();
final src = await source(t, [(200, 100)]);
await open(t, c, src);
expect(find.text('1-1 / 1장'), findsOneWidget);
await t.tap(find.byTooltip('두 쪽 나눔 끄기 (한 장으로 보기)'));
await t.pumpAndSettle();
expect(c.settings.reader.split, 'off');
expect(find.text('1 / 1'), findsOneWidget);
});
testWidgets('119: 오른쪽 → 왼쪽으로 읽으면 오른쪽 반부터', (t) async {
final c = controller()..settings.reader.rtl = true;
final src = await source(t, [(200, 100)]);
await open(t, c, src);
expect(find.text('1-1 / 1장'), findsOneWidget);
Align half() => t.widget<Align>(find.descendant(of: find.byType(ClipRect), matching: find.byType(Align)).first);
expect(half().alignment, Alignment.centerRight);
await tapAt(t, 50); // 만화: 왼쪽 = 다음
expect(find.text('1-2 / 1장'), findsOneWidget);
expect(half().alignment, Alignment.centerLeft);
});
testWidgets('120: 회전하면 회전한 뒤의 가로 · 세로로 나누기를 다시 판단 · 상하 반전 · 파일은 그대로', (t) async {
final c = controller();
final src = await source(t, [(100, 200), (100, 200)]);
await open(t, c, src);
expect(find.text('1 / 2'), findsOneWidget);
await t.tap(find.byTooltip('90° 회전 (시계 방향)'));
await t.pumpAndSettle();
expect(t.widget<RotatedBox>(find.descendant(of: find.byType(FittedBox), matching: find.byType(RotatedBox)).first)
.quarterTurns, 1);
expect(find.text('1-1 / 2장'), findsOneWidget, reason: '돌리니 가로로 길어져 반씩');
await t.tap(find.byTooltip('반대로 90° 회전'));
await t.pumpAndSettle();
expect(find.text('1 / 2'), findsOneWidget);
expect(find.descendant(of: find.byType(FittedBox), matching: find.byType(RotatedBox)), findsNothing);
await t.tap(find.byTooltip('상하 반전'));
await t.pumpAndSettle();
expect(find.byWidgetPredicate((w) => w is Transform && w.transform.storage[5] == -1), findsWidgets);
// 다음 장에도 같이
await tapAt(t, 850);
expect(find.text('2 / 2'), findsOneWidget);
expect(find.byWidgetPredicate((w) => w is Transform && w.transform.storage[5] == -1), findsWidgets);
});
testWidgets('117: 계속 보기 - 정한 초마다 다음 장, ▲ · ▼ 로 간격, 마지막 장에서 멈춤, 화면 켜 두기', (t) async {
final c = controller();
final src = await source(t, [(100, 200), (100, 200), (100, 200), (100, 200)]);
await open(t, c, src);
await t.tap(find.byTooltip('계속 보기 간격 늘리기 (지금 3초)'));
await t.pumpAndSettle();
expect(c.settings.reader.autoSeconds, 4);
expect(find.text('4초'), findsOneWidget);
await t.tap(find.byTooltip('계속 보기 간격 줄이기 (지금 4초)'));
await t.pumpAndSettle();
await t.tap(find.byTooltip('계속 보기 간격 줄이기 (지금 3초)'));
await t.pumpAndSettle();
expect(c.settings.reader.autoSeconds, 2);
await t.tap(find.byTooltip('계속 보기 (2초마다 다음 장)'));
await t.pump();
expect(screenOn, [true]);
await t.pump(const Duration(milliseconds: 1900));
expect(find.text('1 / 4'), findsOneWidget);
await t.pump(const Duration(milliseconds: 200));
expect(find.text('2 / 4'), findsOneWidget);
// 관리자 지시 10/20: 시계는 그 장이 다 보인 뒤부터 - 넘긴 뒤 한 장면 (FutureBuilder 가 그림을 받아 그림) 이 지나야 잰다
await t.pump();
await t.pump(const Duration(milliseconds: 1900));
expect(find.text('2 / 4'), findsOneWidget);
await t.pump(const Duration(milliseconds: 200));
expect(find.text('3 / 4'), findsOneWidget);
await t.pump();
await t.pump(const Duration(seconds: 2));
expect(find.text('4 / 4'), findsOneWidget);
await t.pump();
await t.pump(const Duration(seconds: 2));
expect(find.byTooltip('계속 보기 (2초마다 다음 장)'), findsOneWidget, reason: '마지막 장에서 멈춤');
expect(screenOn, [true, false]);
await t.pumpAndSettle(const Duration(seconds: 3));
});
testWidgets('117 (관리자 지시 10/20): 느린 원본 - 받는 동안은 넘기지 않고, 다 보인 뒤부터 정한 초를 잰다', (t) async {
final c = controller();
final sized = await source(t, [(100, 200), (100, 200), (100, 200)]);
final src = _Slow(sized.images, 1);
await open(t, c, src);
await t.tap(find.byTooltip('계속 보기 (3초마다 다음 장)'));
await t.pump();
await t.pump(const Duration(seconds: 3));
expect(find.text('2 / 3'), findsOneWidget);
// 2 장은 받는 중 - 한참 지나도 넘기지 않는다
for (var k = 0; k < 10; k++) {
await t.pump(const Duration(seconds: 1));
}
expect(find.text('2 / 3'), findsOneWidget, reason: '받는 동안 시계가 흐르지 않음');
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// 다 받으면 그때부터 3초
src.release.complete();
await t.pump();
await t.pump();
await t.pump(const Duration(milliseconds: 2900));
expect(find.text('2 / 3'), findsOneWidget);
await t.pump(const Duration(milliseconds: 200));
expect(find.text('3 / 3'), findsOneWidget);
await t.tap(find.byTooltip('계속 보기 멈추기'));
await t.pumpAndSettle();
});
testWidgets('117: 화면을 누르면 멈추고 그 누름으로는 넘기지 않음 · 손으로 밀어도 멈춤', (t) async {
final c = controller();
final src = await source(t, [(100, 200), (100, 200), (100, 200), (100, 200)]);
await open(t, c, src);
await t.tap(find.byTooltip('계속 보기 (3초마다 다음 장)'));
await t.pump();
await tapAt(t, 850);
expect(find.text('1 / 4'), findsOneWidget);
expect(find.byTooltip('계속 보기 (3초마다 다음 장)'), findsOneWidget);
expect(screenOn, [true, false]);
await t.pump(const Duration(seconds: 7));
expect(find.text('1 / 4'), findsOneWidget);
// 다음 누름은 다시 넘긴다
await tapAt(t, 850);
expect(find.text('2 / 4'), findsOneWidget);
// 밀어서 멈춤 (민 만큼은 넘어감)
await t.tap(find.byTooltip('계속 보기 (3초마다 다음 장)'));
await t.pump();
await t.dragFrom(const Offset(700, 600), const Offset(-500, 0));
await t.pumpAndSettle();
expect(find.text('3 / 4'), findsOneWidget);
await t.pump(const Duration(seconds: 7));
expect(find.text('3 / 4'), findsOneWidget);
expect(screenOn, [true, false, true, false]);
await t.pumpAndSettle(const Duration(seconds: 3));
});
testWidgets('좁은 화면 (폰 세로): 못 들어간 버튼은 ⋮ 메뉴로 - 없어지지 않음', (t) async {
final c = controller();
final src = await source(t, [(100, 200), (100, 200)]);
await open(t, c, src, width: 400);
expect(find.byTooltip('더 보기'), findsOneWidget);
expect(find.byTooltip('계속 보기 (3초마다 다음 장)'), findsOneWidget, reason: '계속 보기는 막대에 남음');
expect(find.byTooltip('넘기는 방향: 왼쪽 → 오른쪽'), findsNothing);
await t.tap(find.byTooltip('더 보기'));
await t.pumpAndSettle();
for (final s in ['상하 반전', '한 쪽 맞추기', '넘기는 방향: 왼쪽 → 오른쪽', '밝게', '화면 가득 (시스템 막대 숨기기)']) {
expect(find.text(s), findsOneWidget, reason: s);
}
await t.tap(find.text('넘기는 방향: 왼쪽 → 오른쪽'));
await t.pumpAndSettle();
expect(c.settings.reader.rtl, isTrue);
// 넓으면 메뉴 없이 모두
await open(t, c, src, width: 1200);
expect(find.byTooltip('더 보기'), findsNothing);
expect(find.byTooltip('상하 반전'), findsOneWidget);
});
}