Skip to content
Merged

Next #314

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (C) 2026 The MegaMek Team
// SPDX-License-Identifier: GPL-3.0-or-later
// Author: Drake

import { TestBed } from '@angular/core/testing';
import { Equipment } from '../../models/equipment.model';
import { CBTGameRulesService } from '../../services/cbt-game-rules.service';
import { DataService } from '../../services/data.service';
import { createEmptyUnit } from '../../testing/unit-test-helpers';
import { FloatingCompInfoComponent } from './floating-comp-info.component';

describe('FloatingCompInfoComponent', () => {
it('renders structured equipment rules references', () => {
const equipment = new Equipment({
id: 'test-equipment',
name: 'Test Equipment',
type: 'misc',
rulesRefs: [
{ book: 'TO:AUE', page: 181 },
{ book: 'TM', page: null },
{ book: 'BMM' },
],
});
TestBed.configureTestingModule({
imports: [FloatingCompInfoComponent],
providers: [
{ provide: DataService, useValue: { findEquipment: () => equipment } },
{
provide: CBTGameRulesService,
useValue: { gameRules: () => ({ resolveToHit: () => ({ profile: [0] }) }) },
},
],
});
const fixture = TestBed.createComponent(FloatingCompInfoComponent);
fixture.componentRef.setInput('unit', createEmptyUnit());
fixture.componentRef.setInput('comp', {
id: equipment.id,
q: 1,
n: equipment.name,
t: 'C',
p: 0,
l: 'CT',
});

fixture.detectChanges();

const root = fixture.nativeElement as HTMLElement;
const reference = Array.from(root.querySelectorAll('.equip-item'))
.find(item => item.querySelector('.equip-label')?.textContent?.trim() === 'Reference:');
expect(reference?.querySelector('.equip-value')?.textContent?.trim())
.toBe('TO:AUE, 181; TM; BMM');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Component, input, computed, inject, ChangeDetectionStrategy } from '@an
import type { UnitComponent } from '../../models/units.model';
import { DataService } from '../../services/data.service';
import type { Unit } from '../../models/units.model';
import { AmmoEquipment, type Equipment, WeaponEquipment } from '../../models/equipment.model';
import { AmmoEquipment, type Equipment, formatEquipmentRulesRefs, WeaponEquipment } from '../../models/equipment.model';
import { TechDate, TechAdvancementDates, techDateYear, formatTechDate } from '../../models/entity';
import { getWeaponTypeCSSClass } from '../../utils/equipment.util';
import { CBTGameRulesService } from '../../services/cbt-game-rules.service';
Expand Down Expand Up @@ -237,7 +237,7 @@ export class FloatingCompInfoComponent {
{ label: 'Cost', value: eq.cost },
{ label: 'Tonnage', value: eq.tonnage },
{ label: 'Criticals', value: eq.critSlots },
{ label: 'Reference', value: eq.rulesRefs }
{ label: 'Reference', value: formatEquipmentRulesRefs(eq.rulesRefs) }
]
},
{
Expand All @@ -258,4 +258,4 @@ export class FloatingCompInfoComponent {

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('AdvancementTimelineComponent', () => {
id,
name: id,
type: 'misc',
rulesRefs: 'Test Rules',
rulesRefs: [{ book: 'Test Rules', page: null }],
tech,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function createAmmo(id: string, kgPerShot = 100, ammo: Partial<ConstructorParame
id,
name: id,
type: 'ammo',
rulesRefs: '207, TM',
rulesRefs: [{ book: 'TM', page: 207 }],
tech: {
base: 'Clan',
rating: 'E',
Expand Down Expand Up @@ -264,4 +264,4 @@ describe('SetAmmoDialogComponent', () => {

expect(getAmmoInfoItems(ammo).find(item => item.label === 'Damage')?.value).toBe(40);
});
});
});
30 changes: 30 additions & 0 deletions src/app/models/equipment.model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
EquipmentMap,
findStandardAmmoForWeapon,
findIntrinsicAmmoForWeapon,
formatEquipmentRulesRefs,
isBombEquipment,
MiscEquipment,
resolveWeaponDamage,
Expand All @@ -29,6 +30,35 @@ function catalog(equipment: EquipmentMap = {}): EquipmentRegistry {
}

describe('equipment model', () => {
it('formats structured equipment rules references', () => {
expect(formatEquipmentRulesRefs([
{ book: 'TO:AUE', page: 181 },
{ book: 'TM', page: null },
{ book: 'BMM' },
])).toBe('TO:AUE, 181; TM; BMM');
expect(formatEquipmentRulesRefs([])).toBe('');
});

it('defaults missing equipment rules references to an empty array', () => {
const equipment = createEquipment({ id: 'test', name: 'Test', type: 'misc' });

expect(equipment.rulesRefs).toEqual([]);
});

it('hydrates structured equipment rules references', () => {
const equipment = createEquipment({
id: 'test',
name: 'Test',
type: 'misc',
rulesRefs: [{ book: 'TO:AUE', page: 181 }, { book: 'TM', page: null }],
});

expect(equipment.rulesRefs).toEqual([
{ book: 'TO:AUE', page: 181 },
{ book: 'TM', page: null },
]);
});

it('identifies fixed and variable equipment stats in one place', () => {
const fixed = createEquipment({
id: 'fixed', name: 'Fixed', type: 'misc',
Expand Down
19 changes: 16 additions & 3 deletions src/app/models/equipment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ export type WeaponCategory = 'energy' | 'missile' | 'ballistic' | 'artillery' |

export type WeaponDamageUnit = 'missile' | 'shot' | 'artillery';

/** A rulebook containing rules for an equipment entry, optionally at a specific page. */
export interface EquipmentRulesReference {
readonly book: string;
readonly page?: number | null;
}

/** Formats equipment rule references for display. */
export function formatEquipmentRulesRefs(references: readonly EquipmentRulesReference[]): string {
return references
.map(reference => reference.page == null ? reference.book : `${reference.book}, ${reference.page}`)
.join('; ');
}

/** Resolved damage values, using zero when the source has no intrinsic numeric damage. */
export interface WeaponDamage {
readonly values: readonly number[];
Expand Down Expand Up @@ -306,7 +319,7 @@ export interface EquipmentRawData {
name: string;
shortName?: string;
sortingName?: string;
rulesRefs?: string;
rulesRefs?: EquipmentRulesReference[];
aliases?: string[];
stats?: Partial<EquipmentStats>;
tech?: Partial<WireEquipmentTechData>;
Expand Down Expand Up @@ -432,7 +445,7 @@ export class Equipment {
readonly name: string;
readonly shortName: string;
readonly sortingName: string;
readonly rulesRefs: string;
readonly rulesRefs: EquipmentRulesReference[];
readonly aliases: string[];
protected readonly stats: EquipmentStats;
readonly tech: TechData;
Expand All @@ -446,7 +459,7 @@ export class Equipment {
this.name = data.name;
this.shortName = data.shortName ?? data.name;
this.sortingName = data.sortingName ?? data.name;
this.rulesRefs = data.rulesRefs ?? '';
this.rulesRefs = Array.isArray(data.rulesRefs) ? data.rulesRefs : [];
this.aliases = data.aliases ?? [];
this.type = data.type;
this.modes = data.modes ?? [];
Expand Down
2 changes: 1 addition & 1 deletion src/app/services/unit-search-filters.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ export const SORT_OPTIONS: SortOption[] = [
{ key: 'name', label: 'Name' },
...ADVANCED_FILTERS
.filter(f => f.type !== AdvFilterType.BOOLEAN)
.filter(f => !['era', 'faction', 'availabilityRarity', 'availabilityFrom', 'forcePack', 'componentName', 'weaponType', 'source', '_tags', 'as.specials', 'name', 'chassis', 'model', 'as._motive', 'quirks', 'features'].includes(f.key))
.filter(f => !['era', 'faction', 'availabilityRarity', 'availabilityFrom', 'forcePack', 'componentName', 'weaponType', 'source', 'rulesRefs', '_tags', 'as.specials', 'name', 'chassis', 'model', 'as._motive', 'quirks', 'features'].includes(f.key))
.map(f => ({
key: f.key,
label: f.label,
Expand Down
71 changes: 67 additions & 4 deletions src/app/utils/asprint.util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ interface TestPrintLayout {
}

describe('ASPrintUtil', () => {
afterEach(() => {
window.dispatchEvent(new Event('afterprint'));
document.getElementById('as-multipage-container')?.remove();
document.body.classList.remove('as-multipage-container-active');
});

it('keeps the standard 2 by 4 layout as the default-size preset', () => {
const layout = getPrintLayout('standard');
const styles = getFixedPrintStyles('none', 'standard');
Expand Down Expand Up @@ -131,19 +137,76 @@ describe('ASPrintUtil', () => {
);

expect(renderedHeat).toEqual([0, 0]);
expect(unit.update).toHaveBeenCalledWith(serialized);
expect(heat).toBe(2);
expect(pendingHeat).toBe(1);
expect(unit.disabledSaving).toBeFalse();
expect(unit.update).not.toHaveBeenCalled();
expect(heat).toBe(0);
expect(pendingHeat).toBe(0);
expect(unit.disabledSaving).toBeTrue();

window.dispatchEvent(new Event('afterprint'));

expect(unit.update).toHaveBeenCalledWith(serialized);
expect(unit.update).toHaveBeenCalledTimes(1);
expect(heat).toBe(2);
expect(pendingHeat).toBe(1);
expect(unit.disabledSaving).toBeFalse();
});

it('keeps dynamically rendered card hosts mounted until print cleanup', async () => {
const cardHost = document.createElement('alpha-strike-card');
const hostView = {};
const destroy = jasmine.createSpy('destroy');
const createContainer = (options: { componentRefs: unknown[] }) => {
const overlay = document.createElement('div');
overlay.id = 'as-multipage-container';
const cardCell = document.createElement('div');
cardCell.className = 'as-card-cell';
cardCell.appendChild(cardHost);
overlay.appendChild(cardCell);
options.componentRefs.push({ hostView, destroy });
return overlay;
};
spyOn<any>(ASPrintUtil, 'createFixedPrintContainer').and.callFake(createContainer);
spyOn<any>(ASPrintUtil, 'createFlexPrintContainer').and.callFake(createContainer);

const detachView = jasmine.createSpy('detachView').and.callFake(() => cardHost.remove());
const appRef = {
tick: jasmine.createSpy('tick'),
detachView,
};
const unit = {
disabledSaving: false,
serialize: () => ({ state: 'original' }),
update: jasmine.createSpy('update'),
repairAll: jasmine.createSpy('repairAll'),
getUnit: () => ({ as: { TP: 'BM' } }),
};
const group = { units: () => [unit] };

await ASPrintUtil.multipagePrint(
appRef as never,
{} as never,
{} as never,
[group] as never,
{
clean: true,
ASPrintPageBreakOnGroups: false,
ASPrintCardSize: 'standard',
printMargin: 'none',
},
false,
);

expect(document.querySelector('#as-multipage-container alpha-strike-card')).toBe(cardHost);
expect(detachView).not.toHaveBeenCalled();
expect(destroy).not.toHaveBeenCalled();

window.dispatchEvent(new Event('afterprint'));

expect(detachView).toHaveBeenCalledOnceWith(hostView);
expect(destroy).toHaveBeenCalledTimes(1);
expect(document.getElementById('as-multipage-container')).toBeNull();
});

it('restores unit state and removes the overlay when rendering fails', async () => {
const createContainer = () => {
const overlay = document.createElement('div');
Expand Down
2 changes: 0 additions & 2 deletions src/app/utils/asprint.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,6 @@ export class ASPrintUtil {
triggerPrint,
onMount: () => {
appRef.tick();
detachViews();
restoreUnits();
},
onCleanup: cleanup,
});
Expand Down