Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions TableProTests/Core/Services/ClipboardInstallGuardTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// ClipboardInstallGuardTests.swift
// TableProTests
//
// `ClipboardService.shared` is process-wide, so a test that installs a fake and never puts the
// real one back hands that fake to every test after it. A fake whose `writeText` does nothing
// turned every later copy into an empty string, and the suites that read the copy back failed
// on main for as long as the leak ran first.
//

import Foundation
import Testing

struct ClipboardInstallGuardTests {
@Test("Every test that installs a clipboard puts the real one back")
func everyInstallIsRestored() throws {
let offenders = try Self.unrestoredInstalls()
#expect(
offenders.isEmpty,
"""
Follow the install with `defer { ClipboardService.shared = NSPasteboardClipboardProvider() }`, \
or keep the previous provider and restore it in a `defer`: \(offenders.sorted())
"""
)
}

private static let install = "ClipboardService.shared = "
private static let restore = "defer { ClipboardService.shared = "
private static let reach = 3

private static func unrestoredInstalls() throws -> [String] {
let testRoot = try repoRoot().appendingPathComponent("TableProTests")
guard let enumerator = FileManager.default.enumerator(
at: testRoot,
includingPropertiesForKeys: [.isRegularFileKey]
) else { return [] }

var offenders: [String] = []
for case let url as URL in enumerator
where url.pathExtension == "swift" && url.lastPathComponent != "ClipboardInstallGuardTests.swift" {
let lines = try String(contentsOf: url, encoding: .utf8).components(separatedBy: .newlines)
for (index, line) in lines.enumerated() where line.contains(install) && !line.contains(restore) {
let window = lines[max(0, index - reach) ... min(lines.count - 1, index + reach)]
guard !window.contains(where: { $0.contains(restore) }) else { continue }
offenders.append("\(url.lastPathComponent):\(index + 1)")
}
}
return offenders
}

