Fix variant-aware fleet card map labels (Refs #150)
CI / checks (push) Successful in 5m47s

This commit is contained in:
2026-10-12 19:55:26 -04:00
parent 106d3a5ded
commit bcd3b99ee4
5 changed files with 220 additions and 10 deletions
+3 -1
View File
@@ -66,7 +66,9 @@ export function freshServer(server: FleetServer, now: number): boolean {
);
}
export function fleetIdentity(server: FleetServer) {
export function fleetIdentity(
server: Pick<FleetServer, 'name' | 'game' | 'map' | 'map_slug' | 'game_mode' | 'last_map'>
) {
const parsed = parseServerName(server.name);
const game = server.game ?? parsed?.game ?? '';
const mode = server.game_mode ?? parsed?.gameMode;
+19 -3
View File
@@ -5,7 +5,8 @@
import type { RosterPlayer, Health } from '$ui/stores/servers.js';
import { telemetryByServer, uiNow } from '$ui/stores/servers.js';
import { elapsedSince } from '$ui/duration.js';
import { mapTitle, serverIdentity, serverTitle } from '$ui/server-labels.js';
import { serverIdentity, serverTitle } from '$ui/server-labels.js';
import { fleetIdentity } from '$lib/fleet-overview.js';
import { decodeRconError, rconFixHint } from '$ui/rconError.js';
import Card from './primitives/Card.svelte';
import Badge from './primitives/Badge.svelte';
@@ -20,6 +21,9 @@
name: string;
players?: number | null;
map?: string | null;
game?: string | null;
mapSlug?: string | null;
gameMode?: string | null;
gametype?: string | null;
roster?: RosterPlayer[] | null;
health?: Health;
@@ -45,6 +49,9 @@
name,
players = null,
map = null,
game: serverGame = null,
mapSlug = null,
gameMode = null,
roster = null,
health = 'offline',
lastSeenAt = null,
@@ -76,8 +83,17 @@
const compact = $derived(compactEmpty && health === 'online' && players === 0 && !restarting);
const state = $derived(restarting ? 'restarting' : compact ? 'ready' : health);
const error = $derived(decodeRconError(lastError));
const mapLabel = $derived(mapTitle(map));
const showMap = $derived(mapLabel !== '—' && mapLabel !== title);
const mapLabel = $derived(
fleetIdentity({
name,
game: serverGame,
map,
last_map: null,
map_slug: mapSlug,
game_mode: gameMode
}).map
);
const showMap = $derived(map !== null && mapLabel !== '—' && mapLabel !== title);
const topRoster = $derived(
(roster ?? [])
.slice()
+4 -5
View File
@@ -7,13 +7,12 @@ export function gameTitle(game: string | null | undefined, compact = false): str
}
export function mapTitle(map: string | null | undefined): string {
return (
[...T5_ZOMBIE_MAPS, ...T6_ZOMBIE_MAPS].find(
const matches = [...T5_ZOMBIE_MAPS, ...T6_ZOMBIE_MAPS].filter(
(entry) => entry.canonical === map?.toLowerCase()
)?.friendly ??
map ??
'—'
);
// Engine identifiers alone cannot distinguish selections such as Town and TranZit.
if (matches.length > 1) return `${map} (variant unknown)`;
return matches[0]?.friendly ?? map ?? '—';
}
export function serverTitle(name: string): string {
+3
View File
@@ -552,6 +552,9 @@
telemetry={server.telemetry}
players={server.last_players}
map={server.last_map ?? server.map}
game={server.game}
mapSlug={server.map_slug}
gameMode={server.game_mode}
capacity={currentCapacity(server, observationNow())}
gametype={server.last_gametype}
roster={server.roster}
+190
View File
@@ -0,0 +1,190 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { flushSync, mount, unmount, type ComponentProps } from 'svelte';
import { fromStore, writable } from 'svelte/store';
import ServerCard from '../../src/lib/ui/components/ServerCard.svelte';
import ServerIdentity from '../../src/lib/ui/components/ServerIdentity.svelte';
import PlayerScoreboard from '../../src/lib/ui/components/PlayerScoreboard.svelte';
import { fleetIdentity } from '../../src/lib/fleet-overview.js';
import { gameEvent } from '../helpers/telemetry.js';
afterEach(async () => {
await Promise.all(components.splice(0).map((component) => unmount(component)));
document.body.replaceChildren();
});
vi.mock('$app/stores', () => ({
page: writable({ url: new URL('http://localhost/') })
}));
const components: ReturnType<typeof mount>[] = [];
type MapServer = Parameters<typeof fleetIdentity>[0];
const town: MapServer = {
name: 't6_zombie_town_1',
game: 't6',
map: 'zm_transit',
last_map: 'zm_transit'
};
function show(
initial: MapServer,
options: Partial<ComponentProps<typeof ServerCard>> = {},
fleetHeading = true
) {
const store = writable(initial);
const state = fromStore(store);
const identity = () => fleetIdentity(state.current);
components.push(
mount(ServerCard, {
target: document.body,
props: {
get name() {
return state.current.name;
},
get game() {
return state.current.game;
},
get map() {
return state.current.last_map ?? state.current.map;
},
get mapSlug() {
return state.current.map_slug;
},
get gameMode() {
return state.current.game_mode;
},
get mapLabelOverride() {
return fleetHeading ? identity().map : undefined;
},
health: 'online',
players: 4,
...options
}
}),
mount(ServerIdentity, {
target: document.body,
props: {
get name() {
return state.current.name;
},
get mapLabel() {
return identity().map;
}
}
}),
mount(PlayerScoreboard, {
target: document.body,
props: {
get report() {
return {
...gameEvent(
'heartbeat',
12,
4,
state.current.name,
state.current.last_map ?? state.current.map ?? ''
),
receivedAt: new Date().toISOString()
};
},
get mapLabel() {
return identity().map;
}
}
})
);
flushSync();
return (next: Partial<MapServer>) => {
store.update((server) => ({ ...server, ...next }));
flushSync();
};
}
function expectLabels(label: string) {
expect(document.querySelector('h3')?.textContent).toBe(label);
expect(document.querySelector('.identity .map')?.textContent).toBe(label);
expect(document.querySelector('.match-title h2')?.textContent).toBe(label);
expect(document.querySelector('.map-note')).toBeNull();
}
const variants = [
['town', 'Town Survival'],
['farm', 'Farm Survival'],
['busdepot', 'Bus Depot Survival'],
['transit', 'TranZit']
];
describe('variant-aware rendered map labels', () => {
it.each(variants)('keeps %s consistent across card, list, and detail', (slug, label) => {
const update = show({ ...town, name: `t6_zombie_${slug}_1` });
expectLabels(label);
update({ name: 'Friday lobby', map_slug: slug, game_mode: 'zombie' });
expectLabels(label);
});
it.each([undefined, null, 'unknown'])('does not infer TranZit from variant %s', (map_slug) => {
const server = { ...town, name: 'Custom lobby', map_slug };
show(server);
expectLabels('zm_transit (variant unknown)');
expect(fleetIdentity(server).launch).toBeNull();
});
it('lets unknown stored metadata override a recognized name', () => {
show({ ...town, map_slug: 'unknown' });
expectLabels('zm_transit (variant unknown)');
});
it('updates map and variant metadata reactively without retaining an obsolete launch shortcut', () => {
const update = show(town);
expectLabels('Town Survival');
update({ last_map: 'zm_tomb' });
expectLabels('Origins');
expect(fleetIdentity({ ...town, last_map: 'zm_tomb' }).launch).toBeNull();
update({ last_map: 'zm_transit', map_slug: 'farm' });
expectLabels('Farm Survival');
expect(fleetIdentity({ ...town, map_slug: 'farm' }).launch).toEqual({
game: 't6',
slug: 'farm'
});
update({ map_slug: 'unknown' });
expectLabels('zm_transit (variant unknown)');
update({ last_map: 'zm_buried' });
expectLabels('Buried');
});
it.each(['stale', 'offline'] as const)(
'keeps the selected variant for %s observations',
(health) => {
show(town, { health, lastSeenAt: '2026-01-01T00:00:00Z' });
expectLabels('Town Survival');
}
);
it.each([true, false])('preserves empty-card compact=%s behavior', (compactEmpty) => {
show(town, { players: 0, compactEmpty });
expectLabels('Town Survival');
expect(document.querySelector('article')?.classList.contains('compact')).toBe(compactEmpty);
expect(document.querySelector('article .metrics') === null).toBe(compactEmpty);
});
it.each([
['online', 'Current map'],
['stale', 'Last map'],
['offline', 'Last map']
] as const)('retains useful %s map context with a name-based heading', (health, prefix) => {
const update = show(town, { health }, false);
expect(document.querySelector('.map-note')).toBeNull();
update({ last_map: 'zm_tomb' });
expect(document.querySelector('h3')?.textContent).toBe('Town Survival');
expect(document.querySelector('.map-note')?.textContent?.trim()).toBe(`${prefix}: Origins`);
});
it('hides additional map context in compact cards', () => {
show({ ...town, last_map: 'zm_tomb' }, { players: 0 }, false);
expect(document.querySelector('.map-note')).toBeNull();
});
it('uses stored variant metadata for a custom heading and does not invent a missing observation', () => {
const update = show({ ...town, name: 'Friday lobby', map_slug: 'town' }, {}, false);
expect(document.querySelector('.map-note')?.textContent?.trim()).toBe(
'Current map: Town Survival'
);
update({ map: null, last_map: null });
expect(document.querySelector('.map-note')).toBeNull();
});
});