// 보기 화면 모듈 (lib/ui/reader/) 의 약속 시험 - 실제 구성 요소로 (가짜 원본 없음, 사용자 설정 저장소 쓰지 않음) import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:jj_mkvmaker/core/reader_sources.dart'; import 'package:jj_mkvmaker/ui/reader/reader_bars.dart'; import 'package:jj_mkvmaker/ui/reader/reader_controls.dart'; import 'package:jj_mkvmaker/ui/reader/reader_images.dart'; import 'package:jj_mkvmaker/ui/reader/reader_slots.dart'; import 'package:path/path.dart' as p; String _png(Directory dir, String name, {int w = 40, int h = 60}) { final im = img.Image(width: w, height: h); img.fill(im, color: img.ColorRgb8(200, 10, 10)); return (File(p.join(dir.path, name))..writeAsBytesSync(img.encodePng(im))).path; } void main() { late Directory tmp; setUp(() => tmp = Directory.systemTemp.createTempSync('jj_reader_mod_')); tearDown(() => tmp.deleteSync(recursive: true)); test('reader_slots: 쪽 만들기 · relocateSlot (반쪽 ↔ 통째) · pageAfterRemoval · 이름', () { final split = buildSlots(3, (i) => i == 1); expect(split, [(page: 0, part: -1), (page: 1, part: 0), (page: 1, part: 1), (page: 2, part: -1)]); final whole = buildSlots(3, (_) => false); // 둘째 반쪽을 보다 나누기를 끄면 그 장 통째로, 통째를 보다 켜면 첫 반쪽으로 expect(relocateSlot(whole, (page: 1, part: 1), 2), 1); expect(relocateSlot(split, (page: 2, part: -1), 2), 3); // 장이 없어지면 옛 번호를 범위 안으로 expect(relocateSlot(buildSlots(1, (_) => false), (page: 5, part: -1), 4), 0); // 보던 1 이 없어지면 뒤의 2 (새 번호 1), 뒤가 다 없으면 앞, 모두 없으면 -1 expect(pageAfterRemoval(1, {1}, 3), 1); expect(pageAfterRemoval(2, {1, 2}, 3), 0); expect(pageAfterRemoval(0, {0, 1}, 2), -1); expect([slotLabel((page: 11, part: -1)), slotLabel((page: 11, part: 1))], ['12', '12-2']); expect(slotAt(const [], 3), (page: 0, part: -1)); expect(slotBaseWidth((page: 0, part: 0), 5000), 8192); }); test('reader_slots: splitsPage - 켜기 · 끄기 · 자동 (회전 반영)', () { final dims = {0: const Size(200, 100), 1: const Size(100, 200), 2: const Size(100, 200)}; expect(splitsPage('on', 1, dims, turns: 0, bundle: false), isTrue); expect(splitsPage('off', 0, dims, turns: 0, bundle: true), isFalse); expect(splitsPage('auto', 0, dims, turns: 0, bundle: false), isTrue, reason: '세로 사이의 가로 한 장'); expect(splitsPage('auto', 0, dims, turns: 1, bundle: false), isFalse, reason: '돌리면 세로'); expect(splitsPage('auto', 9, dims, turns: 0, bundle: true), isFalse, reason: '크기를 모르면 나누지 않음'); }); test('reader_controls: tapZone · volumeKeyStep', () { expect([tapZone(90, 100, rtl: false), tapZone(10, 100, rtl: false), tapZone(50, 100, rtl: false)], [1, -1, 0]); expect([tapZone(90, 100, rtl: true), tapZone(10, 100, rtl: true)], [-1, 1]); const down = KeyDownEvent( physicalKey: PhysicalKeyboardKey.audioVolumeDown, logicalKey: LogicalKeyboardKey.audioVolumeDown, timeStamp: Duration.zero); const upRelease = KeyUpEvent( physicalKey: PhysicalKeyboardKey.audioVolumeUp, logicalKey: LogicalKeyboardKey.audioVolumeUp, timeStamp: Duration.zero); const other = KeyDownEvent(physicalKey: PhysicalKeyboardKey.keyA, logicalKey: LogicalKeyboardKey.keyA, timeStamp: Duration.zero); expect(volumeKeyStep(down, enabled: true), 1); expect(volumeKeyStep(upRelease, enabled: true), 0, reason: '뗄 때도 처리 (볼륨 안 바뀜)'); expect(volumeKeyStep(down, enabled: false), isNull); expect(volumeKeyStep(other, enabled: true), isNull); }); testWidgets('reader_controls: 계속 보기 시계는 장이 보인 뒤부터 재고, 끄면 멈춘다', (tester) async { var ticks = 0; final show = ReaderSlideshow(seconds: () => 2, onTick: () => ticks++); addTearDown(show.dispose); expect(show.set(true, '0:0'), isTrue); await tester.pump(const Duration(seconds: 3)); expect(ticks, 0, reason: '아직 보이지 않은 장'); show.markShown('0:0'); await tester.pump(const Duration(seconds: 2)); expect(ticks, 1); show.markShown('0:1'); expect(show.set(false, '0:1'), isTrue); await tester.pump(const Duration(seconds: 3)); expect(ticks, 1); final z = ReaderZoom(); expect([z.finger(1), z.finger(1), z.holdPage], [false, true, true]); z.reset(); }); testWidgets('reader_images: 장마다 한 번 읽기 · forget 뒤 새로 · 그림 크기 (실제 PNG)', (tester) async { final src = ImageFilesSource([_png(tmp, 'a.png', w: 40, h: 60), _png(tmp, 'b.png')], tempDir: tmp.path, title: 't'); final cache = ReaderImageCache(src); final a = cache.full(0, 800); expect(identical(a, cache.full(0, 800)), isTrue); cache.forget(0, 0); final b = cache.full(0, 800); expect(identical(a, b), isFalse); final size = await tester.runAsync(() async => ReaderImageCache.imageSize(await b)); expect(size, const Size(40, 60)); expect(cache.sharpWidthFor(0, 1.2, 800), isNull); expect(cache.sharpWidthFor(0, 2, 800), 1600); await tester.runAsync(() async { await a; cache.dispose(); }); }); testWidgets('reader_bars: fitBarItems (넘치면 ⋮ 자리 빼고) · 갤러리 줄 키 · 누름', (tester) async { final items = [for (var i = 0; i < 5; i++) ReaderBarItem(48, [ReaderAct(const Icon(Icons.add), 't$i', () {})])]; expect(fitBarItems(items, 240), 5); expect(fitBarItems(items, 239), 3, reason: '48 (⋮) + 48 × 3 = 192'); final src = ImageFilesSource([_png(tmp, 'a.png'), _png(tmp, 'b.png'), _png(tmp, 'c.png')], tempDir: tmp.path, title: 't'); final cache = ReaderImageCache(src); final gallery = ScrollController(); addTearDown(gallery.dispose); int? tapped; await tester.pumpWidget(MaterialApp( home: Scaffold( body: ReaderGalleryStrip(controller: gallery, count: 3, current: 1, rtl: false, thumb: cache.thumb, onTap: (i) => tapped = i), ), )); expect(find.byKey(const ValueKey('thumb2')), findsOneWidget); await tester.tap(find.byKey(const ValueKey('thumb2'))); expect(tapped, 2); expect(galleryOffset(1, 128, 1000), 32); await tester.pumpWidget(const SizedBox()); await tester.runAsync(() async => cache.dispose()); }); }