private static func repoRoot() throws -> URL {
var directory = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
for _ in 0 ..< 12 {
if FileManager.default.fileExists(atPath: directory.appendingPathComponent("project.yml").path) {
return directory
}
directory = directory.deletingLastPathComponent()
}
throw CocoaError(.fileNoSuchFile)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ struct RowOperationsFieldAbsenceTests {
let (manager, _) = makeManager()
let clipboard = AbsenceClipboard()
ClipboardService.shared = clipboard
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }
let tableRows = rows([["1", "Ada", .null], ["2", "Bo", .null]], absentCells: [1: [2]])

manager.copySelectedRowsToClipboard(selectedIndices: [0, 1], tableRows: tableRows, visibleColumnIndices: [2, 1])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ import Foundation
import TableProPluginKit
import Testing

private final class BinaryCopyClipboard: ClipboardProvider {
var writtenRows: String?

func readText() -> String? { nil }
func readGridRows() -> GridRowsClipboardPayload? { nil }
func writeText(_ text: String) {}
func writeCsv(_ csv: String) {}
func writeImage(_ image: NSImage) {}
func writeRows(tsv: String, html: String?, gridRows: GridRowsClipboardPayload) { writtenRows = tsv }
var hasText: Bool { false }
var hasGridRows: Bool { false }
}

@MainActor
struct RowOperationsManagerBinaryCopyTests {
private func makeManagerAndRows(binaryRow: [PluginCellValue]) -> (RowOperationsManager, TableRows) {
Expand All @@ -29,6 +42,16 @@ struct RowOperationsManagerBinaryCopyTests {
return (rowOps, tableRows)
}

/// The copy goes through `ClipboardService.shared`, and the system pasteboard reads back empty
/// on a headless runner, so the text is taken from a clipboard installed for the one copy.
private func copiedText(_ rowOps: RowOperationsManager, _ tableRows: TableRows) -> String {
let clipboard = BinaryCopyClipboard()
ClipboardService.shared = clipboard
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }
rowOps.copySelectedRowsToClipboard(selectedIndices: [0], tableRows: tableRows)
return clipboard.writtenRows ?? ""
}

@Test("Issue #1188 row copies binary cell as 0xHEX, not NULL")
func issue1188CopyAsHex() {
let bytes = Data([
Expand All @@ -41,12 +64,7 @@ struct RowOperationsManagerBinaryCopyTests {
])
let (rowOps, tableRows) = makeManagerAndRows(binaryRow: [.text("1"), .bytes(bytes)])

let pasteboard = NSPasteboard.general
pasteboard.clearContents()

rowOps.copySelectedRowsToClipboard(selectedIndices: [0], tableRows: tableRows)

let copied = pasteboard.string(forType: .string) ?? ""
let copied = copiedText(rowOps, tableRows)
#expect(copied.contains("0xD38CE566"))
#expect(!copied.contains("NULL"))
#expect(copied.contains("\t"))
Expand All @@ -56,9 +74,7 @@ struct RowOperationsManagerBinaryCopyTests {
func emptyBytesCopiesAsZeroX() {
let (rowOps, tableRows) = makeManagerAndRows(binaryRow: [.text("1"), .bytes(Data())])

NSPasteboard.general.clearContents()
rowOps.copySelectedRowsToClipboard(selectedIndices: [0], tableRows: tableRows)
let copied = NSPasteboard.general.string(forType: .string) ?? ""
let copied = copiedText(rowOps, tableRows)

#expect(copied.contains("0x") || copied.hasSuffix("\t"))
#expect(!copied.contains("NULL"))
Expand All @@ -68,9 +84,7 @@ struct RowOperationsManagerBinaryCopyTests {
func mixedNullAndBytes() {
let (rowOps, tableRows) = makeManagerAndRows(binaryRow: [.null, .bytes(Data([0xAA, 0xBB]))])

NSPasteboard.general.clearContents()
rowOps.copySelectedRowsToClipboard(selectedIndices: [0], tableRows: tableRows)
let copied = NSPasteboard.general.string(forType: .string) ?? ""
let copied = copiedText(rowOps, tableRows)

#expect(copied.contains("NULL"))
#expect(copied.contains("0xAABB"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ struct RowOperationsManagerCopyTests {
) -> String? {
let clipboard = MockClipboardProvider()
ClipboardService.shared = clipboard
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }
let tableRows = makeTableRows(rows: rows, columns: columns ?? Self.defaultColumns)
manager.copySelectedRowsToClipboard(
selectedIndices: indices,
Expand Down Expand Up @@ -139,6 +140,7 @@ struct RowOperationsManagerCopyTests {
let rows = TestFixtures.makeRows(count: 3)
let clipboard = MockClipboardProvider()
ClipboardService.shared = clipboard
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }
let tableRows = makeTableRows(rows: rows)

manager.copySelectedRowsToClipboard(
Expand Down Expand Up @@ -285,6 +287,7 @@ struct RowOperationsManagerCopyTests {
let rows: [[String?]] = [["1", "Smith, John", nil]]
let clipboard = MockClipboardProvider()
ClipboardService.shared = clipboard
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }
let tableRows = makeTableRows(rows: rows)

manager.copySelectedRowsToClipboard(selectedIndices: [0], tableRows: tableRows)
Expand All @@ -299,6 +302,7 @@ struct RowOperationsManagerCopyTests {
let rows: [[String?]] = [["1", "Alice", "alice@test.com"]]
let clipboard = MockClipboardProvider()
ClipboardService.shared = clipboard
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }
let tableRows = makeTableRows(rows: rows)

manager.copySelectedRowsToClipboard(selectedIndices: [0], tableRows: tableRows, visibleColumnIndices: [2, 0])
Expand All @@ -318,6 +322,7 @@ struct RowOperationsManagerCopyTests {
]
let clipboard = MockClipboardProvider()
ClipboardService.shared = clipboard
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }
let tableRows = makeTableRows(rows: rows)
let displayIDs: [RowID] = [.existing(0), .existing(2), .existing(3)]

Expand All @@ -340,6 +345,7 @@ struct RowOperationsManagerCopyTests {
]
let clipboard = MockClipboardProvider()
ClipboardService.shared = clipboard
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }
let tableRows = makeTableRows(rows: rows)
let displayIDs: [RowID] = [.existing(2), .existing(0)]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ struct CellPasteRoutingTests {
stub.text = "anything\twith\ttabs"
stub.hasGridRowsValue = true
ClipboardService.shared = stub
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }

let coordinator = makeCoordinator(columns: ["a", "b", "c"], rowCount: 5)
let result = coordinator.pasteCellsFromClipboard(anchorRow: 0, anchorColumn: 0)
Expand All @@ -82,6 +83,7 @@ struct CellPasteRoutingTests {
stub.text = "x\ty\tz\nq\tw\te"
stub.hasGridRowsValue = false
ClipboardService.shared = stub
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }

let coordinator = makeCoordinator(columns: ["a", "b", "c"], rowCount: 5)
let result = coordinator.pasteCellsFromClipboard(anchorRow: 0, anchorColumn: 0)
Expand All @@ -95,6 +97,7 @@ struct CellPasteRoutingTests {
stub.text = "x\ty"
stub.hasGridRowsValue = false
ClipboardService.shared = stub
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }

let coordinator = makeCoordinator(columns: ["a", "b", "c", "d", "e"], rowCount: 5)
let result = coordinator.pasteCellsFromClipboard(anchorRow: 0, anchorColumn: 0)
Expand All @@ -111,6 +114,7 @@ struct CellPasteRoutingTests {
stub.text = "hello"
stub.hasGridRowsValue = false
ClipboardService.shared = stub
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }

let coordinator = makeCoordinator(columns: ["a", "b", "c"], rowCount: 5)

Expand All @@ -124,6 +128,7 @@ struct CellPasteRoutingTests {
stub.text = "hello"
stub.hasGridRowsValue = true
ClipboardService.shared = stub
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }

let coordinator = makeCoordinator(columns: ["a", "b", "c"], rowCount: 5)

Expand All @@ -137,6 +142,7 @@ struct CellPasteRoutingTests {
stub.text = "hello"
stub.hasGridRowsValue = false
ClipboardService.shared = stub
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }

let coordinator = makeCoordinator(columns: ["a"], rowCount: 3)

Expand All @@ -149,6 +155,7 @@ struct CellPasteRoutingTests {
stub.text = "x\ty"
stub.hasGridRowsValue = false
ClipboardService.shared = stub
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }

let coordinator = makeCoordinator(columns: ["a", "b", "c", "d", "e"], rowCount: 5)
let before = Array(coordinator.tableRowsProvider().rows)
Expand All @@ -163,6 +170,7 @@ struct CellPasteRoutingTests {
let stub = StubClipboard()
stub.text = "x\ty"
ClipboardService.shared = stub
defer { ClipboardService.shared = NSPasteboardClipboardProvider() }

let coordinator = TableViewCoordinator(
changeManager: AnyChangeManager(DataChangeManager()),
Expand Down
Loading