MO-Fuel-Tax-Back/test/fuel_report_pdf_test.dart

158 lines
5.3 KiB
Dart

import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:fuel_tax_tracker/models/fuel_entry.dart';
import 'package:fuel_tax_tracker/models/vehicle.dart';
import 'package:fuel_tax_tracker/services/fuel_report.dart';
import 'package:fuel_tax_tracker/services/fuel_report_pdf.dart';
/// PDF content isn't practically assertable byte-for-byte, so this is a
/// smoke test: every include-images/include-line-items combination should
/// render without throwing and produce a well-formed (non-trivial, valid
/// header) document — the real coverage of what each section *contains*
/// lives in fuel_report_test.dart (data) and is exercised by hand.
void main() {
final vehicle = Vehicle(
id: 'v1',
vin: '1FMPU18L1TLB51349',
nickname: 'Truck',
updatedAt: DateTime.utc(2026, 1, 1),
);
final entryA = FuelEntry(
id: 'e1',
vehicleId: 'v1',
date: DateTime(2026, 3, 5),
gallons: 10,
pricePerGallon: 3.5,
totalCost: 35,
updatedAt: DateTime.utc(2026, 3, 5),
receiptImagePath: '/local/a.jpg',
);
final entryB = FuelEntry(
id: 'e2',
vehicleId: 'v1',
date: DateTime(2026, 3, 12),
gallons: 12,
pricePerGallon: 3.6,
totalCost: 43.2,
updatedAt: DateTime.utc(2026, 3, 12),
receiptDriveFileId: 'remote-1',
);
final report = FuelReport(
startDate: DateTime(2026, 3, 1),
endDate: DateTime(2026, 3, 31),
rows: [
VehicleReportRow(
vehicle: vehicle,
entries: [entryA, entryB],
totalGallons: 22,
totalCost: 78.2,
),
],
);
// A 1x1 red pixel PNG — enough for the pdf package to embed as an image.
final onePixelPng = Uint8List.fromList([
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00,
0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, 0xB0, 0x00, 0x00, 0x00,
0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
]);
Future<void> expectValidPdf(Uint8List bytes) async {
expect(bytes.length, greaterThan(100));
expect(String.fromCharCodes(bytes.take(5)), '%PDF-');
}
test('summary only (both toggles off)', () async {
final bytes = await buildFuelReportPdf(report);
await expectValidPdf(bytes);
});
test('with line items, no images', () async {
final bytes = await buildFuelReportPdf(report, includeLineItems: true);
await expectValidPdf(bytes);
});
test('with images, no line items', () async {
final bytes = await buildFuelReportPdf(
report,
includeReceiptImages: true,
receiptImages: {'e1': onePixelPng, 'e2': onePixelPng},
);
await expectValidPdf(bytes);
});
test('with both — line items and images each get their own section, cross-referenced by number',
() async {
final bytes = await buildFuelReportPdf(
report,
includeLineItems: true,
includeReceiptImages: true,
receiptImages: {'e1': onePixelPng, 'e2': onePixelPng},
);
await expectValidPdf(bytes);
});
test('a report with enough entries to span multiple pages does not throw', () async {
// Regression test: the page banner/watermark used to be drawn by
// wrapping the *entire*, unbounded-height body in a Stack, which
// doesn't paginate — MultiPage threw "Widget won't fit into the page"
// for anything long enough to need a page break. Enough months/entries
// here to force at least one break.
final vehicle2 = Vehicle(id: 'v2', vin: '1GNWK5EG4DR302326', updatedAt: DateTime.utc(2026, 1, 1));
final manyEntries = <FuelEntry>[];
for (var month = 1; month <= 12; month++) {
for (var day = 2; day <= 26; day += 6) {
final id = 'm$month-d$day';
manyEntries.add(FuelEntry(
id: id,
vehicleId: month.isEven ? 'v1' : 'v2',
date: DateTime(2026, month, day),
gallons: 20,
pricePerGallon: 3.5,
totalCost: 70,
updatedAt: DateTime.utc(2026, month, day),
receiptDriveFileId: 'remote-$id',
));
}
}
final bigReport = buildFuelReport(
vehicles: [vehicle, vehicle2],
fuelEntries: manyEntries,
startDate: DateTime(2026, 1, 1),
endDate: DateTime(2026, 12, 31),
);
final bytes = await buildFuelReportPdf(
bigReport,
includeLineItems: true,
includeReceiptImages: true,
receiptImages: {for (final e in manyEntries) e.id: onePixelPng},
);
await expectValidPdf(bytes);
});
test('receiptImages missing an entry does not crash — that photo is just skipped', () async {
final bytes = await buildFuelReportPdf(
report,
includeReceiptImages: true,
receiptImages: {'e1': onePixelPng}, // e2 deliberately missing
);
await expectValidPdf(bytes);
});
test('empty report (no rows) with all sections on', () async {
final empty = FuelReport(startDate: DateTime(2026, 1, 1), endDate: DateTime(2026, 1, 31), rows: []);
final bytes = await buildFuelReportPdf(empty, includeLineItems: true, includeReceiptImages: true);
await expectValidPdf(bytes);
});
test('fuelReportFileName formats the date range', () {
expect(fuelReportFileName(report), 'fuel-report-20260301-20260331.pdf');
});
}