Files
TUIkit/Tests/TUIkitTests/NotificationModifierTests.swift
T
2026-02-08 14:34:42 +01:00

234 lines
7.6 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 🖥 TUIKit Terminal UI Kit for Swift
// NotificationModifierTests.swift
//
// Created by LAYERED.work
// License: MIT
import Foundation
import Testing
@testable import TUIkit
@MainActor
@Suite("Notification Tests", .serialized)
struct NotificationTests {
/// Creates a test context with a fresh TUIContext.
private func testContext(
width: Int = 80,
height: Int = 24,
identity: ViewIdentity = ViewIdentity(path: "Root")
) -> RenderContext {
RenderContext(
availableWidth: width,
availableHeight: height,
tuiContext: TUIContext(),
identity: identity
)
}
// MARK: - Fade Timing
@Test("Opacity is 0 at elapsed 0")
func opacityAtStart() {
let opacity = NotificationTiming.opacity(elapsed: 0.0, visibleDuration: 3.0)
#expect(opacity == 0.0)
}
@Test("Opacity ramps to 1.0 at end of fade-in")
func opacityAfterFadeIn() {
let fadeIn = NotificationTiming.fadeInDuration
let opacity = NotificationTiming.opacity(elapsed: fadeIn, visibleDuration: 3.0)
#expect(opacity == 1.0)
}
@Test("Opacity is 1.0 during visible phase")
func opacityDuringVisible() {
let fadeIn = NotificationTiming.fadeInDuration
let opacity = NotificationTiming.opacity(elapsed: fadeIn + 1.5, visibleDuration: 3.0)
#expect(opacity == 1.0)
}
@Test("Opacity drops during fade-out phase")
func opacityDuringFadeOut() {
let fadeIn = NotificationTiming.fadeInDuration
let visible = 3.0
let halfFadeOut = NotificationTiming.fadeOutDuration / 2
let opacity = NotificationTiming.opacity(elapsed: fadeIn + visible + halfFadeOut, visibleDuration: visible)
#expect(opacity > 0.0)
#expect(opacity < 1.0)
}
@Test("Opacity is 0 after full animation completes")
func opacityAfterDismiss() {
let total = NotificationTiming.fadeInDuration + 3.0 + NotificationTiming.fadeOutDuration + 0.1
let opacity = NotificationTiming.opacity(elapsed: total, visibleDuration: 3.0)
#expect(opacity == 0.0)
}
@Test("Fade-in is a linear ramp from 0 to 1")
func fadeInIsLinear() {
let fadeIn = NotificationTiming.fadeInDuration
let quarter = NotificationTiming.opacity(elapsed: fadeIn * 0.25, visibleDuration: 3.0)
let half = NotificationTiming.opacity(elapsed: fadeIn * 0.5, visibleDuration: 3.0)
let threeQuarter = NotificationTiming.opacity(elapsed: fadeIn * 0.75, visibleDuration: 3.0)
#expect(quarter > 0.0)
#expect(half > quarter)
#expect(threeQuarter > half)
#expect(threeQuarter < 1.0)
#expect(abs(half - 0.5) < 0.01)
}
// MARK: - Word Wrap
@Test("Short text stays on one line")
func wordWrapShortText() {
let lines = NotificationTiming.wordWrap("Hello world", maxWidth: 40)
#expect(lines == ["Hello world"])
}
@Test("Long text wraps at word boundaries")
func wordWrapLongText() {
let text = "This is a longer message that should wrap across multiple lines"
let lines = NotificationTiming.wordWrap(text, maxWidth: 20)
#expect(lines.count > 1)
for line in lines {
#expect(line.count <= 20)
}
}
@Test("Single word longer than maxWidth gets its own line")
func wordWrapLongWord() {
let lines = NotificationTiming.wordWrap("Supercalifragilistic", maxWidth: 10)
#expect(lines == ["Supercalifragilistic"])
}
@Test("Empty text returns single empty line")
func wordWrapEmpty() {
let lines = NotificationTiming.wordWrap("", maxWidth: 40)
#expect(lines == [""])
}
// MARK: - NotificationService
@Test("Post adds an entry to the service")
func postAddsEntry() {
let service = NotificationService()
service.post("Hello")
let entries = service.activeEntries()
#expect(entries.count == 1)
#expect(entries[0].message == "Hello")
}
@Test("Multiple posts stack entries in order")
func multiplePostsStack() {
let service = NotificationService()
service.post("First")
service.post("Second")
service.post("Third")
let entries = service.activeEntries()
#expect(entries.count == 3)
#expect(entries[0].message == "First")
#expect(entries[1].message == "Second")
#expect(entries[2].message == "Third")
}
@Test("Clear removes all entries")
func clearRemovesAll() {
let service = NotificationService()
service.post("One")
service.post("Two")
service.clear()
let entries = service.activeEntries()
#expect(entries.isEmpty)
}
@Test("Expired entries are pruned by activeEntries")
func expiredEntriesPruned() {
let service = NotificationService()
// Post with a very short duration so it expires almost immediately.
service.post("Quick", duration: 0.0)
// Wait slightly longer than fade-in + fade-out.
Thread.sleep(forTimeInterval: NotificationTiming.fadeInDuration + NotificationTiming.fadeOutDuration + 0.05)
let entries = service.activeEntries()
#expect(entries.isEmpty)
}
// MARK: - NotificationHostModifier Rendering
@Test("Host renders base content when no notifications are active")
func hostRendersBaseWhenEmpty() {
let context = testContext()
let service = NotificationService()
var env = context.environment
env.notificationService = service
let view = NotificationHostModifier(
content: Text("Base"),
width: 40
)
let buffer = renderToBuffer(view, context: context.withEnvironment(env))
#expect(buffer.lines[0].stripped == "Base")
#expect(buffer.height == 1)
}
@Test("Host renders notification overlay when entries exist")
func hostRendersNotification() {
let context = testContext()
let service = NotificationService()
service.post("Alert!")
var env = context.environment
env.notificationService = service
let view = NotificationHostModifier(
content: Text("Base"),
width: 40
)
let buffer = renderToBuffer(view, context: context.withEnvironment(env))
let joined = buffer.lines.joined()
#expect(joined.contains("Alert!"))
}
@Test(".notificationHost() modifier compiles and renders correctly")
func modifierExtension() {
let context = testContext()
let service = NotificationService()
service.post("Done!")
var env = context.environment
env.notificationService = service
let view = Text("Content").notificationHost()
let buffer = renderToBuffer(view, context: context.withEnvironment(env))
let joined = buffer.lines.joined()
#expect(joined.contains("Done!"))
}
@Test("Multiple notifications stack vertically")
func multipleNotificationsStack() {
let context = testContext(width: 80, height: 24)
let service = NotificationService()
service.post("First")
service.post("Second")
var env = context.environment
env.notificationService = service
let view = Text("Base").notificationHost()
let buffer = renderToBuffer(view, context: context.withEnvironment(env))
let joined = buffer.lines.joined()
#expect(joined.contains("First"))
#expect(joined.contains("Second"))
// Both notifications should be in the buffer, stacked.
#expect(buffer.height > 3)
}
}