This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
(entry) => entry.canonical === map?.toLowerCase()
|
||||
)?.friendly ??
|
||||
map ??
|
||||
'—'
|
||||
const matches = [...T5_ZOMBIE_MAPS, ...T6_ZOMBIE_MAPS].filter(
|
||||
(entry) => entry.canonical === map?.toLowerCase()
|
||||
);
|
||||
// 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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user