Give mixed selections something to do

A Drag of different kinds showed an empty wheel, because the catalog only
answers for a single category. Filee's answer is a mixed profile, so add
Zip (one archive, flat names) and Merge (non-PDFs are converted to PDF
first); a multi-file drag of one kind gets Zip too.
This commit is contained in:
2026-10-20 01:30:42 +08:00
parent 2d031e58a4
commit a01d5def89
8 changed files with 295 additions and 41 deletions
+74
View File
@@ -1,6 +1,7 @@
import AppKit
import CoreGraphics
import Foundation
import PDFKit
/// `--self-test` exercises the real conversion paths end to end and exits
/// non-zero on failure. Kept in the shipped binary so a build can be verified
@@ -29,6 +30,7 @@ enum SelfTest {
testDocuments()
testAudio()
testEbook()
testCombining()
testGeometry()
print("")
@@ -224,6 +226,25 @@ enum SelfTest {
try ArchiveConverter.convert(source: folder, to: .tar, at: tar)
try requireFile(tar)
}
check("several files → one zip") {
let first = directory.appendingPathComponent("note.txt")
let second = directory.appendingPathComponent("spreadsheet.csv")
try? "one".write(to: first, atomically: true, encoding: .utf8)
try? "a,b\n1,2\n".write(to: second, atomically: true, encoding: .utf8)
let zip = directory.appendingPathComponent("selection.zip")
try ArchiveConverter.createArchive(from: [first, second], at: zip)
try requireFile(zip)
let target = directory.appendingPathComponent("selection-unpacked", isDirectory: true)
try ArchiveConverter.extract(source: zip, into: target)
for name in ["note.txt", "spreadsheet.csv"] {
let entry = target.appendingPathComponent(name)
guard FileManager.default.fileExists(atPath: entry.path) else {
throw TestFailure("\(name) missing from the archive")
}
}
}
}
private static func testDocuments() {
@@ -325,6 +346,59 @@ enum SelfTest {
}
}
/// Filee's "mixed profile": a drag of different kinds still has something
/// useful to do, instead of an empty wheel.
private static func testCombining() {
section("Mixed selections")
let photo = directory.appendingPathComponent("sample.png")
let pdf = directory.appendingPathComponent("sample.pdf")
let note = directory.appendingPathComponent("note.txt")
check("a mixed drag still gets a wheel") {
let items = WheelBuilder.items(kind: .convert, urls: [photo, pdf])
let tools = items.compactMap { item -> ToolID? in
if case .tool(let tool) = item.payload { return tool }
return nil
}
guard !items.isEmpty else { throw TestFailure("the convert wheel was empty") }
guard tools.contains(.mergePDF), tools.contains(.createArchive) else {
throw TestFailure("expected Merge and Zip, got \(tools.map(\.rawValue))")
}
}
check("a multi-file wheel can always be zipped") {
let items = WheelBuilder.items(kind: .tools, urls: [photo, directory.appendingPathComponent("note.txt")])
let tools = items.compactMap { item -> ToolID? in
if case .tool(let tool) = item.payload { return tool }
return nil
}
guard tools.contains(.createArchive) else {
throw TestFailure("Zip is missing from a two-file image wheel")
}
}
check("photos and pdfs merge into one pdf") {
let outcome = try ToolRunner.run(tool: .mergePDF, inputs: [photo, pdf], params: ToolParams())
guard let merged = outcome.outputs.first,
let document = PDFDocument(url: merged) else {
throw TestFailure("no merged pdf was produced")
}
guard document.pageCount >= 2 else {
throw TestFailure("merged pdf has \(document.pageCount) page(s)")
}
}
check("any files → one zip") {
let outcome = try ToolRunner.run(tool: .createArchive, inputs: [note, photo], params: ToolParams())
guard let zip = outcome.outputs.first else { throw TestFailure("no archive was produced") }
try requireFile(zip)
let entries = try ArchiveConverter.list(zip)
for name in ["note.txt", "sample.png"] where !entries.contains(name) {
throw TestFailure("\(name) is missing from the archive: \(entries)")
}
}
}
private static func testGeometry() {
section("Wheel geometry")
let frame = CGRect(x: 0, y: 0, width: 400, height: 400)