Layout-Engine mit vier Spalten, testgetrieben. Widgets werden nicht stur hintereinander gesetzt, sondern jeweils an die erste Stelle, an die sie passen. Der Unterschied zeigt sich neben einem 2x2-Widget: dort bleiben rechts zwei 1x1-Plätze frei, die ein reines Anhängen dauerhaft leer ließe. Persistenz unterscheidet zwei Fälle, die gleich aussehen und es nicht sind. Eine leere Layout-Datei ist eine Aussage — der Nutzer hat alle Widgets entfernt. Eine Datei, aus der nach dem Filtern unbekannter Kennungen nichts übrig bleibt, ist dagegen ein Zeichen, dass sich die Kennungen geändert haben; dort wäre ein leeres Panel eine stille Fehlfunktion, also greift das Standardlayout. Beschädigte und fehlende Dateien führen beide zum Standardlayout statt zu einem Absturz oder einem leeren Panel. Die Panelgröße folgt dem Layout statt umgekehrt. Positioniert wird über LayoutEngine.frame, nicht über LazyVGrid — das kann keine Kacheln über zwei Zeilen führen, und genau das braucht der Mini-Monat. Platzhalter-Widgets für alle 13 geplanten Karten. Sie tragen die endgültigen Kennungen, damit gespeicherte Layouts weitergelten, wenn die echten Widgets sie Phase für Phase ersetzen. 66 Tests grün.
195 lines
6.7 KiB
Swift
195 lines
6.7 KiB
Swift
import Testing
|
||
import CoreGraphics
|
||
@testable import OnyxWidgetKit
|
||
|
||
// Das Panel-Raster hat vier Spalten. Widgets werden in der gespeicherten
|
||
// Reihenfolge eingesetzt, jedes an die erste Stelle, an die es passt. Bleibt
|
||
// links eine Lücke, füllt sie das nächste Widget, das hineinpasst — sonst
|
||
// stünden im Panel dauernd Löcher, die niemand füllen kann.
|
||
|
||
@Suite("Layout-Engine")
|
||
struct LayoutEngineTests {
|
||
|
||
private func placement(_ widget: String, _ size: WidgetSize) -> WidgetPlacement {
|
||
WidgetPlacement(widgetID: widget, size: size)
|
||
}
|
||
|
||
// MARK: - Grundlegendes Setzen
|
||
|
||
@Test("Ein kleines Widget landet oben links")
|
||
func singleSmall() {
|
||
let resolved = LayoutEngine.resolve([placement("cpu", .small)])
|
||
|
||
#expect(resolved.count == 1)
|
||
#expect(resolved[0].column == 0)
|
||
#expect(resolved[0].row == 0)
|
||
}
|
||
|
||
@Test("Vier kleine Widgets füllen genau die erste Zeile")
|
||
func fourSmallFillFirstRow() {
|
||
let resolved = LayoutEngine.resolve((0..<4).map { placement("w\($0)", .small) })
|
||
|
||
#expect(resolved.map(\.row) == [0, 0, 0, 0])
|
||
#expect(resolved.map(\.column) == [0, 1, 2, 3])
|
||
}
|
||
|
||
@Test("Das fünfte kleine Widget rutscht in die zweite Zeile")
|
||
func fifthWraps() {
|
||
let resolved = LayoutEngine.resolve((0..<5).map { placement("w\($0)", .small) })
|
||
|
||
#expect(resolved[4].row == 1)
|
||
#expect(resolved[4].column == 0)
|
||
}
|
||
|
||
@Test("Ein breites Widget belegt alle vier Spalten")
|
||
func wideTakesFullRow() {
|
||
let resolved = LayoutEngine.resolve([placement("media", .wide)])
|
||
|
||
#expect(resolved[0].column == 0)
|
||
#expect(resolved[0].columnSpan == 4)
|
||
}
|
||
|
||
@Test("Ein hohes Widget belegt zwei Zeilen")
|
||
func largeSpansTwoRows() {
|
||
let resolved = LayoutEngine.resolve([placement("calendar", .large)])
|
||
|
||
#expect(resolved[0].columnSpan == 2)
|
||
#expect(resolved[0].rowSpan == 2)
|
||
}
|
||
|
||
// MARK: - Packen
|
||
|
||
@Test("Ein breites Widget passt nicht neben ein kleines und beginnt eine neue Zeile")
|
||
func wideStartsNewRowWhenBlocked() {
|
||
let resolved = LayoutEngine.resolve([
|
||
placement("cpu", .small),
|
||
placement("media", .wide),
|
||
])
|
||
|
||
#expect(resolved[0].row == 0)
|
||
#expect(resolved[1].row == 1)
|
||
#expect(resolved[1].column == 0)
|
||
}
|
||
|
||
@Test("Kleine Widgets füllen die Lücke neben einem hohen Widget")
|
||
func smallFillsGapBesideLarge() {
|
||
// Ein 2×2-Widget links lässt rechts zwei freie 1×1-Plätze in Zeile 0
|
||
// und zwei in Zeile 1 — die dürfen nicht verloren gehen.
|
||
let resolved = LayoutEngine.resolve([
|
||
placement("calendar", .large),
|
||
placement("cpu", .small),
|
||
placement("ram", .small),
|
||
placement("gpu", .small),
|
||
])
|
||
|
||
#expect(resolved[1].row == 0)
|
||
#expect(resolved[1].column == 2)
|
||
#expect(resolved[2].row == 0)
|
||
#expect(resolved[2].column == 3)
|
||
#expect(resolved[3].row == 1)
|
||
#expect(resolved[3].column == 2)
|
||
}
|
||
|
||
@Test("Belegte Felder werden nie doppelt vergeben")
|
||
func noOverlaps() {
|
||
let resolved = LayoutEngine.resolve([
|
||
placement("a", .large), placement("b", .medium), placement("c", .small),
|
||
placement("d", .wide), placement("e", .small), placement("f", .large),
|
||
])
|
||
|
||
var occupied = Set<GridCell>()
|
||
for item in resolved {
|
||
for row in item.row..<(item.row + item.rowSpan) {
|
||
for column in item.column..<(item.column + item.columnSpan) {
|
||
let cell = GridCell(row: row, column: column)
|
||
#expect(!occupied.contains(cell), "Feld \(cell) doppelt belegt")
|
||
occupied.insert(cell)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Test("Nichts ragt über den rechten Rand hinaus")
|
||
func nothingOverflowsRight() {
|
||
let resolved = LayoutEngine.resolve([
|
||
placement("a", .small), placement("b", .medium),
|
||
placement("c", .large), placement("d", .wide),
|
||
])
|
||
|
||
for item in resolved {
|
||
#expect(item.column + item.columnSpan <= LayoutEngine.columns)
|
||
}
|
||
}
|
||
|
||
@Test("Die gespeicherte Reihenfolge bleibt erhalten")
|
||
func orderIsStable() {
|
||
let placements = [placement("a", .small), placement("b", .wide), placement("c", .small)]
|
||
let resolved = LayoutEngine.resolve(placements)
|
||
|
||
#expect(resolved.map(\.placement.widgetID) == ["a", "b", "c"])
|
||
}
|
||
|
||
@Test("Leeres Layout ergibt kein Ergebnis und keinen Absturz")
|
||
func emptyLayout() {
|
||
#expect(LayoutEngine.resolve([]).isEmpty)
|
||
}
|
||
|
||
// MARK: - Panelgröße
|
||
|
||
@Test("Panelgröße folgt der belegten Zeilenzahl, nicht der Widgetzahl")
|
||
func panelSizeFollowsRows() {
|
||
let oneRow = LayoutEngine.panelSize(for: LayoutEngine.resolve([
|
||
placement("a", .small), placement("b", .small),
|
||
]))
|
||
let twoRows = LayoutEngine.panelSize(for: LayoutEngine.resolve(
|
||
(0..<5).map { placement("w\($0)", .small) }))
|
||
|
||
#expect(twoRows.height > oneRow.height)
|
||
// Die Breite steht fest: vier Spalten, immer.
|
||
#expect(twoRows.width == oneRow.width)
|
||
}
|
||
|
||
@Test("Ein leeres Panel hat trotzdem eine sinnvolle Mindestgröße")
|
||
func emptyPanelHasMinimumSize() {
|
||
let size = LayoutEngine.panelSize(for: [])
|
||
|
||
#expect(size.width > 0)
|
||
#expect(size.height > 0)
|
||
}
|
||
|
||
@Test("Ein hohes Widget allein ergibt zwei Zeilen Höhe")
|
||
func largeAloneIsTwoRowsTall() {
|
||
let large = LayoutEngine.panelSize(for: LayoutEngine.resolve([placement("a", .large)]))
|
||
let small = LayoutEngine.panelSize(for: LayoutEngine.resolve([placement("a", .small)]))
|
||
|
||
#expect(large.height > small.height)
|
||
}
|
||
|
||
// MARK: - Rahmen im Panel
|
||
|
||
@Test("Ein Widget in Spalte 2 sitzt weiter rechts als eines in Spalte 0")
|
||
func framesFollowColumns() {
|
||
let resolved = LayoutEngine.resolve([
|
||
placement("a", .medium), placement("b", .medium),
|
||
])
|
||
let panel = LayoutEngine.panelSize(for: resolved)
|
||
|
||
let first = LayoutEngine.frame(for: resolved[0], in: panel)
|
||
let second = LayoutEngine.frame(for: resolved[1], in: panel)
|
||
|
||
#expect(second.minX > first.minX)
|
||
#expect(first.minY == second.minY)
|
||
}
|
||
|
||
@Test("Ein breites Widget ist im Rahmen wirklich breiter als ein kleines")
|
||
func wideFrameIsWider() {
|
||
let wide = LayoutEngine.resolve([placement("a", .wide)])
|
||
let small = LayoutEngine.resolve([placement("a", .small)])
|
||
|
||
let wideFrame = LayoutEngine.frame(for: wide[0], in: LayoutEngine.panelSize(for: wide))
|
||
let smallFrame = LayoutEngine.frame(for: small[0], in: LayoutEngine.panelSize(for: small))
|
||
|
||
#expect(wideFrame.width > smallFrame.width)
|
||
}
|
||
}
|