import 'dart:async'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:integration_test/integration_test.dart'; import 'package:jj_mkvmaker/core/hwp_source.dart'; import 'package:jj_mkvmaker/core/reader_sources.dart'; import 'package:path/path.dart' as p; import 'package:pdfrx/pdfrx.dart'; import 'support/temp_dir.dart'; /// 쪽마다 그림 한 장인 진짜 PDF 를 만드는 가짜 엔진 (rhwp 대신) - 부른 범위를 적어 둔다 class _FakeEngine implements HwpPdfEngine { final int pages; final String tempDir; final calls = <(int, int)>[]; var failNext = 0; Completer? hold; _FakeEngine(this.pages, this.tempDir); @override Future open(String path, {String? password}) async { if (password == 'bad') throw const HwpPdfException('암호가 틀림'); return _FakeDoc(this); } } class _FakeDoc implements HwpPdfDoc { final _FakeEngine e; var closed = false; _FakeDoc(this.e); @override int get pageCount => e.pages; @override Future exportPdf(int from, int to, String out, {List fontDirs = const []}) async { e.calls.add((from, to)); // 끝나지 않는 변환 (시간 제한 시험): 풀려도 아무것도 쓰지 않는다 (시험이 끝난 뒤 폴더에 쓰지 않게) if (e.hold != null) { await e.hold!.future; throw const HwpPdfException('늦게 끝남'); } if (e.failNext > 0) { e.failNext--; throw const HwpPdfException('렌더 실패'); } final docs = []; for (var i = from; i < to; i++) { final png = p.join(e.tempDir, 'pg$i.png'); // 쪽마다 폭이 달라 (100 + 쪽 번호) 어느 쪽이 그려졌는지 크기로 안다 File(png).writeAsBytesSync(img.encodePng(img.Image(width: 100 + i, height: 140))); docs.add(await imagePageDocument(png, tempDir: e.tempDir, sourceName: 'jj_fake_$i')); } final outDoc = await PdfDocument.createNew(sourceName: 'jj_fake_out_$from'); outDoc.pages = [for (final d in docs) d.pages.first]; File(out).writeAsBytesSync(await outDoc.encodePdf()); await outDoc.dispose(); for (final d in docs) { await d.dispose(); } } @override Future close() async => closed = true; } /// 관리자 지시 (10/20): HWP · HWPX 보기 - 보는 쪽의 묶음만 PDF 로 바꾸고 다음 묶음을 미리 만든다 · 시간 제한 · 실패하면 다시 · 닫기. /// (rhwp 대신 가짜 엔진, PDF 를 그리는 PDFium 이 필요해 Windows 통합 시험) void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('보는 쪽의 묶음 (5쪽) 만 바꾸고 다음 묶음을 미리 · 끝 묶음은 남은 쪽만', (tester) async { final dir = testTempDir('jj_hwpsrc_'); final doc = File(p.join(dir.path, 'a.hwp'))..writeAsStringSync('x'); final engine = _FakeEngine(12, dir.path); final src = await tester.runAsync(() => HwpSource.open(doc.path, engine: engine, tempDir: dir.path, chunk: 5)); expect(src!.length, 12); expect(src.pageName(0), '1 / 12'); final first = await tester.runAsync(() => src.load(0, maxWidth: 200)); expect(first!.image, isNotNull); first.dispose(); await tester.runAsync(() => Future.delayed(const Duration(seconds: 2))); expect(engine.calls, [(0, 5), (5, 10)], reason: '첫 묶음 + 다음 묶음 미리'); // 끝 쪽: 남은 2쪽만 (11번째 = 폭 110) final last = await tester.runAsync(() => src.load(11, maxWidth: 200)); expect(last!.image, isNotNull); last.dispose(); expect(engine.calls.last, (10, 12)); // 이미 만든 묶음은 다시 만들지 않는다 final again = await tester.runAsync(() => src.load(3, maxWidth: 200)); again!.dispose(); expect(engine.calls.where((c) => c == (0, 5)), hasLength(1)); await tester.runAsync(src.dispose); expect(Directory(src.work.path).existsSync(), isFalse, reason: '닫으면 임시 PDF 를 지움'); }); testWidgets('묶음이 실패하면 그 쪽에 이유, 다시 보면 다시 만든다 · 시간 제한 · 열기 실패', (tester) async { final dir = testTempDir('jj_hwpsrc_'); final doc = File(p.join(dir.path, 'b.hwpx'))..writeAsStringSync('x'); final engine = _FakeEngine(3, dir.path)..failNext = 1; final src = await tester.runAsync(() => HwpSource.open(doc.path, engine: engine, tempDir: dir.path, chunk: 5, timeout: const Duration(seconds: 2))); Object? err; await tester.runAsync(() => src!.load(0, maxWidth: 200).then((r) => r.dispose(), onError: (Object e) { err = e; })); expect('$err', contains('렌더 실패')); final ok = await tester.runAsync(() => src!.load(0, maxWidth: 200)); expect(ok!.image, isNotNull, reason: '실패한 묶음은 다시'); ok.dispose(); await tester.runAsync(src!.dispose); // 시간 제한: 끝나지 않는 변환 final slow = _FakeEngine(3, dir.path)..hold = Completer(); final s2 = await tester.runAsync(() => HwpSource.open(doc.path, engine: slow, tempDir: dir.path, timeout: const Duration(seconds: 1))); Object? err2; await tester.runAsync(() => s2!.load(1, maxWidth: 200).then((r) => r.dispose(), onError: (Object e) { err2 = e; })); expect(err2, isA()); expect('$err2', contains('1~3')); slow.hold!.complete(); await tester.runAsync(s2!.dispose); // 열기 실패 (암호 등) 는 그대로 알린다 - 파일 탐색기는 다른 앱으로 연다 Object? err3; await tester.runAsync(() => HwpSource.open(doc.path, engine: engine, tempDir: dir.path, password: 'bad').then((_) {}, onError: (Object e) { err3 = e; })); expect('$err3', contains('암호')); }); }