fix: close out remaining German player-facing text across web and API tests
Repo-wide grep sweep (apps/web/src, apps/api/src) for leftover German content strings missed by Tasks 1-9, mostly in spec fixtures/assertions that mirror already-translated seed content (monster/item names, POI titles and action labels, location names/descriptions) plus a few real source-file gaps: - inventory-detail-panel.component.ts: STAT_LABELS (Waffenschaden, Angriff, Leben, Rüstung) were never translated by Task 6; now match the identical English labels already used in inventory-page.component.html. - app-shell.component.html: aria-label="Spielinhalt" -> "Game content" (this file was outside every prior task's file list). - location-interaction-panel.component.spec.ts: dead NPC-quote fixture translated to match the real wounded-scout POI text. Code comments referencing German source-spec section titles or not-yet-seeded faction names, and inline calculation-documentation comments, are left as-is per the source spec's scope (dev-facing comments may stay German). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACkMEDYiwtcfchqKkiUJNX
This commit is contained in:
@@ -161,7 +161,7 @@ describe('App', () => {
|
||||
attack: 1,
|
||||
hpRegenPerSecond: 1,
|
||||
hpRegenSince: null,
|
||||
currentLocation: { id: 'location-id', key: 'south-gate', name: 'Südtor von Graufurt' },
|
||||
currentLocation: { id: 'location-id', key: 'south-gate', name: 'Graufurt South Gate' },
|
||||
};
|
||||
const value: CharacterResponse = {
|
||||
id: 'character-id',
|
||||
@@ -173,7 +173,7 @@ describe('App', () => {
|
||||
attack: 12,
|
||||
hpRegenPerSecond: 1,
|
||||
hpRegenSince: null,
|
||||
currentLocation: { id: 'location-id', key: 'south-gate', name: 'Südtor von Graufurt' },
|
||||
currentLocation: { id: 'location-id', key: 'south-gate', name: 'Graufurt South Gate' },
|
||||
};
|
||||
character.set(decoy);
|
||||
displayedCharacter.set(value);
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('GameApiService', () => {
|
||||
const request = http.expectOne('/api/world/current-location/interactions/inspect-tracks');
|
||||
expect(request.request.method).toBe('POST');
|
||||
expect(request.request.body).toEqual({});
|
||||
request.flush({ interactionKey: 'inspect-tracks', title: 'Spuren', text: '…' });
|
||||
request.flush({ interactionKey: 'inspect-tracks', title: 'Suspicious Tracks', text: '…' });
|
||||
});
|
||||
|
||||
it('escapes an interaction key so it cannot break out of its path segment', () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ const runningCombat: Combat = {
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 62, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'road-bandit',
|
||||
name: 'Straßenräuber',
|
||||
name: 'Road Bandit',
|
||||
level: 3,
|
||||
maxHp: 75,
|
||||
currentHp: 30,
|
||||
|
||||
@@ -14,7 +14,7 @@ const activeCombat: Combat = {
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 95, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Aschenratte',
|
||||
name: 'Ash Rat',
|
||||
level: 1,
|
||||
maxHp: 45,
|
||||
currentHp: 31,
|
||||
@@ -28,7 +28,7 @@ const activeCombat: Combat = {
|
||||
rewards: null,
|
||||
};
|
||||
|
||||
const monsterHitLine = 'Aschenratte hits Aric Duskwalker for 5 damage.';
|
||||
const monsterHitLine = 'Ash Rat hits Aric Duskwalker for 5 damage.';
|
||||
|
||||
function countOccurrences(haystack: string | null, needle: string): number {
|
||||
return haystack ? haystack.split(needle).length - 1 : 0;
|
||||
@@ -97,7 +97,7 @@ describe('CombatPageComponent', () => {
|
||||
|
||||
expect(element.textContent).toContain('Aric Duskwalker');
|
||||
expect(element.textContent).toContain('95 / 100');
|
||||
expect(element.textContent).toContain('Aschenratte');
|
||||
expect(element.textContent).toContain('Ash Rat');
|
||||
expect(element.textContent).toContain('31 / 45');
|
||||
expect(element.querySelector('[data-combat-round]')?.textContent).toContain('Round 2');
|
||||
expect(element.querySelector('[data-combat-attack]')).toBeTruthy();
|
||||
@@ -169,7 +169,7 @@ describe('CombatPageComponent', () => {
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.querySelector('[data-combat-telegraph]')?.textContent).toContain(
|
||||
'Aschenratte is winding up a Heavy Strike.',
|
||||
'Ash Rat is winding up a Heavy Strike.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -194,16 +194,16 @@ describe('CombatPageComponent', () => {
|
||||
|
||||
expect(element.textContent).toContain('Aric Duskwalker drinks a potion and heals 35 HP.');
|
||||
expect(element.textContent).toContain('Aric Duskwalker braces to defend.');
|
||||
expect(element.textContent).toContain('Aschenratte is winding up a Heavy Strike.');
|
||||
expect(element.textContent).toContain("Aric Duskwalker interrupts Aschenratte's attack.");
|
||||
expect(element.textContent).toContain('Ash Rat is winding up a Heavy Strike.');
|
||||
expect(element.textContent).toContain("Aric Duskwalker interrupts Ash Rat's attack.");
|
||||
});
|
||||
|
||||
it('renders the structured events as readable English combat-log entries', async () => {
|
||||
const fixture = await setup(activeCombat);
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.textContent).toContain('Aric Duskwalker hits Aschenratte for 14 damage.');
|
||||
expect(element.textContent).toContain('Aschenratte hits Aric Duskwalker for 5 damage.');
|
||||
expect(element.textContent).toContain('Aric Duskwalker hits Ash Rat for 14 damage.');
|
||||
expect(element.textContent).toContain('Ash Rat hits Aric Duskwalker for 5 damage.');
|
||||
});
|
||||
|
||||
it('calls combatStore.performAction("ATTACK") when Attack is clicked', async () => {
|
||||
@@ -334,7 +334,7 @@ describe('CombatPageComponent', () => {
|
||||
await vi.advanceTimersByTimeAsync(540);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.textContent).toContain("Aric Duskwalker interrupts Aschenratte's attack.");
|
||||
expect(element.textContent).toContain("Aric Duskwalker interrupts Ash Rat's attack.");
|
||||
expect(element.querySelector('[data-combat-telegraph]')).toBeNull();
|
||||
expect(monster?.classList.contains('sprite--lunge')).toBe(false);
|
||||
});
|
||||
@@ -557,7 +557,7 @@ describe('CombatPageComponent', () => {
|
||||
it('renders a dropped item with its icon, name, and rarity', async () => {
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
monster: { ...activeCombat.monster, key: 'road-bandit', name: 'Straßenräuber', currentHp: 0 },
|
||||
monster: { ...activeCombat.monster, key: 'road-bandit', name: 'Road Bandit', currentHp: 0 },
|
||||
rewards: {
|
||||
silver: 12,
|
||||
items: [
|
||||
@@ -565,7 +565,7 @@ describe('CombatPageComponent', () => {
|
||||
characterItemId: 'character-item-1',
|
||||
item: {
|
||||
key: 'bandit-blade',
|
||||
name: 'Räuberklinge',
|
||||
name: 'Bandit Blade',
|
||||
rarity: 'COMMON',
|
||||
iconPath: '/images/items/bandit-blade.png',
|
||||
},
|
||||
@@ -579,7 +579,7 @@ describe('CombatPageComponent', () => {
|
||||
expect(element.querySelector<HTMLImageElement>('[data-item-icon]')?.getAttribute('src')).toBe(
|
||||
'/images/items/bandit-blade.png',
|
||||
);
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain('Räuberklinge');
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain('Bandit Blade');
|
||||
expect(element.querySelector('[data-item-rarity]')?.textContent).toContain('Common');
|
||||
expect(element.querySelector('[data-reward-empty]')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ const startedCombat: Combat = {
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Aschenratte',
|
||||
name: 'Ash Rat',
|
||||
level: 1,
|
||||
maxHp: 45,
|
||||
currentHp: 45,
|
||||
|
||||
@@ -7,7 +7,7 @@ const dawnwolfEncounter: HuntEncounter = {
|
||||
id: 'encounter-id-9f2c',
|
||||
monster: {
|
||||
key: 'dawnwolf',
|
||||
name: 'Dämmerwolf',
|
||||
name: 'Dawnwolf',
|
||||
level: 3,
|
||||
artworkPath: '/images/enemies/Dawnwolf.png',
|
||||
},
|
||||
@@ -19,7 +19,7 @@ const ashRatEncounter: HuntEncounter = {
|
||||
id: 'encounter-id-4b71',
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Aschenratte',
|
||||
name: 'Ash Rat',
|
||||
level: 1,
|
||||
artworkPath: '/images/monsters/ash-rat.png',
|
||||
},
|
||||
@@ -45,11 +45,11 @@ describe('EncounterCardComponent', () => {
|
||||
const element = render(dawnwolfEncounter);
|
||||
const artwork = element.querySelector('.encounter-card__artwork');
|
||||
|
||||
expect(element.textContent).toContain('Dämmerwolf');
|
||||
expect(element.textContent).toContain('Dawnwolf');
|
||||
expect(element.textContent).toContain('Level 3');
|
||||
expect(element.textContent).toContain('Suitable');
|
||||
expect(artwork?.getAttribute('src')).toBe('/images/enemies/Dawnwolf.png');
|
||||
expect(artwork?.getAttribute('alt')).toBe('Dämmerwolf');
|
||||
expect(artwork?.getAttribute('alt')).toBe('Dawnwolf');
|
||||
});
|
||||
|
||||
it('shows the background-free cut-out instead of the framed artwork when one exists', () => {
|
||||
|
||||
@@ -19,11 +19,11 @@ const burnedRoad = burnedRoadFixture({ connections: [] });
|
||||
|
||||
const threeEncounterHunt: HuntResult = {
|
||||
id: 'hunt-id',
|
||||
location: { id: 'burned-road-id', key: 'burned-road', name: 'Verbrannte Straße' },
|
||||
location: { id: 'burned-road-id', key: 'burned-road', name: 'Burned Road' },
|
||||
encounters: [
|
||||
{
|
||||
id: 'encounter-1',
|
||||
monster: { key: 'ash-rat', name: 'Aschenratte', level: 1, artworkPath: '/images/enemies/AshRat.png' },
|
||||
monster: { key: 'ash-rat', name: 'Ash Rat', level: 1, artworkPath: '/images/enemies/AshRat.png' },
|
||||
dangerRating: 'WEAK',
|
||||
status: 'AVAILABLE',
|
||||
},
|
||||
@@ -31,7 +31,7 @@ const threeEncounterHunt: HuntResult = {
|
||||
id: 'encounter-2',
|
||||
monster: {
|
||||
key: 'road-bandit',
|
||||
name: 'Straßenräuber',
|
||||
name: 'Road Bandit',
|
||||
level: 3,
|
||||
artworkPath: '/images/enemies/RoadBandit.png',
|
||||
},
|
||||
@@ -40,7 +40,7 @@ const threeEncounterHunt: HuntResult = {
|
||||
},
|
||||
{
|
||||
id: 'encounter-3',
|
||||
monster: { key: 'ash-rat', name: 'Aschenratte', level: 1, artworkPath: '/images/enemies/AshRat.png' },
|
||||
monster: { key: 'ash-rat', name: 'Ash Rat', level: 1, artworkPath: '/images/enemies/AshRat.png' },
|
||||
dangerRating: 'WEAK',
|
||||
status: 'AVAILABLE',
|
||||
},
|
||||
@@ -54,7 +54,7 @@ const startedCombat: Combat = {
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'road-bandit',
|
||||
name: 'Straßenräuber',
|
||||
name: 'Road Bandit',
|
||||
level: 3,
|
||||
maxHp: 75,
|
||||
currentHp: 75,
|
||||
@@ -130,7 +130,7 @@ describe('HuntPageComponent', () => {
|
||||
return fixture;
|
||||
}
|
||||
|
||||
it('shows the hunting-unavailable state at the Südtor, with no Begin Hunt button, and a way back to the location', async () => {
|
||||
it('shows the hunting-unavailable state at the South Gate, with no Begin Hunt button, and a way back to the location', async () => {
|
||||
const fixture = await setup(southGate);
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
@@ -164,7 +164,7 @@ describe('HuntPageComponent', () => {
|
||||
|
||||
const cards = element.querySelectorAll('app-encounter-card');
|
||||
expect(cards.length).toBe(3);
|
||||
expect(element.textContent).toMatch(/Aschenratte[\s\S]*Straßenräuber[\s\S]*Aschenratte/);
|
||||
expect(element.textContent).toMatch(/Ash Rat[\s\S]*Road Bandit[\s\S]*Ash Rat/);
|
||||
expect(
|
||||
element.querySelectorAll(
|
||||
'.encounter-card__artwork[src="/images/combat/sprites/ash-rat-760.png"]',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { HuntingStore } from './hunting.store';
|
||||
|
||||
const huntResult: HuntResult = {
|
||||
id: 'hunt-id',
|
||||
location: { id: 'origin-id', key: 'south-gate', name: 'Südtor' },
|
||||
location: { id: 'origin-id', key: 'south-gate', name: 'South Gate' },
|
||||
encounters: [
|
||||
{
|
||||
id: 'encounter-1',
|
||||
@@ -18,7 +18,7 @@ const huntResult: HuntResult = {
|
||||
},
|
||||
{
|
||||
id: 'encounter-2',
|
||||
monster: { key: 'bear', name: 'Bär', level: 3, artworkPath: '/images/enemies/Bear.png' },
|
||||
monster: { key: 'bear', name: 'Bear', level: 3, artworkPath: '/images/enemies/Bear.png' },
|
||||
dangerRating: 'STRONG',
|
||||
status: 'DEFEATED',
|
||||
},
|
||||
@@ -27,11 +27,11 @@ const huntResult: HuntResult = {
|
||||
|
||||
const refreshedHuntResult: HuntResult = {
|
||||
id: 'hunt-id-2',
|
||||
location: { id: 'origin-id', key: 'south-gate', name: 'Südtor' },
|
||||
location: { id: 'origin-id', key: 'south-gate', name: 'South Gate' },
|
||||
encounters: [
|
||||
{
|
||||
id: 'encounter-3',
|
||||
monster: { key: 'rat', name: 'Ratte', level: 1, artworkPath: '/images/enemies/Rat.png' },
|
||||
monster: { key: 'rat', name: 'Rat', level: 1, artworkPath: '/images/enemies/Rat.png' },
|
||||
dangerRating: 'WEAK',
|
||||
status: 'AVAILABLE',
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ const wornSword: InventoryItem = {
|
||||
item: {
|
||||
key: 'worn-short-sword',
|
||||
name: 'Abgenutztes Kurzschwert',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 8,
|
||||
@@ -27,8 +27,8 @@ const banditBlade: InventoryItem = {
|
||||
equipped: false,
|
||||
item: {
|
||||
key: 'bandit-blade',
|
||||
name: 'Räuberklinge',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
name: 'Bandit Blade',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 11,
|
||||
@@ -46,7 +46,7 @@ const ashPelt: InventoryItem = {
|
||||
item: {
|
||||
key: 'ash-pelt',
|
||||
name: 'Aschenfell',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: null,
|
||||
weaponDamage: 0,
|
||||
|
||||
@@ -11,10 +11,10 @@ interface StatRow {
|
||||
|
||||
type StatKey = 'weaponDamage' | 'bonusAttack' | 'bonusHp' | 'bonusArmor';
|
||||
const STAT_LABELS: ReadonlyArray<{ label: string; key: StatKey }> = [
|
||||
{ label: 'Waffenschaden', key: 'weaponDamage' },
|
||||
{ label: 'Angriff', key: 'bonusAttack' },
|
||||
{ label: 'Leben', key: 'bonusHp' },
|
||||
{ label: 'Rüstung', key: 'bonusArmor' },
|
||||
{ label: 'Weapon Damage', key: 'weaponDamage' },
|
||||
{ label: 'Attack', key: 'bonusAttack' },
|
||||
{ label: 'Health', key: 'bonusHp' },
|
||||
{ label: 'Armor', key: 'bonusArmor' },
|
||||
];
|
||||
|
||||
/** Selected-item details and equip comparison (spec §32–37). */
|
||||
|
||||
@@ -17,7 +17,7 @@ const inventory: InventoryResponse = {
|
||||
item: {
|
||||
key: 'worn-short-sword',
|
||||
name: 'Abgenutztes Kurzschwert',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 8,
|
||||
@@ -33,8 +33,8 @@ const inventory: InventoryResponse = {
|
||||
equipped: false,
|
||||
item: {
|
||||
key: 'bandit-blade',
|
||||
name: 'Räuberklinge',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
name: 'Bandit Blade',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 11,
|
||||
@@ -70,7 +70,7 @@ const character: CharacterResponse = {
|
||||
attack: 6,
|
||||
hpRegenPerSecond: 1,
|
||||
hpRegenSince: null,
|
||||
currentLocation: { id: 'loc-1', key: 'south-gate', name: 'Südtor' },
|
||||
currentLocation: { id: 'loc-1', key: 'south-gate', name: 'South Gate' },
|
||||
};
|
||||
|
||||
interface SetupOptions {
|
||||
@@ -176,8 +176,8 @@ describe('InventoryPageComponent', () => {
|
||||
equipped: true,
|
||||
item: {
|
||||
key: 'iron-helm',
|
||||
name: 'Eiserner Helm',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
name: 'Iron Helm',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'HEAD',
|
||||
weaponDamage: 0,
|
||||
|
||||
@@ -16,7 +16,7 @@ const inventory: InventoryResponse = {
|
||||
item: {
|
||||
key: 'worn-short-sword',
|
||||
name: 'Abgenutztes Kurzschwert',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 8,
|
||||
@@ -32,8 +32,8 @@ const inventory: InventoryResponse = {
|
||||
equipped: false,
|
||||
item: {
|
||||
key: 'bandit-blade',
|
||||
name: 'Räuberklinge',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
name: 'Bandit Blade',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 11,
|
||||
@@ -61,7 +61,7 @@ const equipment: EquipmentResponse = {
|
||||
|
||||
const equippedAfter: EquipmentResponse = {
|
||||
...equipment,
|
||||
slots: { ...equipment.slots, WEAPON: { characterItemId: 'item-blade', item: { key: 'bandit-blade', name: 'Räuberklinge', rarity: 'COMMON', iconPath: '/images/items/bandit-blade.png' } } },
|
||||
slots: { ...equipment.slots, WEAPON: { characterItemId: 'item-blade', item: { key: 'bandit-blade', name: 'Bandit Blade', rarity: 'COMMON', iconPath: '/images/items/bandit-blade.png' } } },
|
||||
stats: { maxHp: 100, attack: 7, weaponDamage: 11, armor: 0 },
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { WorldStore } from './world.store';
|
||||
|
||||
const trackResult = {
|
||||
interactionKey: 'inspect-tracks',
|
||||
title: 'Verdächtige Spuren',
|
||||
text: 'Frische Stiefelabdrücke führen nach Osten.',
|
||||
title: 'Suspicious Tracks',
|
||||
text: 'Among the ash and broken stones you make out several fresh bootprints. They lead east, toward the abandoned watchpost.',
|
||||
};
|
||||
|
||||
function setup(api: Partial<GameApiService> = {}, world: Partial<WorldStore> = {}) {
|
||||
@@ -128,7 +128,7 @@ describe('LocalLocationStore', () => {
|
||||
expect(
|
||||
store.interactionKeyOf({
|
||||
key: 'investigate-tracks',
|
||||
label: 'Spuren untersuchen',
|
||||
label: 'Investigate tracks',
|
||||
type: 'INVESTIGATE',
|
||||
iconKey: 'investigate',
|
||||
enabled: true,
|
||||
|
||||
@@ -37,25 +37,27 @@ describe('LocationInteractionPanelComponent', () => {
|
||||
const { element } = await setup({
|
||||
result: {
|
||||
interactionKey: 'inspect-tracks',
|
||||
title: 'Verdächtige Spuren',
|
||||
text: 'Frische Stiefelabdrücke führen nach Osten.',
|
||||
title: 'Suspicious Tracks',
|
||||
text: 'Among the ash and broken stones you make out several fresh bootprints. They lead east, toward the abandoned watchpost.',
|
||||
},
|
||||
});
|
||||
|
||||
expect(element.textContent).toContain('Verdächtige Spuren');
|
||||
expect(element.textContent).toContain('Frische Stiefelabdrücke führen nach Osten.');
|
||||
expect(element.textContent).toContain('Suspicious Tracks');
|
||||
expect(element.textContent).toContain(
|
||||
'Among the ash and broken stones you make out several fresh bootprints. They lead east, toward the abandoned watchpost.',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows an NPC line through the same panel, with no branching choices', async () => {
|
||||
const { element } = await setup({
|
||||
result: {
|
||||
interactionKey: 'wounded-scout',
|
||||
title: 'Verwundeter Kundschafter',
|
||||
text: '„Die Straße ist nicht mehr sicher."',
|
||||
title: 'Wounded Scout',
|
||||
text: '"The road isn\'t safe anymore. The raiders are coming from the direction of the old watchpost. If you keep going, keep your eyes open."',
|
||||
},
|
||||
});
|
||||
|
||||
expect(element.textContent).toContain('Verwundeter Kundschafter');
|
||||
expect(element.textContent).toContain('Wounded Scout');
|
||||
// Exactly one control: close. A dialogue tree is out of scope here.
|
||||
expect(element.querySelectorAll('button')).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -62,11 +62,11 @@ describe('LocationPageComponent', () => {
|
||||
it('names the place and its region straight away', async () => {
|
||||
const { element } = await setup();
|
||||
|
||||
expect(element.querySelector('h1')?.textContent).toContain('Verbrannte Straße');
|
||||
expect(element.textContent).toContain('Gebiet 1');
|
||||
expect(element.textContent).toContain('Aschenfelder');
|
||||
expect(element.querySelector('h1')?.textContent).toContain('Burned Road');
|
||||
expect(element.textContent).toContain('Tier 1');
|
||||
expect(element.textContent).toContain('Ashen Fields');
|
||||
expect(element.textContent).toContain(
|
||||
'Ein alter Handelsweg, der durch Feuer und Krieg in Asche gelegt wurde.',
|
||||
'An old trade road, burned to ash by fire and war. Charred carts, broken weapons, and silenced cries line the path into the Ashen Fields.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('LocationPageComponent', () => {
|
||||
const image = element.querySelector('img') as HTMLImageElement;
|
||||
|
||||
expect(image.getAttribute('src')).toBe('/images/backgrounds/Aschestrasse.png');
|
||||
expect(image.getAttribute('alt')).toBe('Location view: Verbrannte Straße');
|
||||
expect(image.getAttribute('alt')).toBe('Location view: Burned Road');
|
||||
});
|
||||
|
||||
it('places every hotspot on the artwork', async () => {
|
||||
@@ -96,10 +96,10 @@ describe('LocationPageComponent', () => {
|
||||
const actions = element.querySelectorAll('[data-action]');
|
||||
|
||||
expect([...actions].map((action) => action.querySelector('.location-action__label')?.textContent?.trim())).toEqual([
|
||||
'Jagd beginnen',
|
||||
'Spuren untersuchen',
|
||||
'Umgebung durchsuchen',
|
||||
'Zur Karte',
|
||||
'Begin Hunt',
|
||||
'Investigate tracks',
|
||||
'Search surroundings',
|
||||
'To Map',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -173,13 +173,13 @@ describe('LocationPageComponent', () => {
|
||||
|
||||
interactionResult.set({
|
||||
interactionKey: 'inspect-tracks',
|
||||
title: 'Verdächtige Spuren',
|
||||
text: 'Frische Stiefelabdrücke führen nach Osten.',
|
||||
title: 'Suspicious Tracks',
|
||||
text: 'Among the ash and broken stones you make out several fresh bootprints. They lead east, toward the abandoned watchpost.',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.querySelector('[data-interaction-panel]')?.textContent).toContain(
|
||||
'Frische Stiefelabdrücke führen nach Osten.',
|
||||
'Among the ash and broken stones you make out several fresh bootprints. They lead east, toward the abandoned watchpost.',
|
||||
);
|
||||
// The scene is not replaced by the panel.
|
||||
expect(element.querySelector('img')).not.toBeNull();
|
||||
@@ -234,7 +234,7 @@ describe('LocationPageComponent', () => {
|
||||
it('renders a second location from its own data alone', async () => {
|
||||
const { element } = await setup(southGateFixture(southGateContent()));
|
||||
|
||||
expect(element.querySelector('h1')?.textContent).toContain('Südtor von Graufurt');
|
||||
expect(element.querySelector('h1')?.textContent).toContain('Graufurt South Gate');
|
||||
expect(element.querySelectorAll('app-location-poi')).toHaveLength(1);
|
||||
expect(element.querySelectorAll('[data-action]')).toHaveLength(1);
|
||||
});
|
||||
@@ -245,8 +245,8 @@ function southGateContent(): Partial<CurrentLocationResponse> {
|
||||
pointsOfInterest: [
|
||||
{
|
||||
key: 'gate-watch',
|
||||
title: 'Torwache',
|
||||
actionLabel: 'Sprechen',
|
||||
title: 'Gate Watch',
|
||||
actionLabel: 'Talk',
|
||||
type: 'NPC',
|
||||
iconKey: 'speak',
|
||||
xPercent: 45,
|
||||
@@ -257,8 +257,8 @@ function southGateContent(): Partial<CurrentLocationResponse> {
|
||||
primaryActions: [
|
||||
{
|
||||
key: 'talk-to-watch',
|
||||
label: 'Wache ansprechen',
|
||||
description: 'Lage erfragen',
|
||||
label: 'Talk to the watch',
|
||||
description: 'Ask about the situation',
|
||||
type: 'NPC',
|
||||
iconKey: 'speak',
|
||||
enabled: true,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { LocationPoiComponent } from './location-poi.component';
|
||||
|
||||
const wagon: LocationPointOfInterest = {
|
||||
key: 'search-abandoned-wagon',
|
||||
title: 'Verlassener Wagen',
|
||||
actionLabel: 'Durchsuchen',
|
||||
title: 'Abandoned Wagon',
|
||||
actionLabel: 'Search',
|
||||
type: 'SEARCH',
|
||||
iconKey: 'search',
|
||||
xPercent: 80,
|
||||
@@ -53,8 +53,8 @@ describe('LocationPoiComponent', () => {
|
||||
const { fixture } = await setup();
|
||||
const text = (fixture.nativeElement as HTMLElement).textContent ?? '';
|
||||
|
||||
expect(text).toContain('Verlassener Wagen');
|
||||
expect(text).toContain('Durchsuchen');
|
||||
expect(text).toContain('Abandoned Wagon');
|
||||
expect(text).toContain('Search');
|
||||
});
|
||||
|
||||
it('emits the whole hotspot when clicked', async () => {
|
||||
@@ -102,7 +102,7 @@ describe('LocationPoiComponent', () => {
|
||||
it('describes itself with both title and action for screen readers', async () => {
|
||||
const { button } = await setup();
|
||||
|
||||
expect(button.getAttribute('aria-label')).toBe('Verlassener Wagen: Durchsuchen');
|
||||
expect(button.getAttribute('aria-label')).toBe('Abandoned Wagon: Search');
|
||||
});
|
||||
|
||||
it('falls back to the title alone when a hotspot has no action label', async () => {
|
||||
@@ -111,6 +111,6 @@ describe('LocationPoiComponent', () => {
|
||||
actionLabel: undefined,
|
||||
});
|
||||
|
||||
expect(button.getAttribute('aria-label')).toBe('Verlassener Wagen');
|
||||
expect(button.getAttribute('aria-label')).toBe('Abandoned Wagon');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,8 +20,8 @@ describe('LocationSidebarComponent', () => {
|
||||
const element = await render(burnedRoadFixture());
|
||||
const text = element.textContent ?? '';
|
||||
|
||||
expect(text).toContain('Aschenfelder');
|
||||
expect(text).toContain('Verbrannte Straße');
|
||||
expect(text).toContain('Ashen Fields');
|
||||
expect(text).toContain('Burned Road');
|
||||
expect(text).toContain('Hunting Ground');
|
||||
expect(text).toContain('1–2');
|
||||
});
|
||||
@@ -44,8 +44,8 @@ describe('LocationSidebarComponent', () => {
|
||||
const encounters = element.querySelectorAll('[data-sidebar="encounters"] li');
|
||||
|
||||
expect([...encounters].map((item) => item.textContent?.trim())).toEqual([
|
||||
'Aschenratte',
|
||||
'Straßenräuber',
|
||||
'Ash Rat',
|
||||
'Road Bandit',
|
||||
]);
|
||||
expect(element.textContent).toContain('the hunt rolls independently');
|
||||
});
|
||||
@@ -55,10 +55,10 @@ describe('LocationSidebarComponent', () => {
|
||||
const interactions = element.querySelectorAll('[data-sidebar="interactions"] li');
|
||||
|
||||
expect([...interactions].map((item) => item.textContent?.trim())).toEqual([
|
||||
'Jagd beginnen',
|
||||
'Untersuchen',
|
||||
'Durchsuchen',
|
||||
'Sprechen',
|
||||
'Begin Hunt',
|
||||
'Investigate',
|
||||
'Search',
|
||||
'Talk',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('LocationSidebarComponent', () => {
|
||||
});
|
||||
|
||||
expect(element.querySelector('[data-sidebar="interactions"]')?.textContent).not.toContain(
|
||||
'Sprechen',
|
||||
'Talk',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('LocationSidebarComponent', () => {
|
||||
const rewards = element.querySelectorAll('[data-sidebar="rewards"] li');
|
||||
|
||||
expect([...rewards].map((item) => item.textContent?.trim())).toEqual([
|
||||
'Ausrüstung',
|
||||
'Equipment',
|
||||
'Material',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ const burnedRoadConnection: CurrentLocationConnection = {
|
||||
targetLocation: {
|
||||
id: 'burned-road-id',
|
||||
key: 'burned-road',
|
||||
name: 'Verbrannte Straße',
|
||||
name: 'Burned Road',
|
||||
},
|
||||
travelDurationSeconds: 10,
|
||||
danger: 'LOW',
|
||||
@@ -84,7 +84,7 @@ describe('WorldPageComponent', () => {
|
||||
|
||||
expect(store.selectConnection).toHaveBeenCalledWith(burnedRoadConnection);
|
||||
expect(element.textContent).toContain('Destination');
|
||||
expect(element.textContent).toContain('Verbrannte Straße');
|
||||
expect(element.textContent).toContain('Burned Road');
|
||||
expect(element.textContent).toContain('00:00:10');
|
||||
expect(element.textContent).toContain('Low');
|
||||
});
|
||||
@@ -129,7 +129,7 @@ describe('WorldPageComponent', () => {
|
||||
it('renders the server-derived countdown while travel is active without enabling another start', () => {
|
||||
store.currentTravel.set({
|
||||
status: 'TRAVELLING',
|
||||
originLocation: { id: 'south-gate-id', key: 'south-gate', name: 'Südtor von Graufurt' },
|
||||
originLocation: { id: 'south-gate-id', key: 'south-gate', name: 'Graufurt South Gate' },
|
||||
targetLocation: burnedRoadConnection.targetLocation,
|
||||
startedAt: '2026-08-19T06:00:00.000Z',
|
||||
arrivesAt: '2026-08-19T06:00:06.000Z',
|
||||
@@ -140,7 +140,7 @@ describe('WorldPageComponent', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
expect(element.textContent).toContain('Destination: Verbrannte Straße');
|
||||
expect(element.textContent).toContain('Destination: Burned Road');
|
||||
expect(element.textContent).toContain('Arrival in');
|
||||
expect(element.textContent).toContain('00:00:06');
|
||||
expect(element.querySelector<HTMLButtonElement>('[data-travel-start]')?.disabled).toBe(true);
|
||||
@@ -194,7 +194,7 @@ describe('WorldPageComponent', () => {
|
||||
store.arrived.set({
|
||||
id: 'burned-road-id',
|
||||
key: 'burned-road',
|
||||
name: 'Verbrannte Straße',
|
||||
name: 'Burned Road',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -207,7 +207,7 @@ describe('WorldPageComponent', () => {
|
||||
it('stays on the map while a journey is still running', () => {
|
||||
store.currentTravel.set({
|
||||
status: 'TRAVELLING',
|
||||
originLocation: { id: 'south-gate-id', key: 'south-gate', name: 'Südtor von Graufurt' },
|
||||
originLocation: { id: 'south-gate-id', key: 'south-gate', name: 'Graufurt South Gate' },
|
||||
targetLocation: burnedRoadConnection.targetLocation,
|
||||
startedAt: '2026-08-20T10:00:00.000Z',
|
||||
arrivesAt: '2026-08-20T10:00:10.000Z',
|
||||
|
||||
@@ -21,12 +21,12 @@ const character: CharacterResponse = {
|
||||
attack: 6,
|
||||
hpRegenPerSecond: 1,
|
||||
hpRegenSince: null,
|
||||
currentLocation: { id: 'origin-id', key: 'south-gate', name: 'Südtor' },
|
||||
currentLocation: { id: 'origin-id', key: 'south-gate', name: 'South Gate' },
|
||||
};
|
||||
|
||||
const currentLocation = southGateFixture({
|
||||
id: 'origin-id',
|
||||
name: 'Südtor',
|
||||
name: 'South Gate',
|
||||
description: 'Der Ausgang zur Wildnis.',
|
||||
dangerLevel: 1,
|
||||
connections: [
|
||||
@@ -34,7 +34,7 @@ const currentLocation = southGateFixture({
|
||||
targetLocation: {
|
||||
id: 'target-id',
|
||||
key: 'burned-road',
|
||||
name: 'Verbrannte Straße',
|
||||
name: 'Burned Road',
|
||||
},
|
||||
travelDurationSeconds: 10,
|
||||
danger: 'LOW',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<div class="app-shell__content" [class.app-shell__content--no-context]="!showContextPanel()">
|
||||
<app-side-navigation />
|
||||
<main class="app-shell__main" aria-label="Spielinhalt">
|
||||
<main class="app-shell__main" aria-label="Game content">
|
||||
<router-outlet />
|
||||
</main>
|
||||
@if (showContextPanel()) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ContextPanelComponent } from './context-panel.component';
|
||||
const southGate = {
|
||||
id: 'south-gate-id',
|
||||
key: 'south-gate',
|
||||
name: 'Südtor von Graufurt',
|
||||
name: 'Graufurt South Gate',
|
||||
description: 'Der letzte sichere Schritt vor den Aschenfeldern.',
|
||||
regionKey: 'ashen-fields',
|
||||
minRecommendedLevel: 1,
|
||||
@@ -23,11 +23,11 @@ const burnedRoad = {
|
||||
...southGate,
|
||||
id: 'burned-road-id',
|
||||
key: 'burned-road',
|
||||
name: 'Verbrannte Straße',
|
||||
name: 'Burned Road',
|
||||
isSafe: false,
|
||||
huntingEnabled: true,
|
||||
artworkPath: '/images/backgrounds/Aschestrasse.png',
|
||||
possibleMonsters: ['Aschenratte', 'Straßenräuber'],
|
||||
possibleMonsters: ['Ash Rat', 'Road Bandit'],
|
||||
};
|
||||
|
||||
describe('ContextPanelComponent', () => {
|
||||
@@ -97,7 +97,7 @@ describe('ContextPanelComponent', () => {
|
||||
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
expect(element.textContent).toContain('Possible Encounters');
|
||||
expect(element.textContent).toContain('Aschenratte, Straßenräuber');
|
||||
expect(element.textContent).toContain('Ash Rat, Road Bandit');
|
||||
expect(element.textContent).not.toMatch(/\d+\s?%/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ItemCardComponent } from './item-card.component';
|
||||
|
||||
const banditBlade: RewardItemSummary = {
|
||||
key: 'bandit-blade',
|
||||
name: 'Räuberklinge',
|
||||
name: 'Bandit Blade',
|
||||
rarity: 'COMMON',
|
||||
iconPath: '/images/items/bandit-blade.png',
|
||||
};
|
||||
@@ -27,8 +27,8 @@ describe('ItemCardComponent', () => {
|
||||
|
||||
const icon = element.querySelector<HTMLImageElement>('[data-item-icon]');
|
||||
expect(icon?.getAttribute('src')).toBe('/images/items/bandit-blade.png');
|
||||
expect(icon?.getAttribute('alt')).toBe('Räuberklinge');
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain('Räuberklinge');
|
||||
expect(icon?.getAttribute('alt')).toBe('Bandit Blade');
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain('Bandit Blade');
|
||||
expect(element.querySelector('[data-item-rarity]')?.textContent).toContain('Common');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user