aboutsummaryrefslogtreecommitdiff
path: root/MapTests/UserPreferencesTests.swift
blob: 70ea210c69d54c7d21f885a27eeeec315ea23a38 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
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
  }
}