portico/Tests/PorticoTests/ClipboardTests.swift

95 lines
3.4 KiB
Swift

import Testing
import Gtk
import Portico
// MARK: - GLib pump
@_silgen_name("g_main_context_iteration")
private nonisolated func test_g_main_context_iteration(
_ context: UnsafeMutableRawPointer?, _ mayBlock: Int32
) -> Int32
/// Pumps the default main context, yielding each turn so the concurrently
/// running read task can reach its next suspension point, until `condition`
/// holds or `timeout` elapses.
///
/// `Clipboard`'s async reads/writes complete via a `GAsyncReadyCallback`
/// dispatched from an idle source on the default main context - nothing
/// pumps that context automatically in a test process, so the read task and
/// this loop must interleave explicitly.
@MainActor private func asyncPump(
until condition: () -> Bool, timeout: Duration = .seconds(2)
) async -> Bool {
var expired = false
let watchdog = Timeout(interval: timeout, repeats: false) { expired = true }
defer { watchdog.remove() }
while !condition() && !expired {
_ = test_g_main_context_iteration(nil, 0)
await _Concurrency.Task.yield()
}
return expired
}
/// Captures the outcome of a concurrently running `Clipboard.text()` read.
@MainActor private final class TextReadBox {
var outcome: Result<String?, Error>?
}
/// Starts `Clipboard.general.text()` on its own task and pumps the main
/// context until it completes.
///
/// - Returns: The read text.
/// - Throws: Whatever the underlying read throws, or an error if the pump
/// watchdog expired first.
@MainActor private func pumpedGeneralText() async throws -> String? {
let box = TextReadBox()
_Concurrency.Task {
do {
box.outcome = .success(try await Clipboard.general.text())
} catch {
box.outcome = .failure(error)
}
}
let timedOut = await asyncPump(until: { box.outcome != nil })
#expect(!timedOut, "clipboard read timed out - no GAsyncReadyCallback fired")
return try box.outcome!.get()
}
@MainActor @Suite(.serialized) struct ClipboardTests {
@Test func generalAndPrimaryAreDistinctAndStable() {
#expect(Clipboard.general === Clipboard.general)
#expect(Clipboard.primary === Clipboard.primary)
#expect(Clipboard.general !== Clipboard.primary)
}
@Test func environmentDefaultsToGeneralAndOverridesToPrimary() {
#expect(EnvironmentValues().clipboard === Clipboard.general)
var scoped = EnvironmentValues()
scoped.clipboard = .primary
#expect(scoped.clipboard === Clipboard.primary)
}
@Test func setTextThenReadRoundTrips() async throws {
guard Gtk.initCheck() else { return }
#expect(Clipboard.general.setText("portico"))
#expect(Clipboard.general.contains(mimeType: "text/plain;charset=utf-8"))
#expect(Clipboard.general.isLocal)
let result = try await pumpedGeneralText()
#expect(result == "portico")
}
@Test func clearEmptiesTheClipboard() async throws {
guard Gtk.initCheck() else { return }
#expect(Clipboard.general.setText("to be cleared"))
#expect(Clipboard.general.clear())
let result = try await pumpedGeneralText()
#expect(result == nil)
}
@Test func setTextIncrementsChangeCount() {
guard Gtk.initCheck() else { return }
let before = Clipboard.general.changeCount
#expect(Clipboard.general.setText("increment-check"))
#expect(Clipboard.general.changeCount > before)
}
}