macOS CI / build-and-test (push) Skipped
- Detect vendor tray keys in CupsParsers with InputSlot/MediaSource/EPIJ_FdSo/EPIJ_PSrc/CNIJMediaSupply roster; EPIJ_FdSo wins over EPIJ_PSrc. - PrinterTray now carries raw token + display name; PrinterCapabilities exposes trayKey, trayDefault and tray-specific quality maps. - CupsService enumerates trays from the detected key, enriches labels from PPD, seeds selection from CUPS default, and precomputes per-tray media→quality constraints. - MediaQualityConstraints resolves quality availability from both selected media and selected tray, falling back to the media-only map. - TicketWriteResolver emits the selected tray key=token in the locked write order between PageSize and media key. - PrintPanelService applies the Stage 2 tray token to PMPrintSettings on open and captures the tray back from the full roster plus AP_D_InputSlot. - PrintSessionViewModel keeps selectedTrayToken, applies capture-back, preserves unknown captured tokens as synthetic trays, and passes tray into spool overrides. - Stage2View replaces portrait/landscape buttons with an orientation dropdown (printerOrientationSelect) while preserving portrait/landscape semantics and orientation-requested=3|4. - Add/update core, UI and fixture tests for tray detection, enumeration, PPD labels, defaults, unknown-token synthesis, panel capture, ticket write order and tray-aware quality clamping. - Update docs/10-print-system.md, docs/11-print-macos.md, docs/21-ui-reference.md and docs/24-issues-invariants.md. Verified on the macOS 12.7.6 x86_64 CI runner: xcodebuild build-for-testing and ICCeryCoreTests pass (557 tests, 0 failures).
227 lines
8.9 KiB
Swift
227 lines
8.9 KiB
Swift
import XCTest
|
||
@testable import ICCeryCore
|
||
@testable import ICCery
|
||
|
||
/// Issue #201 Phase 4 / #218 — the resolved ticket-write list that
|
||
/// replaces the `lp` argv goldens: locked order 1–11, D6 (Stage 2
|
||
/// always wins), `raw` can never appear.
|
||
final class TicketWriteResolverTests: XCTestCase {
|
||
|
||
private func resolve(
|
||
overrides: TargetPrintOverrides = TargetPrintOverrides(),
|
||
optionKeys: Set<String> = []
|
||
) -> ResolvedTicketWrites {
|
||
TicketWriteResolver.resolve(
|
||
overrides: overrides, optionKeys: optionKeys)
|
||
}
|
||
|
||
private func keys(_ r: ResolvedTicketWrites) -> [String] {
|
||
r.writes.map(\.key)
|
||
}
|
||
|
||
private func value(
|
||
_ key: String, in r: ResolvedTicketWrites
|
||
) -> String? {
|
||
r.writes.first { $0.key == key }?.value
|
||
}
|
||
|
||
/// Both AP_* spellings are always present and locked; all three
|
||
/// Quartz keys are always present (D2 dual vocabulary).
|
||
func testColourKeysAlwaysPresent() {
|
||
let r = resolve()
|
||
let ap = r.writes.filter {
|
||
ColorMatchingAttempts.printSettingsKeys.contains($0.key)
|
||
}
|
||
XCTAssertEqual(ap.count, 2)
|
||
XCTAssertTrue(ap.allSatisfy {
|
||
$0.locked
|
||
&& $0.value == "AP_ApplicationColorMatching"
|
||
})
|
||
XCTAssertEqual(value("PMColorMatchingMode", in: r),
|
||
"APCustomColorMatching")
|
||
XCTAssertEqual(value("PMCustomColorMatchingProfile", in: r), "")
|
||
XCTAssertEqual(
|
||
value("com.apple.print.PrintSettings.PMColorMatchingMode",
|
||
in: r),
|
||
"APCustomColorMatching")
|
||
// The Quartz mode key is locked; profile + legacy key are not.
|
||
XCTAssertTrue(r.writes.first { $0.key == "PMColorMatchingMode" }!
|
||
.locked)
|
||
XCTAssertFalse(r.writes.first {
|
||
$0.key == "PMCustomColorMatchingProfile" }!.locked)
|
||
XCTAssertFalse(r.writes.first {
|
||
$0.key == "com.apple.print.PrintSettings.PMColorMatchingMode"
|
||
}!.locked)
|
||
}
|
||
|
||
/// `raw` can never appear — there is no `lp` and the resolver has
|
||
/// no captured-options input that could smuggle it in (#92).
|
||
func testRawNeverAppears() {
|
||
for optionKeys in [Set<String>(), ["raw"],
|
||
["MediaType", "raw"]] {
|
||
let r = resolve(
|
||
overrides: TargetPrintOverrides(
|
||
paperSize: "raw", mediaType: "raw",
|
||
qualityKey: "raw", quality: "raw",
|
||
trayKey: "raw", tray: "raw",
|
||
orientation: "raw"),
|
||
optionKeys: optionKeys)
|
||
XCTAssertFalse(r.writes.contains {
|
||
$0.key.lowercased() == "raw"
|
||
})
|
||
}
|
||
}
|
||
|
||
/// D6 inversion — the deleted argv builder suppressed the Stage 2
|
||
/// value when the captured string already carried the key (the old
|
||
/// media branch at `build`'s line 81). There is no captured-wins
|
||
/// path any more:
|
||
/// the explicit Stage 2 value is emitted unconditionally.
|
||
func testStage2MediaTypeAlwaysWins() {
|
||
let r = resolve(
|
||
overrides: TargetPrintOverrides(mediaType: "Matte"),
|
||
optionKeys: ["MediaType"])
|
||
XCTAssertEqual(value("MediaType", in: r), "Matte")
|
||
// …and likewise for a vendor key — nothing consults captured
|
||
// options before writing.
|
||
let epson = resolve(
|
||
overrides: TargetPrintOverrides(mediaType: "Matte"),
|
||
optionKeys: ["EPIJ_Medi"])
|
||
XCTAssertEqual(value("EPIJ_Medi", in: epson), "Matte")
|
||
}
|
||
|
||
/// Vendor media-key selection follows `CupsParsers.mediaTypeKeys`:
|
||
/// `EPIJ_Medi` is picked when present; `CNIJMediaType` beats
|
||
/// `MediaType`.
|
||
func testVendorMediaKeySelection() {
|
||
let epson = resolve(
|
||
overrides: TargetPrintOverrides(mediaType: "Photo"),
|
||
optionKeys: ["EPIJ_Medi", "MediaType"])
|
||
XCTAssertEqual(value("EPIJ_Medi", in: epson), "Photo")
|
||
XCTAssertNil(value("MediaType", in: epson))
|
||
|
||
let canon = resolve(
|
||
overrides: TargetPrintOverrides(mediaType: "Photo"),
|
||
optionKeys: ["CNIJMediaType", "MediaType"])
|
||
XCTAssertEqual(value("CNIJMediaType", in: canon), "Photo")
|
||
XCTAssertNil(value("MediaType", in: canon))
|
||
}
|
||
|
||
/// Driver "no colour adjustment" bypass — Canon, Epson (CCor
|
||
/// preferred over CMat), Gutenprint — always unlocked and never
|
||
/// gated on `ppdUncorrectedPassthrough`.
|
||
func testDriverBypass() {
|
||
let canon = resolve(optionKeys: ["CNIJIntent2"])
|
||
XCTAssertEqual(value("CNIJIntent2", in: canon), "4")
|
||
XCTAssertFalse(canon.writes.first { $0.key == "CNIJIntent2" }!
|
||
.locked)
|
||
|
||
let epson = resolve(optionKeys: ["EPIJ_CCor", "EPIJ_CMat"])
|
||
XCTAssertEqual(value("EPIJ_CCor", in: epson), "0")
|
||
XCTAssertNil(value("EPIJ_CMat", in: epson))
|
||
|
||
let epsonMat = resolve(optionKeys: ["EPIJ_CMat"])
|
||
XCTAssertEqual(value("EPIJ_CMat", in: epsonMat), "3")
|
||
|
||
let gutenprint = resolve(optionKeys: ["StpColorCorrection"])
|
||
XCTAssertEqual(value("StpColorCorrection", in: gutenprint),
|
||
"Uncorrected")
|
||
|
||
XCTAssertNil(value("EPIJ_CMat", in: resolve()))
|
||
}
|
||
|
||
/// Orientation: landscape → 4, portrait → 3, nil → no write.
|
||
func testOrientation() {
|
||
XCTAssertEqual(
|
||
value("orientation-requested", in: resolve(
|
||
overrides: TargetPrintOverrides(
|
||
orientation: "landscape"))),
|
||
"4")
|
||
XCTAssertEqual(
|
||
value("orientation-requested", in: resolve(
|
||
overrides: TargetPrintOverrides(
|
||
orientation: "portrait"))),
|
||
"3")
|
||
XCTAssertNil(value("orientation-requested", in: resolve()))
|
||
}
|
||
|
||
/// A `nil` override emits no write for that key; quality and tray
|
||
/// need **both** the detected key and the value.
|
||
func testNilOverridesEmitNoWrite() {
|
||
let r = resolve(optionKeys: ["MediaType", "EPIJ_Qual"])
|
||
XCTAssertNil(value("PageSize", in: r))
|
||
XCTAssertNil(value("MediaType", in: r))
|
||
XCTAssertNil(value("EPIJ_Qual", in: r))
|
||
XCTAssertNil(value("orientation-requested", in: r))
|
||
XCTAssertNil(r.paperToken)
|
||
XCTAssertNil(r.orientation)
|
||
|
||
let keyOnly = resolve(
|
||
overrides: TargetPrintOverrides(qualityKey: "EPIJ_Qual"),
|
||
optionKeys: ["EPIJ_Qual"])
|
||
XCTAssertNil(value("EPIJ_Qual", in: keyOnly))
|
||
let valueOnly = resolve(
|
||
overrides: TargetPrintOverrides(quality: "305"),
|
||
optionKeys: ["EPIJ_Qual"])
|
||
XCTAssertNil(value("EPIJ_Qual", in: valueOnly))
|
||
// An empty paper token emits no write and no paperToken.
|
||
let emptyPaper = resolve(
|
||
overrides: TargetPrintOverrides(paperSize: ""))
|
||
XCTAssertNil(value("PageSize", in: emptyPaper))
|
||
XCTAssertNil(emptyPaper.paperToken)
|
||
|
||
// Tray: key or value alone, or an empty value, emits no write.
|
||
let trayKeyOnly = resolve(
|
||
overrides: TargetPrintOverrides(trayKey: "EPIJ_FdSo"),
|
||
optionKeys: ["EPIJ_FdSo"])
|
||
XCTAssertNil(value("EPIJ_FdSo", in: trayKeyOnly))
|
||
let trayValueOnly = resolve(
|
||
overrides: TargetPrintOverrides(tray: "Rear"),
|
||
optionKeys: ["EPIJ_FdSo"])
|
||
XCTAssertNil(value("EPIJ_FdSo", in: trayValueOnly))
|
||
let emptyTray = resolve(
|
||
overrides: TargetPrintOverrides(trayKey: "EPIJ_FdSo", tray: ""),
|
||
optionKeys: ["EPIJ_FdSo"])
|
||
XCTAssertNil(value("EPIJ_FdSo", in: emptyTray))
|
||
}
|
||
|
||
/// #218 paper source is emitted with its canonical key and the raw
|
||
/// token; the display label must never reach the ticket.
|
||
func testTrayWrite() {
|
||
let r = resolve(
|
||
overrides: TargetPrintOverrides(
|
||
trayKey: "EPIJ_FdSo", tray: "Rear"),
|
||
optionKeys: ["EPIJ_FdSo"])
|
||
XCTAssertEqual(value("EPIJ_FdSo", in: r), "Rear")
|
||
XCTAssertEqual(r.writes.first { $0.key == "EPIJ_FdSo" }?.locked,
|
||
false)
|
||
XCTAssertNil(value("InputSlot", in: r))
|
||
}
|
||
|
||
/// The write list equals the locked 1–11 order, exactly.
|
||
func testLockedWriteOrder() {
|
||
let r = resolve(
|
||
overrides: TargetPrintOverrides(
|
||
paperSize: "A4", mediaType: "Photo",
|
||
qualityKey: "EPIJ_Qual", quality: "305",
|
||
trayKey: "EPIJ_FdSo", tray: "Rear",
|
||
orientation: "landscape"),
|
||
optionKeys: ["MediaType", "EPIJ_Qual", "EPIJ_CMat"])
|
||
XCTAssertEqual(keys(r), [
|
||
"AP_ColorMatchingMode",
|
||
"AP.ColorMatchingMode",
|
||
"PMColorMatchingMode",
|
||
"PMCustomColorMatchingProfile",
|
||
"com.apple.print.PrintSettings.PMColorMatchingMode",
|
||
"PageSize",
|
||
"EPIJ_FdSo",
|
||
"MediaType",
|
||
"EPIJ_Qual",
|
||
"EPIJ_CMat",
|
||
"orientation-requested",
|
||
])
|
||
XCTAssertEqual(r.paperToken, "A4")
|
||
XCTAssertEqual(r.orientation, "landscape")
|
||
}
|
||
}
|