240 lines
9.7 KiB
Swift
240 lines
9.7 KiB
Swift
//
|
|
// BinderExporterTest.swift
|
|
// MonsterCardsTests
|
|
//
|
|
// Unit tests for BinderExporter — mirrors Android's BinderImportExportTest.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import MonsterCards
|
|
|
|
final class BinderExporterTest: XCTestCase {
|
|
|
|
// MARK: - Export with collection name
|
|
|
|
func testExportBinder_singleCollection_createsValidJson() throws {
|
|
let monster = sampleMonster()
|
|
|
|
let json = BinderExporter.exportBinder(collectionName: "Goblins", monsters: [monster])
|
|
|
|
// Verify root structure.
|
|
guard let data = json.data(using: .utf8),
|
|
let root = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let collections = root["collections"] as? [[String: Any]],
|
|
let col = collections.first,
|
|
let cards = col["cards"] as? [[String: Any]] else {
|
|
XCTFail("JSON structure invalid")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(root["$schema"], "https://majinnaibu.com/schemas/binder.schema.json")
|
|
XCTAssertEqual(root["schemaVersion"] as? Int, 1)
|
|
XCTAssertEqual(col["name"] as? String, "Goblins")
|
|
XCTAssertEqual(cards.count, 1)
|
|
|
|
// Verify monster card fields.
|
|
let card = cards[0]
|
|
XCTAssertEqual(card["$schema"] as? String, "https://majinnaibu.com/schemas/monster-card.schema.json")
|
|
XCTAssertEqual(card["schemaVersion"] as? Int, 1)
|
|
XCTAssertEqual(card["id"] as? String, monster.id?.uuidString ?? "")
|
|
XCTAssertEqual(card["name"] as? String, "Goblin Warrior")
|
|
XCTAssertEqual(card["size"] as? String, "Small")
|
|
XCTAssertEqual(card["type"] as? String, "humanoid")
|
|
}
|
|
|
|
func testExportBinder_customId_preservesIt() throws {
|
|
let monster = sampleMonster()
|
|
let customId = UUID()
|
|
|
|
let json = BinderExporter.exportBinder(collectionName: "", monsters: [monster], idOverride: customId)
|
|
|
|
guard let data = json.data(using: .utf8),
|
|
let root = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let collections = root["collections"] as? [[String: Any]],
|
|
let cards = collections.first["cards"] as? [[String: Any]] else {
|
|
XCTFail("JSON structure invalid")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(cards[0]["id"] as? String, customId.uuidString)
|
|
}
|
|
|
|
// MARK: - Export with full backup (dashboard)
|
|
|
|
func testExportFullBackup_withDashboard_includesDashboardField() throws {
|
|
let col = BinderExport.CollectionExport(
|
|
id: "test-col-id",
|
|
name: "Test Collection",
|
|
description: "A test collection",
|
|
cards: [MonsterExport.makeDictionary(monster: sampleMonster())]
|
|
)
|
|
|
|
let json = BinderExporter.exportFullBackup(collections: [col], dashboardMonsters: [sampleMonster()])
|
|
|
|
guard let data = json.data(using: .utf8),
|
|
let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
XCTFail("JSON structure invalid")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(root["$schema"] as? String, "https://majinnaibu.com/schemas/binder.schema.json")
|
|
|
|
// Should have a dashboard field with at least 1 entry.
|
|
XCTAssertTrue(root["dashboard"] is NSArray || root["dashboard"] is [[String: Any]])
|
|
}
|
|
|
|
func testExportFullBackup_withoutDashboard_excludesDashboardField() throws {
|
|
let col = BinderExport.CollectionExport(
|
|
id: "uuid-test",
|
|
name: "No Dash",
|
|
description: "",
|
|
cards: []
|
|
)
|
|
|
|
let json = BinderExporter.exportFullBackup(collections: [col], dashboardMonsters: nil)
|
|
|
|
XCTAssertFalse(json.contains("\"dashboard\""), "Should not contain 'dashboard' when none provided")
|
|
}
|
|
|
|
// MARK: - Ability score mapping
|
|
|
|
func testExportBinder_abilityScores_matchJsonKeys() throws {
|
|
let monster = sampleMonster()
|
|
|
|
let json = BinderExporter.exportBinder(collectionName: "", monsters: [monster])
|
|
|
|
guard let data = json.data(using: .utf8),
|
|
let root = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let collections = root["collections"] as? [[String: Any]],
|
|
let cards = collections.first["cards"] as? [[String: Any]] else {
|
|
XCTFail("JSON structure invalid")
|
|
return
|
|
}
|
|
|
|
let monsterJson = cards[0]
|
|
|
|
// Verify ability score keys and values.
|
|
XCTAssertEqual(monsterJson["strengthScore"] as? Int64, monster.strengthScore)
|
|
XCTAssertEqual(monsterJson["dexterityScore"] as? Int64, monster.dexterityScore)
|
|
XCTAssertEqual(monsterJson["constitutionScore"] as? Int64, monster.constitutionScore)
|
|
XCTAssertEqual(monsterJson["intelligenceScore"] as? Int64, monster.intelligenceScore)
|
|
XCTAssertEqual(monsterJson["wisdomScore"] as? Int64, monster.wisdomScore)
|
|
XCTAssertEqual(monsterJson["charismaScore"] as? Int64, monster.charismaScore)
|
|
|
|
// Verify saving throw keys exist.
|
|
XCTAssertNotNil(monsterJson["strengthSavingThrowProficiency"])
|
|
XCTAssertNotNil(monsterJson["strengthSavingThrowAdvantage"])
|
|
XCTAssertNotNil(monsterJson["dexteritySavingThrowProficiency"])
|
|
}
|
|
|
|
// MARK: - Round-trip (export then verify it would import)
|
|
|
|
func testExportRoundTrip_matchesImportExportTest_expectancy() throws {
|
|
// Mirror the Android BinderImportExportTest.testExportAndImportBinder expectations.
|
|
let monster = sampleMonster()
|
|
let json = BinderExporter.exportBinder(collectionName: "Goblins", monsters: [monster])
|
|
|
|
// Deserialize to verify shape (simulates what the Android BinderImporter would read).
|
|
guard let data = json.data(using: .utf8),
|
|
let root = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let collections = root["collections"] as? [[String: Any]],
|
|
let col = collections.first,
|
|
let cards = col["cards"] as? [[String: Any]] else {
|
|
XCTFail("Failed to parse exported JSON")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(root["schemaVersion"] as? Int, 1)
|
|
XCTAssertEqual(collections.count, 1)
|
|
XCTAssertEqual(col["name"] as? String, "Goblins")
|
|
XCTAssertEqual(cards.count, 1)
|
|
XCTAssertEqual(cards[0]["name"] as? String, "Goblin Warrior")
|
|
XCTAssertEqual(cards[0]["schemaVersion"] as? Int, 1)
|
|
}
|
|
|
|
// MARK: - All MonsterViewModel fields present
|
|
|
|
func testExportAllMonsterFields_haveValidKeys() throws {
|
|
let monster = sampleFullMonster()
|
|
|
|
let json = BinderExporter.exportBinder(collectionName: "", monsters: [monster])
|
|
|
|
guard let data = json.data(using: .utf8),
|
|
let root = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let cards = (root["collections"] as? [[String: Any]])?.first["cards"] as? [[String: Any]] else {
|
|
XCTFail("JSON structure invalid")
|
|
return
|
|
}
|
|
|
|
let m = cards[0]
|
|
|
|
// Verify all expected keys are present.
|
|
let expectedKeys: [String] = [
|
|
"$schema", "schemaVersion", "id", "name", "size", "type", "subtype",
|
|
"alignment",
|
|
"strengthScore", "dexterityScore", "constitutionScore", "intelligenceScore",
|
|
"wisdomScore", "charismaScore",
|
|
"strengthSavingThrowProficiency", "strengthSavingThrowAdvantage",
|
|
"dexteritySavingThrowProficiency", "dexteritySavingThrowAdvantage",
|
|
"constitutionSavingThrowProficiency", "constitutionSavingThrowAdvantage",
|
|
"intelligenceSavingThrowProficiency", "intelligenceSavingThrowAdvantage",
|
|
"wisdomSavingThrowProficiency", "wisdomSavingThrowAdvantage",
|
|
"charismaSavingThrowProficiency", "charismaSavingThrowAdvantage",
|
|
"armorType", "shieldBonus", "naturalArmorBonus", "otherArmorDescription",
|
|
"hitDice", "hasCustomHP", "customHitPointsDescription",
|
|
"walkSpeed", "burrowSpeed", "climbSpeed", "flySpeed",
|
|
"canHover", "swimSpeed", "hasCustomSpeed", "customSpeedDescription",
|
|
"challengeRating", "customChallengeRatingDescription", "customProficiencyBonus",
|
|
"telepathyRange", "understandsButDescription",
|
|
"senses", "damageImmunities", "damageResistances", "damageVulnerabilities",
|
|
"conditionImmunities", "skills", "languages",
|
|
"abilities", "actions", "reactions",
|
|
"legendaryActions", "lairActions", "regionalActions",
|
|
"bonusActions"
|
|
]
|
|
|
|
for key in expectedKeys {
|
|
XCTAssertTrue(m.keys.contains(key), "Missing key '\(key)' in export; keys found: \(m.keys.sorted())")
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func sampleMonster() -> MonsterViewModel {
|
|
let monster = MonsterViewModel()
|
|
monster.name = "Goblin Warrior"
|
|
monster.size = "Small"
|
|
monster.type = "humanoid"
|
|
monster.subType = "goblinoid"
|
|
monster.alignment = "neutral evil"
|
|
monster.strengthScore = 8
|
|
monster.dexterityScore = 14
|
|
// Keep id as default (not set).
|
|
return monster
|
|
}
|
|
|
|
private func sampleFullMonster() -> MonsterViewModel {
|
|
let m = sampleMonster()
|
|
m.hitDice = 2
|
|
m.strengthScore = 8
|
|
m.dexterityScore = 14
|
|
m.constitutionScore = 10
|
|
m.intelligenceScore = 8
|
|
m.wisdomScore = 8
|
|
m.charismaScore = 8
|
|
|
|
m.walkSpeed = 30
|
|
m.hitDice = 2
|
|
|
|
// Add some damage immunities.
|
|
let di = StringViewModel("fire")
|
|
m.damageImmunities.append(di)
|
|
|
|
// Add abilities.
|
|
m.abilities.append(AbilityViewModel("Nimble Escape", "Disengage or Hide as a bonus action."))
|
|
m.actions.append(AbilityViewModel("Scimitar", "+4 to hit, 1d6+2 slashing"))
|
|
|
|
return m
|
|
}
|
|
}
|