From c21a68c807dcb84b4ce910ee0f73238a8019ffe9 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Tue, 8 Jul 2025 21:21:08 +0200 Subject: Allow export/import of preferences --- MapTests/UserPreferencesTests.swift | 327 ++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 MapTests/UserPreferencesTests.swift (limited to 'MapTests/UserPreferencesTests.swift') diff --git a/MapTests/UserPreferencesTests.swift b/MapTests/UserPreferencesTests.swift new file mode 100644 index 0000000..70ea210 --- /dev/null +++ b/MapTests/UserPreferencesTests.swift @@ -0,0 +1,327 @@ +// 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 + } +} -- cgit