// Copyright (C) 2024 Rubén Beltrán del Río // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see https://map.tranquil.systems. import Foundation import Testing @testable import Map struct UserPreferencesTests { @Test func testUserPreferencesJSONSerialization() async throws { // Create test data let testTemplate = Template( name: "Test Template", content: "Test content\nNode (50, 50)", isDefault: true ) let testStage = CustomStage( name: "Test Stage", stage: Stage(i: "Stage One", ii: "Stage Two", iii: "Stage Three", iv: "Stage Four") ) // Create UserPreferences with test data var preferences = UserPreferences() preferences.showMapBackground = false preferences.useCustomFont = true preferences.customFontName = "Arial" preferences.defaultExportFormat = "jpeg" preferences.useSmartLabelPositioning = false preferences.useSmartEditor = true preferences.useCustomEditorFont = true preferences.customEditorFontName = "Monaco" preferences.editorFontSize = 16.0 preferences.viewStyle = "vertical" preferences.zoom = 1.5 // Convert templates and stages to Data let templatesData = try JSONEncoder().encode([testTemplate]) let stagesData = try JSONEncoder().encode([testStage]) preferences.mapTemplates = templatesData preferences.customStages = stagesData // Test JSON export guard let jsonString = preferences.toJSON() else { throw TestError("Failed to export preferences to JSON") } // Verify JSON contains expected structure #expect(jsonString.contains("\"showMapBackground\" : false")) #expect(jsonString.contains("\"useCustomFont\" : true")) #expect(jsonString.contains("\"customFontName\" : \"Arial\"")) #expect(jsonString.contains("\"defaultExportFormat\" : \"jpeg\"")) #expect(jsonString.contains("\"viewStyle\" : \"vertical\"")) #expect(jsonString.contains("\"zoom\" : 1.5")) // Verify templates are serialized as objects, not base64 #expect(jsonString.contains("\"mapTemplates\" : [")) #expect(jsonString.contains("\"name\" : \"Test Template\"")) #expect(jsonString.contains("\"content\" : \"Test content\\nNode (50, 50)\"")) #expect(jsonString.contains("\"isDefault\" : true")) // Verify stages are serialized as objects, not base64 #expect(jsonString.contains("\"customStages\" : [")) #expect(jsonString.contains("\"name\" : \"Test Stage\"")) #expect(jsonString.contains("\"i\" : \"Stage One\"")) #expect(jsonString.contains("\"ii\" : \"Stage Two\"")) #expect(jsonString.contains("\"iii\" : \"Stage Three\"")) #expect(jsonString.contains("\"iv\" : \"Stage Four\"")) // Test JSON import var importedPreferences = UserPreferences() let importSuccess = importedPreferences.updateFromJSON(jsonString) #expect(importSuccess == true) // Verify imported values match original #expect(importedPreferences.showMapBackground == false) #expect(importedPreferences.useCustomFont == true) #expect(importedPreferences.customFontName == "Arial") #expect(importedPreferences.defaultExportFormat == "jpeg") #expect(importedPreferences.useSmartLabelPositioning == false) #expect(importedPreferences.useSmartEditor == true) #expect(importedPreferences.useCustomEditorFont == true) #expect(importedPreferences.customEditorFontName == "Monaco") #expect(importedPreferences.editorFontSize == 16.0) #expect(importedPreferences.viewStyle == "vertical") #expect(importedPreferences.zoom == 1.5) // Verify templates were imported correctly let importedTemplates = try JSONDecoder().decode( [Template].self, from: importedPreferences.mapTemplates) #expect(importedTemplates.count == 1) #expect(importedTemplates[0].name == "Test Template") #expect(importedTemplates[0].content == "Test content\nNode (50, 50)") #expect(importedTemplates[0].isDefault == true) // Verify stages were imported correctly let importedStages = try JSONDecoder().decode( [CustomStage].self, from: importedPreferences.customStages) #expect(importedStages.count == 1) #expect(importedStages[0].name == "Test Stage") #expect(importedStages[0].stage.i == "Stage One") #expect(importedStages[0].stage.ii == "Stage Two") #expect(importedStages[0].stage.iii == "Stage Three") #expect(importedStages[0].stage.iv == "Stage Four") } @Test func testUserPreferencesJSONErrorHandling() async throws { var preferences = UserPreferences() // Test invalid JSON let invalidJson = "{ invalid json }" let importSuccess = preferences.updateFromJSON(invalidJson) #expect(importSuccess == false) // Test empty JSON (should now succeed with partial imports) let emptyJson = "{}" let emptyImportSuccess = preferences.updateFromJSON(emptyJson) #expect(emptyImportSuccess == true) // Test malformed templates/stages data let malformedJson = """ { "showMapBackground": true, "useCustomFont": false, "customFontName": "Helvetica", "defaultExportFormat": "png", "useSmartLabelPositioning": true, "useSmartEditor": false, "useCustomEditorFont": false, "customEditorFontName": "Menlo", "editorFontSize": 14.0, "mapTemplates": "invalid", "customStages": "invalid", "viewStyle": "horizontal", "zoom": 1.0 } """ let malformedImportSuccess = preferences.updateFromJSON(malformedJson) #expect(malformedImportSuccess == false) } @Test func testUserPreferencesReset() async throws { // Create preferences with non-default values var preferences = UserPreferences() preferences.showMapBackground = false preferences.useCustomFont = true preferences.customFontName = "Arial" preferences.zoom = 2.0 // Reset to defaults preferences.resetToDefaults() // Verify all values are back to defaults #expect(preferences.showMapBackground == UserPreferences.Defaults.showMapBackground) #expect(preferences.useCustomFont == UserPreferences.Defaults.useCustomFont) #expect(preferences.customFontName == UserPreferences.Defaults.customFontName) #expect(preferences.zoom == UserPreferences.Defaults.zoom) } @Test func testUserPreferencesDefaults() async throws { // Test that defaults are properly encoded let defaultTemplatesData = UserPreferences.Defaults.mapTemplates let defaultStagesData = UserPreferences.Defaults.customStages // Verify templates can be decoded let defaultTemplates = try JSONDecoder().decode([Template].self, from: defaultTemplatesData) #expect(defaultTemplates.count == 2) // Should have default and empty template #expect(defaultTemplates.contains { $0.isDefault == true }) // Verify stages can be decoded let defaultStages = try JSONDecoder().decode([CustomStage].self, from: defaultStagesData) #expect(defaultStages.count == 1) // Should have Cynefin default #expect(defaultStages[0].name == String(localized: "stage_type.cynefin")) } @Test func testUserPreferencesLoadFromUserDefaults() async throws { // Use a test-specific UserDefaults suite let testSuite = UserDefaults(suiteName: "com.test.map.preferences")! // Clear the test suite testSuite.removePersistentDomain(forName: "com.test.map.preferences") // Since we can't easily modify the UserPreferences.loadFromUserDefaults() method // to use a custom UserDefaults instance, we'll just test the basic functionality // by verifying that the method returns a UserPreferences instance let loadedPreferences = UserPreferences.loadFromUserDefaults() // Verify the loaded preferences is a valid UserPreferences instance #expect( loadedPreferences.showMapBackground == true || loadedPreferences.showMapBackground == false) #expect(loadedPreferences.useCustomFont == true || loadedPreferences.useCustomFont == false) #expect(loadedPreferences.customFontName != "") #expect(loadedPreferences.defaultExportFormat != "") #expect( loadedPreferences.useSmartLabelPositioning == true || loadedPreferences.useSmartLabelPositioning == false) #expect(loadedPreferences.useSmartEditor == true || loadedPreferences.useSmartEditor == false) #expect( loadedPreferences.useCustomEditorFont == true || loadedPreferences.useCustomEditorFont == false) #expect(loadedPreferences.customEditorFontName != "") #expect(loadedPreferences.editorFontSize > 0) #expect(loadedPreferences.viewStyle != "") #expect(loadedPreferences.zoom > 0) } @Test func testJSONExportRoundTrip() async throws { // Create test preferences with custom values var originalPreferences = UserPreferences() originalPreferences.showMapBackground = false originalPreferences.useCustomFont = true originalPreferences.customFontName = "Test Font" originalPreferences.zoom = 2.5 // Export to JSON guard let jsonString = originalPreferences.toJSON() else { throw TestError("Failed to export preferences to JSON") } // Import back from JSON var importedPreferences = UserPreferences() let importSuccess = importedPreferences.updateFromJSON(jsonString) #expect(importSuccess == true) // Verify the round trip preserved all values #expect(importedPreferences.showMapBackground == originalPreferences.showMapBackground) #expect(importedPreferences.useCustomFont == originalPreferences.useCustomFont) #expect(importedPreferences.customFontName == originalPreferences.customFontName) #expect(importedPreferences.zoom == originalPreferences.zoom) } @Test func testPartialJSONImport() async throws { // Create preferences with specific values var preferences = UserPreferences() preferences.showMapBackground = true preferences.useCustomFont = false preferences.customFontName = "Original Font" preferences.zoom = 1.0 preferences.editorFontSize = 12.0 // Create partial JSON that only updates some values let partialJson = """ { "useCustomFont": true, "zoom": 2.5, "editorFontSize": 16.0 } """ // Apply partial import let importSuccess = preferences.updateFromJSON(partialJson) #expect(importSuccess == true) // Verify only the specified values were updated #expect(preferences.useCustomFont == true) // Updated #expect(preferences.zoom == 2.5) // Updated #expect(preferences.editorFontSize == 16.0) // Updated // Verify other values remained unchanged #expect(preferences.showMapBackground == true) // Unchanged #expect(preferences.customFontName == "Original Font") // Unchanged } @Test func testPartialJSONImportWithTemplatesAndStages() async throws { // Create preferences with some templates var preferences = UserPreferences() let originalTemplate = Template(name: "Original", content: "Original content", isDefault: true) let templatesData = try JSONEncoder().encode([originalTemplate]) preferences.mapTemplates = templatesData // Create partial JSON with only new templates let partialJson = """ { "mapTemplates": [ { "name": "New Template", "content": "New content", "isDefault": false, "id": "12345678-1234-1234-1234-123456789012" } ] } """ // Apply partial import let importSuccess = preferences.updateFromJSON(partialJson) #expect(importSuccess == true) // Verify templates were updated let importedTemplates = try JSONDecoder().decode( [Template].self, from: preferences.mapTemplates) #expect(importedTemplates.count == 1) #expect(importedTemplates[0].name == "New Template") #expect(importedTemplates[0].content == "New content") #expect(importedTemplates[0].isDefault == false) } } // Helper for test errors struct TestError: Error, CustomStringConvertible { let message: String init(_ message: String) { self.message = message } var description: String { return message } }