95 lines
3.4 KiB
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)
|
|
}
|
|
}
|