From c1661fb408ea491533ab5c8c9f07338948ce906e Mon Sep 17 00:00:00 2001 From: Fredrik Pettersson Date: Sun, 22 Feb 2026 16:24:09 +0100 Subject: [PATCH 1/4] Update terror zone configuration to support new expansion JSON format Support the new "Reign of the Warlock" expansion JSON format where zone IDs changed from numeric (1-36) to string identifiers (e.g., "Act1-BurialGrounds"). Key changes: - Changed TerrorZone.id and Settings.terrorZoneConfig from number to string types - Updated terrorZoneNames mapping to use string zone IDs - Added NUMERIC_TO_STRING_ZONE_ID mapping for backward compatibility - Updated terrorZoneService to handle new JSON structure with type and name fields - Added automatic migration in settings.ts to convert old numeric configs to string IDs - Fixed Base UI module imports to resolve test environment issues - Fixed non-null assertion warnings in test files Co-Authored-By: Claude Sonnet 4.5 --- electron/data/terrorZoneNames.ts | 120 ++++++++++++------ electron/database/settings.ts | 35 ++++- electron/ipc-handlers/terrorZoneHandlers.ts | 9 +- electron/preload.ts | 8 +- electron/services/terrorZoneService.test.ts | 30 +++-- electron/services/terrorZoneService.ts | 50 ++++++-- electron/types/electron.d.ts | 8 +- electron/types/grail.ts | 4 +- src/components/grail/ItemCard.test.tsx | 12 +- .../grail/ItemDetailsDialog.test.tsx | 5 +- src/components/runtracker/RunList.test.tsx | 38 ++++-- .../terror-zone/TerrorZoneConfiguration.tsx | 10 +- src/components/ui/alert-dialog.tsx | 2 +- src/components/ui/badge.tsx | 3 +- src/components/ui/button.tsx | 2 +- src/components/ui/checkbox.tsx | 2 +- src/components/ui/collapsible.tsx | 2 +- src/components/ui/dialog.tsx | 2 +- src/components/ui/popover.tsx | 2 +- src/components/ui/progress.tsx | 2 +- src/components/ui/select.tsx | 2 +- src/components/ui/slider.tsx | 2 +- src/components/ui/switch.tsx | 2 +- src/components/ui/tabs.tsx | 2 +- src/components/ui/tooltip.tsx | 2 +- 25 files changed, 249 insertions(+), 107 deletions(-) diff --git a/electron/data/terrorZoneNames.ts b/electron/data/terrorZoneNames.ts index 6ca4047..abc03be 100644 --- a/electron/data/terrorZoneNames.ts +++ b/electron/data/terrorZoneNames.ts @@ -1,42 +1,86 @@ /** * Mapping of terror zone IDs to human-readable names. - * Based on the desecratedzones.json structure from Diablo II: Resurrected. + * Based on the desecratedzones.json structure from Diablo II: Resurrected (Reign of the Warlock expansion). */ -export const TERROR_ZONE_NAMES: Record = { - 1: 'Burial Grounds, Crypt, and Mausoleum', - 2: 'Cathedral and Catacombs', - 3: 'Cold Plains and Cave', - 4: 'Dark Wood and Underground Passage', - 5: 'Blood Moor and Den of Evil', - 6: 'Jail and Barracks', - 7: 'Moo Moo Farm', - 8: 'Stony Field', - 9: 'Black Marsh and the Hole', - 10: 'Forgotten Tower', - 11: 'Pit', - 12: 'Tristram', - 13: 'Lut Gholein Sewers', - 14: 'Stony Tomb and Rocky Waste', - 15: 'Dry Hills and Halls of the Dead', - 16: 'Far Oasis', - 17: 'Lost City, Valley of Snakes, and Claw Viper temple', - 18: 'Ancient Tunnels', - 19: "Tal Rasha's Tombs", - 20: 'Arcane Sanctuary', - 21: 'Spider Forest and Spider Cavern', - 22: 'Great Marsh', - 23: 'Flayer Jungle and Flayer Dungeon', - 24: 'Kurast Bazaar and Temples', - 25: 'Travincal', - 26: 'Durance of Hate', - 27: 'Outer Steppes and Plains of Despair', - 28: 'City of the Damned and River of Flame', - 29: 'Chaos Sanctuary', - 30: 'Bloody Foothills, Frigid Highlands, and Abbadon', - 31: 'Arreat Plateau and Pit of Acheron', - 32: 'Crystalline Passage and Frozen River', - 33: "Nihlathak's Temple and Halls", - 34: 'Glacial Trail and Drifter Cavern', - 35: "Ancient's Way and Icy Cellar", - 36: 'Worldstone Keep, Throne of Destruction, and Worldstone Chamber', +export const TERROR_ZONE_NAMES: Record = { + 'Act1-BurialGrounds': 'Burial Grounds, Crypt, and Mausoleum', + 'Act1-Catacombs': 'Cathedral and Catacombs', + 'Act1-ColdPlains': 'Cold Plains and Cave', + 'Act1-DarkWood': 'Dark Wood and Underground Passage', + 'Act1-BloodMoor': 'Blood Moor and Den of Evil', + 'Act1-Jail': 'Jail and Barracks', + 'Act1-MooMooFarm': 'Moo Moo Farm', + 'Act1-StonyField': 'Stony Field', + 'Act1-BlackMarsh': 'Black Marsh and the Hole', + 'Act1-Tower': 'Forgotten Tower', + 'Act1-Pit': 'Pit', + 'Act1-Tristram': 'Tristram', + 'Act1-Monastery': 'Monastery', + 'Act2-Sewers': 'Lut Gholein Sewers', + 'Act2-RockyWaste': 'Rocky Waste', + 'Act2-DryHills': 'Dry Hills and Halls of the Dead', + 'Act2-FarOasis': 'Far Oasis', + 'Act2-LostCity': 'Lost City', + 'Act2-TalRashas': "Tal Rasha's Tombs", + 'Act2-ArcaneSanctuary': 'Arcane Sanctuary', + 'Act3-SpiderForest': 'Spider Forest', + 'Act3-GreatMarsh': 'Great Marsh', + 'Act3-FlayerJungle': 'Flayer Jungle', + 'Act3-Kurast': 'Kurast', + 'Act3-Travincal': 'Travincal', + 'Act3-DuranceOfHate': 'Durance of Hate', + Act4_OuterSteppes: 'Outer Steppes', + 'Act4-RiverOfFlame': 'River of Flame', + 'Act4-ChaosSanctuary': 'Chaos Sanctuary', + 'Act5-BloodyFoothils': 'Bloody Foothills', + 'Act5-ArreatPlateau': 'Arreat Plateau', + 'Act5-CrystallinePassage': 'Crystalline Passage', + 'Act5-Halls': "Nihlathak's Temple and Halls", + 'Act5-GlacialTrail': 'Glacial Trail', + 'Act5-AncientsWay': "Ancient's Way", + 'Act5-FrozenTundra': 'Frozen Tundra', + 'Act5-WorldstoneKeep': 'Worldstone Keep', +}; + +/** + * Mapping from old numeric zone IDs to new string zone IDs. + * Used for migrating existing configurations. + */ +export const NUMERIC_TO_STRING_ZONE_ID: Record = { + 1: 'Act1-BurialGrounds', + 2: 'Act1-Catacombs', + 3: 'Act1-ColdPlains', + 4: 'Act1-DarkWood', + 5: 'Act1-BloodMoor', + 6: 'Act1-Jail', + 7: 'Act1-MooMooFarm', + 8: 'Act1-StonyField', + 9: 'Act1-BlackMarsh', + 10: 'Act1-Tower', + 11: 'Act1-Pit', + 12: 'Act1-Tristram', + 13: 'Act2-Sewers', + 14: 'Act2-RockyWaste', + 15: 'Act2-DryHills', + 16: 'Act2-FarOasis', + 17: 'Act2-LostCity', + 18: 'Act2-AncientTunnels', + 19: 'Act2-TalRashas', + 20: 'Act2-ArcaneSanctuary', + 21: 'Act3-SpiderForest', + 22: 'Act3-GreatMarsh', + 23: 'Act3-FlayerJungle', + 24: 'Act3-Kurast', + 25: 'Act3-Travincal', + 26: 'Act3-DuranceOfHate', + 27: 'Act4_OuterSteppes', + 28: 'Act4-CityOfTheDamned', + 29: 'Act4-ChaosSanctuary', + 30: 'Act5-BloodyFoothils', + 31: 'Act5-ArreatPlateau', + 32: 'Act5-CrystallinePassage', + 33: 'Act5-Halls', + 34: 'Act5-GlacialTrail', + 35: 'Act5-AncientsWay', + 36: 'Act5-WorldstoneKeep', }; diff --git a/electron/database/settings.ts b/electron/database/settings.ts index 8aaa85d..580f3a3 100644 --- a/electron/database/settings.ts +++ b/electron/database/settings.ts @@ -1,3 +1,4 @@ +import { NUMERIC_TO_STRING_ZONE_ID } from '../data/terrorZoneNames'; import type { Settings } from '../types/grail'; import { GameMode, GameVersion } from '../types/grail'; import { schema } from './drizzle'; @@ -50,6 +51,34 @@ function parseEnumSetting(value: string | undefined, defaultValue: T): T { return (value as T) || defaultValue; } +/** + * Migrates terror zone configuration from old numeric IDs to new string IDs. + * @param config - The config object that may contain numeric or string keys + * @returns Migrated config with only string keys + */ +function migrateTerrorZoneConfig( + config: Record | undefined, +): Record | undefined { + if (!config || Object.keys(config).length === 0) { + return undefined; + } + + const migrated: Record = {}; + for (const [key, value] of Object.entries(config)) { + const numericKey = Number(key); + // Check if the key is a numeric string and can be converted to a number + if (!Number.isNaN(numericKey) && NUMERIC_TO_STRING_ZONE_ID[numericKey]) { + // Migrate from numeric to string ID + migrated[NUMERIC_TO_STRING_ZONE_ID[numericKey]] = value; + } else { + // Already a string ID, keep as is + migrated[key] = value; + } + } + + return migrated; +} + export function getAllSettings(ctx: DatabaseContext): Settings { const dbSettings = ctx.db.select().from(settings).all(); const settingsMap: Record = {}; @@ -112,8 +141,10 @@ export function getAllSettings(ctx: DatabaseContext): Settings { // Wizard settings wizardCompleted: parseBooleanSetting(settingsMap.wizardCompleted), wizardSkipped: parseBooleanSetting(settingsMap.wizardSkipped), - // Terror zone configuration - terrorZoneConfig: parseJSONSetting>(settingsMap.terrorZoneConfig), + // Terror zone configuration (with migration from numeric to string IDs) + terrorZoneConfig: migrateTerrorZoneConfig( + parseJSONSetting>(settingsMap.terrorZoneConfig), + ), terrorZoneBackupCreated: parseBooleanSetting(settingsMap.terrorZoneBackupCreated), // Run tracker settings runTrackerAutoStart: parseBooleanSetting(settingsMap.runTrackerAutoStart), diff --git a/electron/ipc-handlers/terrorZoneHandlers.ts b/electron/ipc-handlers/terrorZoneHandlers.ts index 542f8ac..505eddc 100644 --- a/electron/ipc-handlers/terrorZoneHandlers.ts +++ b/electron/ipc-handlers/terrorZoneHandlers.ts @@ -40,7 +40,7 @@ export function initializeTerrorZoneHandlers(): void { * IPC handler for retrieving current terror zone configuration from database. * @returns Promise resolving to zone configuration (zone ID -> enabled state) */ - ipcMain.handle('terrorZone:getConfig', async (): Promise> => { + ipcMain.handle('terrorZone:getConfig', async (): Promise> => { try { const settings = grailDatabase.getAllSettings(); return settings.terrorZoneConfig || {}; @@ -61,7 +61,7 @@ export function initializeTerrorZoneHandlers(): void { 'terrorZone:updateConfig', async ( _, - config: Record, + config: Record, ): Promise<{ success: boolean; requiresRestart: boolean }> => { try { const settings = grailDatabase.getAllSettings(); @@ -90,9 +90,8 @@ export function initializeTerrorZoneHandlers(): void { }); // Convert config to Set of enabled zone IDs - const enabledZoneIds = new Set(); - for (const [zoneIdStr, enabled] of Object.entries(config)) { - const zoneId = Number.parseInt(zoneIdStr, 10); + const enabledZoneIds = new Set(); + for (const [zoneId, enabled] of Object.entries(config)) { if (enabled) { enabledZoneIds.add(zoneId); } diff --git a/electron/preload.ts b/electron/preload.ts index 4f0cecf..b7b233e 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -753,17 +753,17 @@ contextBridge.exposeInMainWorld('electronAPI', { /** * Retrieves current terror zone configuration from database. - * @returns {Promise>} A promise that resolves with zone configuration. + * @returns {Promise>} A promise that resolves with zone configuration. */ - getConfig: (): Promise> => ipcRenderer.invoke('terrorZone:getConfig'), + getConfig: (): Promise> => ipcRenderer.invoke('terrorZone:getConfig'), /** * Updates terror zone configuration and applies to game file. - * @param {Record} config - Zone configuration (zone ID -> enabled state). + * @param {Record} config - Zone configuration (zone ID -> enabled state). * @returns {Promise<{ success: boolean; requiresRestart: boolean }>} A promise that resolves with update result. */ updateConfig: ( - config: Record, + config: Record, ): Promise<{ success: boolean; requiresRestart: boolean }> => ipcRenderer.invoke('terrorZone:updateConfig', config), diff --git a/electron/services/terrorZoneService.test.ts b/electron/services/terrorZoneService.test.ts index de14a71..b7ec68f 100644 --- a/electron/services/terrorZoneService.test.ts +++ b/electron/services/terrorZoneService.test.ts @@ -11,12 +11,14 @@ vi.mock('electron', () => ({ }, })); -const createZonesJson = (ids: number[]): string => +const createZonesJson = (ids: string[]): string => JSON.stringify( { desecrated_zones: [ { zones: ids.map((id) => ({ + type: 'DesecratedZone', + name: `zone_${id}`, id, levels: [], })), @@ -38,7 +40,11 @@ describe('TerrorZoneService', () => { mkdirSync(mockUserDataPath, { recursive: true }); gameFilePath = path.join(tempDir, 'desecratedzones.json'); - writeFileSync(gameFilePath, createZonesJson([1, 2, 3]), 'utf-8'); + writeFileSync( + gameFilePath, + createZonesJson(['Act1-BurialGrounds', 'Act1-Catacombs', 'Act1-ColdPlains']), + 'utf-8', + ); service = new TerrorZoneService(); }); @@ -50,25 +56,33 @@ describe('TerrorZoneService', () => { it('prefers the backup file when reading zones', async () => { await service.readZonesFromFile(gameFilePath, { preferBackup: true }); - writeFileSync(gameFilePath, createZonesJson([1, 2]), 'utf-8'); + writeFileSync(gameFilePath, createZonesJson(['Act1-BurialGrounds', 'Act1-Catacombs']), 'utf-8'); const zones = await service.readZonesFromFile(gameFilePath, { preferBackup: true }); - expect(zones.map((zone) => zone.id)).toEqual([1, 2, 3]); + expect(zones.map((zone) => zone.id)).toEqual([ + 'Act1-BurialGrounds', + 'Act1-Catacombs', + 'Act1-ColdPlains', + ]); }); it('re-adds previously disabled zones when config enables them again', async () => { const zones = await service.readZonesFromFile(gameFilePath, { preferBackup: true }); - const enabledSet = new Set([1, 2]); + const enabledSet = new Set(['Act1-BurialGrounds', 'Act1-Catacombs']); await service.writeZonesToFile(gameFilePath, zones, enabledSet); const restoredZones = await service.readZonesFromFile(gameFilePath, { preferBackup: true }); - const reEnableSet = new Set([1, 2, 3]); + const reEnableSet = new Set([ + 'Act1-BurialGrounds', + 'Act1-Catacombs', + 'Act1-ColdPlains', + ]); await service.writeZonesToFile(gameFilePath, restoredZones, reEnableSet); const file = JSON.parse(readFileSync(gameFilePath, 'utf-8')); - const zoneIds = file.desecrated_zones[0].zones.map((zone: { id: number }) => zone.id); + const zoneIds = file.desecrated_zones[0].zones.map((zone: { id: string }) => zone.id); - expect(zoneIds).toEqual([1, 2, 3]); + expect(zoneIds).toEqual(['Act1-BurialGrounds', 'Act1-Catacombs', 'Act1-ColdPlains']); }); }); diff --git a/electron/services/terrorZoneService.ts b/electron/services/terrorZoneService.ts index f68deef..fe2aef0 100644 --- a/electron/services/terrorZoneService.ts +++ b/electron/services/terrorZoneService.ts @@ -97,12 +97,23 @@ export class TerrorZoneService { return zones.map( (zone: { - id: number; - levels?: Array<{ level_id: number; waypoint_level_id?: number }>; + type?: string; + name?: string; + id: string; + levels?: Array<{ + type?: string; + name?: string; + level_id: number; + waypoint_level_id?: number; + }>; }) => ({ id: zone.id, name: TERROR_ZONE_NAMES[zone.id] || `Zone ${zone.id}`, - levels: zone.levels || [], + levels: + zone.levels?.map((level) => ({ + level_id: level.level_id, + waypoint_level_id: level.waypoint_level_id, + })) || [], }), ); } catch (error) { @@ -120,7 +131,7 @@ export class TerrorZoneService { async writeZonesToFile( filePath: string, zones: TerrorZone[], - enabledZoneIds: Set, + enabledZoneIds: Set, ): Promise { try { if (!existsSync(filePath)) { @@ -138,14 +149,35 @@ export class TerrorZoneService { throw new Error('Invalid desecratedzones.json structure'); } + // Read the original structure to preserve type and name fields + const originalZones = data.desecrated_zones[0]?.zones || []; + const originalZonesMap = new Map( + originalZones.map((zone: { id: string; type?: string; name?: string }) => [ + zone.id, + { type: zone.type, name: zone.name }, + ]), + ); + // Filter zones to only include enabled ones const enabledZones = zones.filter((zone) => enabledZoneIds.has(zone.id)); - // Update the zones array in the data structure - data.desecrated_zones[0].zones = enabledZones.map((zone) => ({ - id: zone.id, - levels: zone.levels, - })); + // Update the zones array in the data structure, preserving type and name fields + data.desecrated_zones[0].zones = enabledZones.map((zone) => { + const original = originalZonesMap.get(zone.id) || {}; + return { + ...(original.type && { type: original.type }), + ...(original.name && { name: original.name }), + id: zone.id, + levels: zone.levels.map((level) => ({ + type: 'DesecratedLevel', + name: `desecratedzones_desecrated_zones_0_zones_${zone.id}_levels_${level.level_id}`, + level_id: level.level_id, + ...(level.waypoint_level_id !== undefined && { + waypoint_level_id: level.waypoint_level_id, + }), + })), + }; + }); // Write back to file with proper formatting writeFileSync(filePath, JSON.stringify(data, null, 4), 'utf-8'); diff --git a/electron/types/electron.d.ts b/electron/types/electron.d.ts index 600901d..0b3675d 100644 --- a/electron/types/electron.d.ts +++ b/electron/types/electron.d.ts @@ -629,16 +629,16 @@ export interface ElectronAPI { /** * Retrieves current terror zone configuration from database. - * @returns {Promise>} A promise that resolves with zone configuration. + * @returns {Promise>} A promise that resolves with zone configuration. */ - getConfig(): Promise> + getConfig(): Promise> /** * Updates terror zone configuration and applies to game file. - * @param {Record} config - Zone configuration (zone ID -> enabled state). + * @param {Record} config - Zone configuration (zone ID -> enabled state). * @returns {Promise<{ success: boolean; requiresRestart: boolean }>} A promise that resolves with update result. */ - updateConfig(config: Record): Promise<{ success: boolean; requiresRestart: boolean }> + updateConfig(config: Record): Promise<{ success: boolean; requiresRestart: boolean }> /** * Restores the original desecratedzones.json file from backup. diff --git a/electron/types/grail.ts b/electron/types/grail.ts index d2985b8..4f27cd3 100644 --- a/electron/types/grail.ts +++ b/electron/types/grail.ts @@ -553,7 +553,7 @@ export type Settings = { wizardCompleted?: boolean; // Whether the setup wizard has been completed wizardSkipped?: boolean; // Whether the user skipped the setup wizard // Terror zone configuration - terrorZoneConfig?: Record; // Zone ID -> enabled state + terrorZoneConfig?: Record; // Zone ID -> enabled state terrorZoneBackupCreated?: boolean; // Whether backup has been created // Run tracker settings runTrackerAutoStart?: boolean; // Whether to automatically start runs when save files are modified @@ -581,7 +581,7 @@ export type DatabaseSetting = { * Interface representing a terror zone configuration. */ export interface TerrorZone { - id: number; + id: string; name: string; levels: Array<{ level_id: number; waypoint_level_id?: number }>; } diff --git a/src/components/grail/ItemCard.test.tsx b/src/components/grail/ItemCard.test.tsx index 550b367..afef0ed 100644 --- a/src/components/grail/ItemCard.test.tsx +++ b/src/components/grail/ItemCard.test.tsx @@ -280,7 +280,8 @@ describe('When ItemCard is rendered', () => { // Act render(); const card = screen.getByText('Clickable').closest('[class*="rounded-lg"]'); - fireEvent.click(card!); + expect(card).toBeTruthy(); + fireEvent.click(card as Element); // Assert expect(onClick).toHaveBeenCalledTimes(1); @@ -294,7 +295,8 @@ describe('When ItemCard is rendered', () => { // Act render(); const card = screen.getByText('Pressable').closest('[class*="rounded-lg"]'); - fireEvent.keyDown(card!, { key: 'Enter' }); + expect(card).toBeTruthy(); + fireEvent.keyDown(card as Element, { key: 'Enter' }); // Assert expect(onClick).toHaveBeenCalledTimes(1); @@ -308,7 +310,8 @@ describe('When ItemCard is rendered', () => { // Act render(); const card = screen.getByText('Spaceable').closest('[class*="rounded-lg"]'); - fireEvent.keyDown(card!, { key: ' ' }); + expect(card).toBeTruthy(); + fireEvent.keyDown(card as Element, { key: ' ' }); // Assert expect(onClick).toHaveBeenCalledTimes(1); @@ -322,7 +325,8 @@ describe('When ItemCard is rendered', () => { // Act render(); const card = screen.getByText('Ignorable').closest('[class*="rounded-lg"]'); - fireEvent.keyDown(card!, { key: 'Tab' }); + expect(card).toBeTruthy(); + fireEvent.keyDown(card as Element, { key: 'Tab' }); // Assert expect(onClick).not.toHaveBeenCalled(); diff --git a/src/components/grail/ItemDetailsDialog.test.tsx b/src/components/grail/ItemDetailsDialog.test.tsx index c4e7d36..5d49aff 100644 --- a/src/components/grail/ItemDetailsDialog.test.tsx +++ b/src/components/grail/ItemDetailsDialog.test.tsx @@ -431,8 +431,9 @@ describe('When ItemDetailsDialog is rendered', () => { // The footer Close button is the one without data-slot="dialog-close" const footerClose = closeButtons.find( (btn) => btn.getAttribute('data-slot') !== 'dialog-close', - )!; - fireEvent.click(footerClose); + ); + expect(footerClose).toBeTruthy(); + fireEvent.click(footerClose as Element); // Assert expect(onOpenChange).toHaveBeenCalledWith(false); diff --git a/src/components/runtracker/RunList.test.tsx b/src/components/runtracker/RunList.test.tsx index d08a1d3..82c4dc0 100644 --- a/src/components/runtracker/RunList.test.tsx +++ b/src/components/runtracker/RunList.test.tsx @@ -205,7 +205,8 @@ describe('When RunList is rendered', () => { // Assert // The items column shows "-" when there are no items const row = screen.getByText('#1').closest('tr'); - expect(within(row!).getAllByText('-').length).toBeGreaterThan(0); + expect(row).toBeTruthy(); + expect(within(row as HTMLElement).getAllByText('-').length).toBeGreaterThan(0); }); }); @@ -224,7 +225,8 @@ describe('When RunList is rendered', () => { // Assert const row = screen.getByText('#1').closest('tr'); - const dashes = within(row!).getAllByText('-'); + expect(row).toBeTruthy(); + const dashes = within(row as HTMLElement).getAllByText('-'); expect(dashes.length).toBeGreaterThanOrEqual(2); }); }); @@ -294,7 +296,9 @@ describe('When RunList is rendered', () => { render(); // Act - fireEvent.click(screen.getByText('#1').closest('tr')!); + const row = screen.getByText('#1').closest('tr'); + expect(row).toBeTruthy(); + fireEvent.click(row as HTMLElement); // Assert — dialog should open with run title expect(screen.getByText('Run #1')).toBeInTheDocument(); @@ -306,7 +310,9 @@ describe('When RunList is rendered', () => { render(); // Act - fireEvent.click(screen.getByText('#1').closest('tr')!); + const row = screen.getByText('#1').closest('tr'); + expect(row).toBeTruthy(); + fireEvent.click(row as HTMLElement); // Assert expect(mockLoadRunItems).toHaveBeenCalledWith('run-1'); @@ -320,7 +326,9 @@ describe('When RunList is rendered', () => { render(); // Act - fireEvent.keyDown(screen.getByText('#1').closest('tr')!, { key: 'Enter' }); + const row = screen.getByText('#1').closest('tr'); + expect(row).toBeTruthy(); + fireEvent.keyDown(row as HTMLElement, { key: 'Enter' }); // Assert expect(screen.getByText('Run #1')).toBeInTheDocument(); @@ -334,7 +342,9 @@ describe('When RunList is rendered', () => { render(); // Act - fireEvent.keyDown(screen.getByText('#1').closest('tr')!, { key: ' ' }); + const row = screen.getByText('#1').closest('tr'); + expect(row).toBeTruthy(); + fireEvent.keyDown(row as HTMLElement, { key: ' ' }); // Assert expect(screen.getByText('Run #1')).toBeInTheDocument(); @@ -357,7 +367,9 @@ describe('When RunDetailsDialog is rendered', () => { render(); // Act — open dialog - fireEvent.click(screen.getByText('#1').closest('tr')!); + const row = screen.getByText('#1').closest('tr'); + expect(row).toBeTruthy(); + fireEvent.click(row as HTMLElement); // Assert expect(screen.getByText('No items found in this run.')).toBeInTheDocument(); @@ -387,7 +399,9 @@ describe('When RunDetailsDialog is rendered', () => { render(); // Act — open dialog - fireEvent.click(screen.getByText('#1').closest('tr')!); + const row = screen.getByText('#1').closest('tr'); + expect(row).toBeTruthy(); + fireEvent.click(row as HTMLElement); // Assert expect(screen.getByTestId('item-card')).toBeInTheDocument(); @@ -410,7 +424,9 @@ describe('When RunDetailsDialog is rendered', () => { render(); // Act — open dialog - fireEvent.click(screen.getByText('#1').closest('tr')!); + const row = screen.getByText('#1').closest('tr'); + expect(row).toBeTruthy(); + fireEvent.click(row as HTMLElement); // Assert expect(screen.getByText('Manual Item')).toBeInTheDocument(); @@ -425,7 +441,9 @@ describe('When RunDetailsDialog is rendered', () => { render(); // Act — open dialog - fireEvent.click(screen.getByText('#1').closest('tr')!); + const row = screen.getByText('#1').closest('tr'); + expect(row).toBeTruthy(); + fireEvent.click(row as HTMLElement); // Assert — skeleton elements have specific class const dialog = screen.getByText('Run #1').closest('[role="dialog"]'); diff --git a/src/components/terror-zone/TerrorZoneConfiguration.tsx b/src/components/terror-zone/TerrorZoneConfiguration.tsx index 4e319cc..ea2574d 100644 --- a/src/components/terror-zone/TerrorZoneConfiguration.tsx +++ b/src/components/terror-zone/TerrorZoneConfiguration.tsx @@ -29,7 +29,7 @@ import { translations } from '@/i18n/translations'; export function TerrorZoneConfiguration() { const { t } = useTranslation(); const [zones, setZones] = useState([]); - const [config, setConfig] = useState>({}); + const [config, setConfig] = useState>({}); const [searchTerm, setSearchTerm] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -77,7 +77,7 @@ export function TerrorZoneConfiguration() { }, [loadData]); const handleZoneToggle = useCallback( - async (zoneId: number, enabled: boolean) => { + async (zoneId: string, enabled: boolean) => { try { setIsSaving(true); const newConfig = { ...config, [zoneId]: enabled }; @@ -102,7 +102,7 @@ export function TerrorZoneConfiguration() { const handleEnableAll = useCallback(async () => { try { setIsSaving(true); - const newConfig: Record = {}; + const newConfig: Record = {}; zones.forEach((zone) => { newConfig[zone.id] = true; }); @@ -124,7 +124,7 @@ export function TerrorZoneConfiguration() { const handleDisableAll = useCallback(async () => { try { setIsSaving(true); - const newConfig: Record = {}; + const newConfig: Record = {}; zones.forEach((zone) => { newConfig[zone.id] = false; }); @@ -371,7 +371,7 @@ export function TerrorZoneConfiguration() { handleZoneToggle(zone.id, checked)} + onCheckedChange={(checked: boolean) => handleZoneToggle(zone.id, checked)} disabled={isSaving || !validationStatus.valid} />