158 lines
5.3 KiB
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');
|
|
});
|
|
}
|