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).
181 lines
6.7 KiB
Swift
181 lines
6.7 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import XCTest
|
|
@testable import ICCery
|
|
@testable import ICCeryCore
|
|
|
|
/// Issue #217 — the panel's pre-modal seam
|
|
/// (`PrintPanelService.configureBoundPanel`) must rehydrate the
|
|
/// captured `PrintTicket` before the Stage 2 writes, validate the
|
|
/// result, and refuse a foreign-queue ticket — all without driving
|
|
/// the modal or needing a real queue (`printer: nil` exercises
|
|
/// restore → selections → validate on a session-less `NSPrintInfo`).
|
|
@MainActor
|
|
final class PrintPanelTicketTests: XCTestCase {
|
|
|
|
private let service = PrintPanelService()
|
|
|
|
/// The queue a destination `NSPrintInfo` will report as bound —
|
|
/// the session's current printer, else a synthetic token for a
|
|
/// queue-less environment (same convention as PrintTicketTests).
|
|
private func boundQueue(of printInfo: NSPrintInfo) -> String {
|
|
PMTicketBridge.currentPrinterID(
|
|
session: PMTicketBridge.session(printInfo),
|
|
fallback: "UnboundQueue")
|
|
}
|
|
|
|
private func makeTicket(
|
|
queue: String,
|
|
key: String = "EPIJ_Qual",
|
|
value: String = "305"
|
|
) throws -> PrintTicket {
|
|
let source = NSPrintInfo()
|
|
XCTAssertTrue(
|
|
PMTicketBridge.setValue(
|
|
value, forKey: key, locked: false,
|
|
in: PMTicketBridge.settings(source),
|
|
context: "PrintPanelTicketTests"))
|
|
return try PMTicketBridge.serialise(source, queue: queue)
|
|
}
|
|
|
|
private func request(
|
|
ticket: PrintTicket?,
|
|
selections: PrintPanelInitialSelections =
|
|
PrintPanelInitialSelections(),
|
|
trays: [PrinterTray] = []
|
|
) -> PrintPanelRequest {
|
|
PrintPanelRequest(
|
|
queue: "Panel_Q", displayName: nil, optionKeys: [],
|
|
initialSelections: selections, ticket: ticket, trays: trays)
|
|
}
|
|
|
|
/// RC1 — a captured vendor key is present in the configured
|
|
/// printInfo before the modal would open.
|
|
func testTicketRestoredPreModal() throws {
|
|
let printInfo = NSPrintInfo()
|
|
let ticket = try makeTicket(queue: boundQueue(of: printInfo))
|
|
|
|
service.configureBoundPanel(
|
|
printInfo, request: request(ticket: ticket), printer: nil)
|
|
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(
|
|
forKey: "EPIJ_Qual",
|
|
in: PMTicketBridge.settings(printInfo)),
|
|
"305")
|
|
}
|
|
|
|
/// D6 parity — Stage 2 selections win over the restored ticket.
|
|
func testStage2WinsOverTicket() throws {
|
|
let printInfo = NSPrintInfo()
|
|
let ticket = try makeTicket(queue: boundQueue(of: printInfo))
|
|
var selections = PrintPanelInitialSelections()
|
|
selections.qualityKey = "EPIJ_Qual"
|
|
selections.quality = "303"
|
|
|
|
service.configureBoundPanel(
|
|
printInfo,
|
|
request: request(ticket: ticket, selections: selections),
|
|
printer: nil)
|
|
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(
|
|
forKey: "EPIJ_Qual",
|
|
in: PMTicketBridge.settings(printInfo)),
|
|
"303")
|
|
}
|
|
|
|
/// R3 — a ticket captured for another queue is refused; the
|
|
/// destination's settings stay untouched and nothing throws.
|
|
func testCrossQueueTicketRefusedAtPanel() throws {
|
|
let printInfo = NSPrintInfo()
|
|
let bound = boundQueue(of: printInfo)
|
|
let foreign = "DefinitelyNot_\(bound)"
|
|
try XCTSkipIf(
|
|
bound == foreign,
|
|
"destination resolved to the foreign queue")
|
|
XCTAssertTrue(
|
|
PMTicketBridge.setValue(
|
|
"999", forKey: "EPIJ_Qual", locked: false,
|
|
in: PMTicketBridge.settings(printInfo),
|
|
context: "PrintPanelTicketTests"))
|
|
let ticket = try makeTicket(queue: foreign)
|
|
|
|
service.configureBoundPanel(
|
|
printInfo, request: request(ticket: ticket), printer: nil)
|
|
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(
|
|
forKey: "EPIJ_Qual",
|
|
in: PMTicketBridge.settings(printInfo)),
|
|
"999")
|
|
}
|
|
|
|
/// #218 — the Stage 2 tray selection lands after `PageSize` and
|
|
/// before the media key in the configured `NSPrintInfo`.
|
|
func testStage2TrayWrittenPreModal() {
|
|
let printInfo = NSPrintInfo()
|
|
var selections = PrintPanelInitialSelections()
|
|
selections.paperSize = "A4"
|
|
selections.trayKey = "EPIJ_FdSo"
|
|
selections.tray = "Rear"
|
|
selections.mediaType = "Photo"
|
|
|
|
var req = request(ticket: nil, selections: selections)
|
|
req = PrintPanelRequest(
|
|
queue: req.queue, displayName: req.displayName,
|
|
optionKeys: ["MediaType"], initialSelections: selections,
|
|
ticket: nil, trays: [])
|
|
service.configureBoundPanel(
|
|
printInfo, request: req, printer: nil)
|
|
|
|
let settings = PMTicketBridge.settings(printInfo)
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(forKey: "PageSize", in: settings),
|
|
"A4")
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(forKey: "EPIJ_FdSo", in: settings),
|
|
"Rear")
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(
|
|
forKey: "MediaType", in: settings), "Photo")
|
|
}
|
|
|
|
/// First-ever open — no ticket, selections still land and the
|
|
/// post-selections validate keeps them readable.
|
|
func testNoTicketKeepsSelections() {
|
|
let printInfo = NSPrintInfo()
|
|
var selections = PrintPanelInitialSelections()
|
|
selections.paperSize = "A4"
|
|
selections.qualityKey = "EPIJ_Qual"
|
|
selections.quality = "303"
|
|
selections.orientation = "landscape"
|
|
|
|
service.configureBoundPanel(
|
|
printInfo,
|
|
request: request(ticket: nil, selections: selections),
|
|
printer: nil)
|
|
|
|
let settings = PMTicketBridge.settings(printInfo)
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(
|
|
forKey: "PageSize", in: settings), "A4")
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(
|
|
forKey: "EPIJ_Qual", in: settings), "303")
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(
|
|
forKey: "orientation-requested", in: settings), "4")
|
|
// The suppression writes still land on the ticket so the PDE
|
|
// sees the locked AP_* pair (③) and Quartz vocabulary (⑤′).
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(
|
|
forKey: "AP_ColorMatchingMode", in: settings),
|
|
"AP_ApplicationColorMatching")
|
|
XCTAssertEqual(
|
|
PMTicketBridge.stringValue(
|
|
forKey: "PMColorMatchingMode", in: settings),
|
|
"APCustomColorMatching")
|
|
}
|
|
}
|