Files
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> e385c74298 Stage 2 live print panel — unmanaged spool UI (#17)
- rawPrintPanel: printerSelect (lpstat -e, auto-refresh on manifest),
  printerStatusBadge (idle/printing/stopped), printerTraySelect +
  printerMediaTypeSelect from lpoptions -l capabilities, portrait/
  landscape toggle, btnPrinterProperties (bound NSPrintPanel), Print
  All + per-page btnPrintPage-N, in-panel printNotification (cancel →
  info, not error). No cupsOptionsGroup/chkPpdFallback — macOS always
  uses the PM-captured path.
- TargetWorkflowViewModel: refreshPrinters, reloadSelectedCapabilities,
  openPrinterPreferences (panel-side queue switch updates the select),
  printAllPages / printPage (sequential, stop-on-first-error),
  capturedCupsOptions per-queue session cache, wizard.printerName set
  on spool (#95).
- CupsError.noPrinterSelected.
- Mock CUPS fixtures: lpstat (2 queues, one idle one disabled),
  lpoptions (tray/media/EPIJ_CMat listings), lp (appends argv to
  ICCERY_TEST_LP_ARGV). Milestone3UITests: enumeration, preferences
  cancel→info, captured options replayed in argv, per-page print,
  lp failure notice, printerName persistence.
- lpoptions parser: skip keyless '=value' tokens instead of truncating.

UI tests written but not executed here: the dev machine's console is
locked (IOConsoleLocked=true) so XCUIApplication.activate() cannot
bring the app to front — same failure on M2 baseline. Suite must be
run unlocked / on CI.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-09 01:19:17 +01:00

132 lines
5.0 KiB
Swift

import Testing
import Foundation
@testable import ICCeryCore
/// Issue 15 — `lp` argv goldens (docs/11 `build_lp_args`).
/// `-d`/`options`/`-t` handling is in `CupsService`; these tests cover
/// flag order, captured-option precedence, and sanitisation.
@Suite("LpArgs")
struct LpArgsTests {
private let tiff = "/tmp/work/target_001.tif"
private let queue = "EPSON_XP_55_Series"
private func build(
options: PrintOptions = PrintOptions(),
optionKeys: Set<String> = []
) throws -> [String] {
try LpArgs.build(
queue: queue, tiffPath: tiff,
options: options, optionKeys: optionKeys)
}
@Test("Header: -d queue -t title, both AP_* first, TIFF last")
func header() throws {
let argv = try build()
#expect(Array(argv[0...1]) == ["-d", queue])
#expect(Array(argv[2...3]) == ["-t", "ICCery Target - target_001.tif"])
#expect(Array(argv[4...5])
== ["-o", "AP_ColorMatchingMode=AP_ApplicationColorMatching"])
#expect(Array(argv[6...7])
== ["-o", "AP.ColorMatchingMode=AP_ApplicationColorMatching"])
#expect(argv.last == tiff)
#expect(!argv.contains { $0 == "raw" || $0 == "-o raw" })
}
@Test("Never emits -o raw; captured raw= is dropped")
func neverRaw() throws {
let argv = try build(options: PrintOptions(
cupsOptions: "raw=true MediaType=Photo"))
for (i, arg) in argv.enumerated() where arg == "-o" {
#expect(argv[i + 1] != "raw")
#expect(argv[i + 1] != "raw=true")
}
#expect(!argv.contains { $0.hasPrefix("raw=") })
#expect(argv.contains("MediaType=Photo"))
}
@Test("Captured options replayed after AP_* headers")
func capturedReplay() throws {
let argv = try build(options: PrintOptions(
cupsOptions: "InputSlot=Rear MediaType=Photo"))
let rear = argv.firstIndex(of: "InputSlot=Rear")!
let apFirst = argv.firstIndex(of:
"AP_ColorMatchingMode=AP_ApplicationColorMatching")!
#expect(rear > apFirst)
}
@Test("Captured wins: media key present → derived media skipped")
func capturedWinsMedia() throws {
let argv = try build(
options: PrintOptions(
mediaType: "Plain",
cupsOptions: "MediaType=Glossy"),
optionKeys: ["MediaType"])
#expect(argv.contains("MediaType=Glossy"))
#expect(!argv.contains("MediaType=Plain"))
}
@Test("Media emitted via detected key when not captured")
func mediaDerived() throws {
let argv = try build(
options: PrintOptions(mediaType: "SemiGloss"),
optionKeys: ["CNIJMediaType", "MediaType"])
// CNIJMediaType wins over MediaType in detection order.
#expect(argv.contains("CNIJMediaType=SemiGloss"))
#expect(!argv.contains("MediaType=SemiGloss"))
}
@Test("Driver bypass emitted when absent, skipped when captured")
func bypassRules() throws {
let withBypass = try build(
optionKeys: ["EPIJ_CMat"])
#expect(withBypass.contains("EPIJ_CMat=3"))
let captured = try build(
options: PrintOptions(cupsOptions: "EPIJ_CMat=1"),
optionKeys: ["EPIJ_CMat"])
// Captured value kept, detection not re-applied.
#expect(captured.filter { $0.hasPrefix("EPIJ_CMat") }
== ["EPIJ_CMat=1"])
}
@Test("Orientation: portrait=3 landscape=4; captured wins")
func orientation() throws {
#expect(try build(options: PrintOptions(orientation: "portrait"))
.contains("orientation-requested=3"))
#expect(try build(options: PrintOptions(orientation: "landscape"))
.contains("orientation-requested=4"))
let capturedOrients = try build(options: PrintOptions(
orientation: "landscape",
cupsOptions: "orientation-requested=5"))
#expect(!capturedOrients.contains("orientation-requested=4"))
#expect(capturedOrients.contains("orientation-requested=5"))
}
@Test("PageSize emitted unless captured")
func pageSize() throws {
#expect(try build(options: PrintOptions(paperSize: "A4"))
.contains("PageSize=A4"))
let capturedSize = try build(options: PrintOptions(
paperSize: "A4", cupsOptions: "PageSize=Letter"))
#expect(!capturedSize.contains("PageSize=A4"))
#expect(capturedSize.contains("PageSize=Letter"))
}
@Test("Sanitise rejects `;`, newline, and shell metachars")
func sanitise() throws {
#expect(throws: LpArgsError.self) {
_ = try build(options: PrintOptions(
cupsOptions: "InputSlot=Rear;rm -rf /"))
}
#expect(throws: LpArgsError.self) {
_ = try build(options: PrintOptions(
cupsOptions: "InputSlot=Rear\nMediaType=Photo"))
}
#expect(throws: LpArgsError.self) {
_ = try build(options: PrintOptions(
cupsOptions: "InputSlot=$(whoami)"))
}
}
}