uto_manager/test/infrastructure/codecs/pdf_chart_exporter_test.dart

301 lines
9.4 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
import 'package:uto_manager/domain/entities/echelon.dart';
import 'package:uto_manager/domain/entities/position_slot.dart';
import 'package:uto_manager/domain/entities/unit_node.dart';
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
import 'package:uto_manager/domain/entities/authorized_rank.dart';
import 'package:uto_manager/infrastructure/codecs/pdf_chart_exporter.dart';
void main() {
const exporter = PdfChartExporter();
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
UnitNode _makeUnit(
String id,
Echelon echelon, {
String? parentId,
String name = '',
}) =>
UnitNode(
unitId: id,
echelon: echelon,
branch: DoctrinalBranch.infantry,
parentUnitId: parentId,
name: name.isEmpty ? 'Unit $id' : name,
isKeyLeader: false,
);
PositionSlot _makeSlot(String id, String unitId) => PositionSlot(
slotId: id,
unitId: unitId,
positionTitle: 'Rifleman',
dutyMos: DutyMos.unvalidated('11B'),
authorizedRank: AuthorizedRank.e3,
isKeyLeader: false,
);
// ---------------------------------------------------------------------------
// Basic output
// ---------------------------------------------------------------------------
group('PdfChartExporter — basic output', () {
test('returns non-empty bytes for empty tree', () {
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: 'Test Unit',
);
expect(bytes, isNotEmpty);
});
test('output starts with PDF magic bytes %PDF-1.4', () {
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: 'Test Unit',
);
final header = String.fromCharCodes(bytes.take(8));
expect(header, startsWith('%PDF-1.4'));
});
test('output ends with %%EOF marker', () {
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: 'Test Unit',
);
final tail = String.fromCharCodes(bytes.skip(bytes.length - 10));
expect(tail, contains('%%EOF'));
});
test('output contains unit designation on cover page', () {
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: '1st MBCT',
);
final content = String.fromCharCodes(bytes);
expect(content, contains('1st MBCT'));
});
test('output contains UNCLASSIFIED classification banner', () {
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: 'Test',
);
final content = String.fromCharCodes(bytes);
expect(content, contains('UNCLASSIFIED'));
});
test('output contains xref table', () {
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: 'Test',
);
final content = String.fromCharCodes(bytes);
expect(content, contains('xref'));
expect(content, contains('trailer'));
expect(content, contains('startxref'));
});
});
// ---------------------------------------------------------------------------
// Tree rendering
// ---------------------------------------------------------------------------
group('PdfChartExporter — tree rendering', () {
test('single root unit renders without error', () {
final units = [_makeUnit('u1', Echelon.platoon, name: '1st Platoon')];
final bytes = exporter.export(
units: units,
slots: const [],
unitDesignation: '1st Platoon',
);
final content = String.fromCharCodes(bytes);
expect(content, contains('1st Platoon'));
});
test('parent-child tree renders connector lines', () {
final units = [
_makeUnit('u1', Echelon.platoon, name: '1st Platoon'),
_makeUnit('u2', Echelon.squad, parentId: 'u1', name: '1st Squad'),
_makeUnit('u3', Echelon.squad, parentId: 'u1', name: '2nd Squad'),
];
final bytes = exporter.export(
units: units,
slots: const [],
unitDesignation: '1st Platoon',
);
// Connector lines use 'm' (moveto) and 'l' (lineto) PDF operators.
final content = String.fromCharCodes(bytes);
expect(content, contains(' m '));
expect(content, contains(' l S'));
});
test('all echelon levels render without error', () {
final echelons = Echelon.values;
final units = echelons
.asMap()
.entries
.map((e) => _makeUnit('u${e.key}', e.value, name: e.value.displayName))
.toList();
expect(
() => exporter.export(
units: units,
slots: const [],
unitDesignation: 'All Echelons',
),
returnsNormally,
);
});
test('unit names appear in PDF content stream', () {
final units = [
_makeUnit('u1', Echelon.company, name: 'Alpha Company'),
_makeUnit('u2', Echelon.platoon, parentId: 'u1', name: '1st Platoon'),
];
final bytes = exporter.export(
units: units,
slots: const [],
unitDesignation: 'Alpha Company',
);
final content = String.fromCharCodes(bytes);
// Names are truncated to 18 chars; both fit.
expect(content, contains('Alpha Company'));
expect(content, contains('1st Platoon'));
});
test('slots are accepted without error', () {
final units = [_makeUnit('u1', Echelon.squad)];
final slots = List.generate(9, (i) => _makeSlot('sl$i', 'u1'));
expect(
() => exporter.export(
units: units,
slots: slots,
unitDesignation: 'Test Squad',
),
returnsNormally,
);
});
});
// ---------------------------------------------------------------------------
// Page sizes
// ---------------------------------------------------------------------------
group('PdfChartExporter — page sizes', () {
for (final size in PdfPageSize.values) {
test('${size.name} page size produces valid PDF', () {
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: 'Test',
pageSize: size,
);
final content = String.fromCharCodes(bytes);
expect(content, startsWith('%PDF-1.4'));
expect(content, contains('%%EOF'));
});
}
test('letter page MediaBox contains 612 and 792', () {
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: 'Test',
pageSize: PdfPageSize.letter,
);
final content = String.fromCharCodes(bytes);
expect(content, contains('612.0'));
expect(content, contains('792.0'));
});
test('tabloid page MediaBox contains 792 and 1224', () {
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: 'Test',
pageSize: PdfPageSize.tabloid,
);
final content = String.fromCharCodes(bytes);
expect(content, contains('792.0'));
expect(content, contains('1224.0'));
});
});
// ---------------------------------------------------------------------------
// Export date
// ---------------------------------------------------------------------------
group('PdfChartExporter — export date', () {
test('custom export date appears in cover page', () {
final date = DateTime(2026, 8, 9);
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: 'Test',
exportDate: date,
);
final content = String.fromCharCodes(bytes);
expect(content, contains('2026-08-09'));
});
test('defaults to current date when exportDate is null', () {
final bytes = exporter.export(
units: const [],
slots: const [],
unitDesignation: 'Test',
);
final year = DateTime.now().year.toString();
final content = String.fromCharCodes(bytes);
expect(content, contains(year));
});
});
// ---------------------------------------------------------------------------
// Large tree
// ---------------------------------------------------------------------------
group('PdfChartExporter — large tree', () {
test('50-node tree exports without error', () {
final units = <UnitNode>[];
units.add(_makeUnit('root', Echelon.company, name: 'Alpha Co'));
for (var i = 0; i < 3; i++) {
units.add(_makeUnit('plt$i', Echelon.platoon, parentId: 'root', name: '${i + 1}st Plt'));
for (var j = 0; j < 3; j++) {
units.add(_makeUnit(
'sq${i}_$j',
Echelon.squad,
parentId: 'plt$i',
name: '${j + 1}st Sq',
));
for (var k = 0; k < 2; k++) {
units.add(_makeUnit(
'tm${i}_${j}_$k',
Echelon.team,
parentId: 'sq${i}_$j',
name: '${k + 1} Team',
));
}
}
}
expect(
() => exporter.export(
units: units,
slots: const [],
unitDesignation: 'Alpha Co',
),
returnsNormally,
);
});
});
}