Alle 7 Card-Types werden gerendert und können erstellt werden. image-occlusion mit Touch-Drag-Mask-Editor (kein PencilKit — Server- Schema erlaubt nur Rechtecke), audio-front mit AVAudioPlayer und File-Picker. - MediaUploadResponse-DTO, MaskRegion-Codable mit 0..1-Coordinates - MaskRegions.parse/encode (1:1-Port aus cards-domain, Sortierung nach ID lexikographisch) - CardFieldsBuilder.imageOcclusion mit stringified-JSON-mask_regions + audioFront - CardsAPI.uploadMedia (Multipart, 25 MiB) + fetchMedia (streamed) - MediaCache actor mit LRU 200 MB (contentModificationDate-Eviction) - mediaCache Environment-Key - RemoteImage + AudioPlayerButton SwiftUI-Views - CardRenderer: imageOcclusion (Mask-Overlay über RemoteImage) + audioFront (AudioPlayerButton + back-Text auf Flip) - MaskEditorView: Touch-Drag-Rechteck, Label-Edit, Delete - CardEditorView erweitert: PhotosPicker für Image, fileImporter für Audio, Magic-Byte-MIME-Detection - 6 neue Tests für MaskRegions (30 Total grün) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
79 lines
2.8 KiB
Swift
79 lines
2.8 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CardsNative
|
|
|
|
@Suite("MaskRegions")
|
|
struct MaskRegionsTests {
|
|
@Test("Parsed Liste sortiert nach ID lexikographisch")
|
|
func parseSortsByIdLexically() {
|
|
let json = """
|
|
[
|
|
{"id":"m003","x":0.1,"y":0.1,"w":0.2,"h":0.2,"label":"C"},
|
|
{"id":"m001","x":0,"y":0,"w":0.1,"h":0.1,"label":"A"},
|
|
{"id":"m002","x":0.5,"y":0.5,"w":0.3,"h":0.3}
|
|
]
|
|
"""
|
|
let regions = MaskRegions.parse(json)
|
|
#expect(regions.count == 3)
|
|
#expect(regions[0].id == "m001")
|
|
#expect(regions[1].id == "m002")
|
|
#expect(regions[2].id == "m003")
|
|
#expect(regions[2].label == "C")
|
|
#expect(regions[1].label == nil)
|
|
}
|
|
|
|
@Test("Bei Parse-Fehler → leere Liste")
|
|
func parseInvalidReturnsEmpty() {
|
|
#expect(MaskRegions.parse("[}").isEmpty)
|
|
#expect(MaskRegions.parse("{}").isEmpty)
|
|
#expect(MaskRegions.parse("").isEmpty)
|
|
}
|
|
|
|
@Test("region(forSubIndex:) mappt aufsteigend")
|
|
func subIndexLookup() {
|
|
let json = """
|
|
[{"id":"b","x":0,"y":0,"w":0.1,"h":0.1},
|
|
{"id":"a","x":0,"y":0,"w":0.2,"h":0.2}]
|
|
"""
|
|
#expect(MaskRegions.region(for: json, subIndex: 0)?.id == "a")
|
|
#expect(MaskRegions.region(for: json, subIndex: 1)?.id == "b")
|
|
#expect(MaskRegions.region(for: json, subIndex: 2) == nil)
|
|
}
|
|
|
|
@Test("Encode-Roundtrip")
|
|
func encodeRoundtrip() {
|
|
let original = [
|
|
MaskRegion(id: "m1", x: 0.1, y: 0.2, w: 0.3, h: 0.4, label: "test"),
|
|
MaskRegion(id: "m2", x: 0.5, y: 0.6, w: 0.2, h: 0.2, label: nil),
|
|
]
|
|
let encoded = MaskRegions.encode(original)
|
|
let parsed = MaskRegions.parse(encoded)
|
|
#expect(parsed.count == 2)
|
|
#expect(parsed[0].id == "m1")
|
|
#expect(parsed[0].label == "test")
|
|
#expect(parsed[1].label == nil)
|
|
}
|
|
|
|
@Test("CardFieldsBuilder.imageOcclusion produziert korrekte Felder")
|
|
func builderImageOcclusion() {
|
|
let regions = [MaskRegion(id: "m1", x: 0, y: 0, w: 0.5, h: 0.5, label: "x")]
|
|
let fields = CardFieldsBuilder.imageOcclusion(
|
|
imageRef: "media_123",
|
|
regions: regions,
|
|
note: "Hinweis"
|
|
)
|
|
#expect(fields["image_ref"] == "media_123")
|
|
#expect(fields["note"] == "Hinweis")
|
|
let reparsed = MaskRegions.parse(fields["mask_regions"] ?? "")
|
|
#expect(reparsed.count == 1)
|
|
#expect(reparsed[0].id == "m1")
|
|
}
|
|
|
|
@Test("CardFieldsBuilder.audioFront produziert korrekte Felder")
|
|
func builderAudioFront() {
|
|
let fields = CardFieldsBuilder.audioFront(audioRef: "audio_456", back: "Antwort")
|
|
#expect(fields["audio_ref"] == "audio_456")
|
|
#expect(fields["back"] == "Antwort")
|
|
#expect(fields.count == 2)
|
|
}
|
|
}
|