Files
onyx/Packages/OnyxKit/Tests/OnyxWidgetKitTests/LayoutEngineTests.swift
Guido Schmit 8e0629d3a5 Phase 2 (Teil 1): Widget-Raster, Layout-Engine, Persistenz
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.
2026-08-10 17:32:48 +02:00

195 lines
6.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}