Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12061a03ac |
@@ -186,17 +186,9 @@ struct ColorSyncSuppressor {
|
|||||||
func captureOptions(
|
func captureOptions(
|
||||||
from settings: PMPrintSettings
|
from settings: PMPrintSettings
|
||||||
) -> (cupsOptions: String?, mediaType: String?) {
|
) -> (cupsOptions: String?, mediaType: String?) {
|
||||||
guard let toOptions = optionsResolver() else {
|
guard let unfiltered = rawOptionsDump(from: settings) else {
|
||||||
log("ColorSync: PMPrintSettingsToOptions unavailable — "
|
|
||||||
+ "panel options not captured")
|
|
||||||
return (nil, nil)
|
return (nil, nil)
|
||||||
}
|
}
|
||||||
var raw: UnsafeMutablePointer<CChar>?
|
|
||||||
guard toOptions(settings, &raw) == 0, let raw else {
|
|
||||||
return (nil, nil)
|
|
||||||
}
|
|
||||||
defer { free(raw) }
|
|
||||||
let unfiltered = String(cString: raw)
|
|
||||||
let filtered = CupsOptionsFilter.filter(unfiltered)
|
let filtered = CupsOptionsFilter.filter(unfiltered)
|
||||||
return (
|
return (
|
||||||
filtered.isEmpty ? nil : filtered,
|
filtered.isEmpty ? nil : filtered,
|
||||||
@@ -204,6 +196,23 @@ struct ColorSyncSuppressor {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unfiltered `PMPrintSettingsToOptions` output — the diagnostics
|
||||||
|
/// half of the capture path (#217 `ICCERY_DEBUG_PANEL_DUMP`).
|
||||||
|
/// `nil` when the symbol or the call fails.
|
||||||
|
func rawOptionsDump(from settings: PMPrintSettings) -> String? {
|
||||||
|
guard let toOptions = optionsResolver() else {
|
||||||
|
log("ColorSync: PMPrintSettingsToOptions unavailable — "
|
||||||
|
+ "panel options not captured")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var raw: UnsafeMutablePointer<CChar>?
|
||||||
|
guard toOptions(settings, &raw) == 0, let raw else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer { free(raw) }
|
||||||
|
return String(cString: raw)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - dlsym
|
// MARK: - dlsym
|
||||||
|
|
||||||
private static func dlsymMode(_ name: String) -> ColorMatchingModeFunction? {
|
private static func dlsymMode(_ name: String) -> ColorMatchingModeFunction? {
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ struct PrintPanelInitialSelections {
|
|||||||
var orientation: String?
|
var orientation: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One bound-panel invocation (#217): the queue, its `lpoptions -l`
|
||||||
|
/// key roster, the Stage 2 pre-selections, and the session's captured
|
||||||
|
/// ticket — everything `makePanelPrintInfo` needs before the modal.
|
||||||
|
struct PrintPanelRequest {
|
||||||
|
let queue: String
|
||||||
|
let displayName: String?
|
||||||
|
let optionKeys: Set<String>
|
||||||
|
let initialSelections: PrintPanelInitialSelections
|
||||||
|
/// The queue's previously captured ticket — rehydrated before the
|
||||||
|
/// Stage 2 writes so vendor PDE state survives a reopen (#217).
|
||||||
|
let ticket: PrintTicket?
|
||||||
|
}
|
||||||
|
|
||||||
/// Preferences → native `NSPrintPanel` bound to the selected CUPS
|
/// Preferences → native `NSPrintPanel` bound to the selected CUPS
|
||||||
/// queue (issue 13, docs/11).
|
/// queue (issue 13, docs/11).
|
||||||
///
|
///
|
||||||
@@ -58,7 +71,8 @@ struct PrintPanelService {
|
|||||||
displayName: String?,
|
displayName: String?,
|
||||||
cupsService: CupsService,
|
cupsService: CupsService,
|
||||||
initialSelections: PrintPanelInitialSelections =
|
initialSelections: PrintPanelInitialSelections =
|
||||||
PrintPanelInitialSelections()
|
PrintPanelInitialSelections(),
|
||||||
|
ticket: PrintTicket? = nil
|
||||||
) async throws -> PanelCaptureResult? {
|
) async throws -> PanelCaptureResult? {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if UITestHooks.printPanelStubbed {
|
if UITestHooks.printPanelStubbed {
|
||||||
@@ -75,83 +89,139 @@ struct PrintPanelService {
|
|||||||
let optionKeys = (try? await cupsService.optionKeys(for: queue))
|
let optionKeys = (try? await cupsService.optionKeys(for: queue))
|
||||||
?? []
|
?? []
|
||||||
return try runNativePanel(
|
return try runNativePanel(
|
||||||
queue: queue, displayName: display, optionKeys: optionKeys,
|
PrintPanelRequest(
|
||||||
initialSelections: initialSelections)
|
queue: queue, displayName: display,
|
||||||
|
optionKeys: optionKeys,
|
||||||
|
initialSelections: initialSelections, ticket: ticket))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Panel
|
// MARK: - Panel
|
||||||
|
|
||||||
private func runNativePanel(
|
/// The pre-modal half of `runNativePanel` — a fully configured
|
||||||
queue: String,
|
/// `NSPrintInfo` plus the `boundViaPM` flag. Extracted (same
|
||||||
displayName: String?,
|
/// pattern as `NativeTargetSpooler.makePrintInfo`) so unit tests
|
||||||
optionKeys: Set<String>,
|
/// assert ticket restore / Stage 2 precedence without driving the
|
||||||
initialSelections: PrintPanelInitialSelections
|
/// modal (#217). The caller owns `printer` — it must stay alive
|
||||||
) throws -> PanelCaptureResult? {
|
/// through the modal.
|
||||||
|
func makePanelPrintInfo(
|
||||||
|
for request: PrintPanelRequest,
|
||||||
|
boundTo printer: PMPrinter?
|
||||||
|
) throws -> (printInfo: NSPrintInfo, boundViaPM: Bool) {
|
||||||
let printInfo = NSPrintInfo()
|
let printInfo = NSPrintInfo()
|
||||||
var pmPrinter: PMPrinter?
|
|
||||||
var boundViaPM = false
|
|
||||||
|
|
||||||
// ① Bind the session to the selected CUPS queue (docs/11).
|
// ① Bind the session to the selected CUPS queue (docs/11).
|
||||||
if let printer = PMTicketBridge.makePrinter(queue: queue) {
|
if let printer {
|
||||||
pmPrinter = printer
|
|
||||||
let session = PMTicketBridge.session(printInfo)
|
|
||||||
let settings = PMTicketBridge.settings(printInfo)
|
|
||||||
|
|
||||||
do {
|
|
||||||
try PMTicketBridge.bind(printer: printer, to: printInfo)
|
try PMTicketBridge.bind(printer: printer, to: printInfo)
|
||||||
} catch {
|
configureBoundPanel(
|
||||||
PMTicketBridge.release(printer)
|
printInfo, request: request, printer: printer)
|
||||||
throw error
|
|
||||||
}
|
|
||||||
// Initial selections — after `PMSessionDefault*`, before
|
|
||||||
// ColorSync suppression ②–⑤ (locked write order,
|
|
||||||
// #183/#186). Paper is TWO writes (E1): the `PageSize`
|
|
||||||
// print-settings value drivers/capture read AND the
|
|
||||||
// `PMPageFormat` paper the panel's dropdown reflects.
|
|
||||||
applyInitialSelections(
|
|
||||||
initialSelections, to: settings, optionKeys: optionKeys)
|
|
||||||
if let paperToken = initialSelections.paperSize {
|
|
||||||
PMTicketBridge.applyPaper(
|
|
||||||
token: paperToken, printer: printer,
|
|
||||||
session: session, printInfo: printInfo)
|
|
||||||
}
|
|
||||||
boundViaPM = true
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback: NSPrinter by display name (docs/11 §binding).
|
// Fallback: NSPrinter by display name (docs/11 §binding).
|
||||||
// Warn — the display name can resolve a *different* queue
|
// Warn — the display name can resolve a *different* queue
|
||||||
// (#186 E2: diagnosable, not a proven defect).
|
// (#186 E2: diagnosable, not a proven defect).
|
||||||
AppLogger.shared.warn(
|
AppLogger.shared.warn(
|
||||||
"Print panel: PM binding unavailable for '\(queue)' — "
|
"Print panel: PM binding unavailable for "
|
||||||
|
+ "'\(request.queue)' — "
|
||||||
+ "falling back to NSPrinter(displayName)")
|
+ "falling back to NSPrinter(displayName)")
|
||||||
guard let displayName,
|
guard let displayName = request.displayName,
|
||||||
let nsPrinter = NSPrinter(name: displayName)
|
let nsPrinter = NSPrinter(name: displayName)
|
||||||
else {
|
else {
|
||||||
throw PrintPanelError.noPrinterFound(
|
throw PrintPanelError.noPrinterFound(
|
||||||
displayName ?? queue)
|
request.displayName ?? request.queue)
|
||||||
}
|
}
|
||||||
printInfo.printer = nsPrinter
|
printInfo.printer = nsPrinter
|
||||||
printInfo.setUpPrintOperationDefaultValues()
|
printInfo.setUpPrintOperationDefaultValues()
|
||||||
|
return (printInfo, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (printInfo, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Post-bind configuration in locked order (#217 — mirrors the
|
||||||
|
/// spooler's S4→S7): ticket restore → Stage 2 selections →
|
||||||
|
/// validate → paper → suppression ②–⑤. Internal so unit tests can
|
||||||
|
/// drive it with `printer: nil` — `applyPaper` and the SPI layer
|
||||||
|
/// need a bound printer and skip when it is absent, while ticket
|
||||||
|
/// restore / selections / validate still run on the session-less
|
||||||
|
/// `NSPrintInfo`.
|
||||||
|
func configureBoundPanel(
|
||||||
|
_ printInfo: NSPrintInfo,
|
||||||
|
request: PrintPanelRequest,
|
||||||
|
printer: PMPrinter?
|
||||||
|
) {
|
||||||
|
let session = PMTicketBridge.session(printInfo)
|
||||||
|
let settings = PMTicketBridge.settings(printInfo)
|
||||||
|
|
||||||
|
// Rehydrate the captured ticket *before* the Stage 2 writes
|
||||||
|
// (#217 — restore-then-override). Warn-only, unlike the
|
||||||
|
// spooler's fatal `try`: a stale/corrupt ticket must not keep
|
||||||
|
// the dialog from opening. `restore` itself refuses a
|
||||||
|
// cross-queue replay (R3).
|
||||||
|
if let ticket = request.ticket {
|
||||||
|
do {
|
||||||
|
try PMTicketBridge.restore(ticket, into: printInfo)
|
||||||
|
} catch {
|
||||||
|
AppLogger.shared.warn(
|
||||||
|
"Print panel: ticket restore failed — "
|
||||||
|
+ "opening on queue defaults "
|
||||||
|
+ "(\(error.localizedDescription))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial selections — Stage 2 wins over the restored ticket
|
||||||
|
// (D6 parity). Paper is TWO writes (E1): the `PageSize`
|
||||||
|
// print-settings value drivers/capture read AND the
|
||||||
|
// `PMPageFormat` paper the panel's dropdown reflects.
|
||||||
|
applyInitialSelections(
|
||||||
|
request.initialSelections, to: settings,
|
||||||
|
optionKeys: request.optionKeys)
|
||||||
|
|
||||||
|
// Reconcile the pre-writes into the ticket before the modal
|
||||||
|
// (#217 RC2): the vendor PDE derives pane state from a
|
||||||
|
// validated ticket. Warn-only — log when the driver adjusts
|
||||||
|
// so a companion-key desync is diagnosable.
|
||||||
|
var changed = DarwinBoolean(false)
|
||||||
|
_ = PMSessionValidatePrintSettings(session, settings, &changed)
|
||||||
|
if changed.boolValue {
|
||||||
|
AppLogger.shared.info(
|
||||||
|
"Print panel: driver adjusted the pre-selections")
|
||||||
|
}
|
||||||
|
printInfo.updateFromPMPrintSettings()
|
||||||
|
|
||||||
|
if let printer, let paperToken = request.initialSelections.paperSize {
|
||||||
|
PMTicketBridge.applyPaper(
|
||||||
|
token: paperToken, printer: printer,
|
||||||
|
session: session, printInfo: printInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ②–⑤ ColourSync suppression. The SPI needs a session with a
|
||||||
|
// current printer to attach to; the SetValue/mirror layers
|
||||||
|
// are warn-only and run regardless.
|
||||||
|
if printer != nil {
|
||||||
|
suppressor.applySPIMode(to: session) // ②
|
||||||
|
}
|
||||||
|
suppressor.applyLockedKeys(to: settings) // ③
|
||||||
|
suppressor.applyQuartzMode(to: settings) // ⑤′-a (D2)
|
||||||
|
let driverBypass = suppressor.applyDriverBypass( // ④
|
||||||
|
to: settings, optionKeys: request.optionKeys)
|
||||||
|
suppressor.mirror(into: printInfo, driverBypass: driverBypass) // ⑤
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runNativePanel(
|
||||||
|
_ request: PrintPanelRequest
|
||||||
|
) throws -> PanelCaptureResult? {
|
||||||
|
// The PM printer handle is held through the modal and
|
||||||
|
// released on every path.
|
||||||
|
let pmPrinter = PMTicketBridge.makePrinter(queue: request.queue)
|
||||||
defer {
|
defer {
|
||||||
if let printer = pmPrinter {
|
if let printer = pmPrinter {
|
||||||
PMTicketBridge.release(printer)
|
PMTicketBridge.release(printer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ②–⑤ ColourSync suppression — only on the PM path: the SPI
|
let (printInfo, boundViaPM) = try makePanelPrintInfo(
|
||||||
// and PMPrintSettingsSetValue need a session with a current
|
for: request, boundTo: pmPrinter)
|
||||||
// printer to attach to.
|
|
||||||
var settings = PMTicketBridge.settings(printInfo)
|
debugDump(printInfo, when: "pre-modal")
|
||||||
var driverBypass: (key: String, value: String)?
|
|
||||||
if boundViaPM {
|
|
||||||
let session = PMTicketBridge.session(printInfo)
|
|
||||||
suppressor.applySPIMode(to: session) // ②
|
|
||||||
suppressor.applyLockedKeys(to: settings) // ③
|
|
||||||
suppressor.applyQuartzMode(to: settings) // ⑤′-a (D2)
|
|
||||||
driverBypass = suppressor.applyDriverBypass( // ④
|
|
||||||
to: settings, optionKeys: optionKeys)
|
|
||||||
suppressor.mirror(into: printInfo, driverBypass: driverBypass) // ⑤
|
|
||||||
}
|
|
||||||
|
|
||||||
let panel = NSPrintPanel()
|
let panel = NSPrintPanel()
|
||||||
panel.options = [
|
panel.options = [
|
||||||
@@ -174,8 +244,9 @@ struct PrintPanelService {
|
|||||||
var cupsOptions: String?
|
var cupsOptions: String?
|
||||||
var mediaType: String?
|
var mediaType: String?
|
||||||
if boundViaPM {
|
if boundViaPM {
|
||||||
settings = PMTicketBridge.settings(printInfo)
|
debugDump(printInfo, when: "post-OK")
|
||||||
let captured = suppressor.captureOptions(from: settings)
|
let captured = suppressor.captureOptions(
|
||||||
|
from: PMTicketBridge.settings(printInfo))
|
||||||
cupsOptions = captured.cupsOptions
|
cupsOptions = captured.cupsOptions
|
||||||
mediaType = captured.mediaType
|
mediaType = captured.mediaType
|
||||||
}
|
}
|
||||||
@@ -183,8 +254,8 @@ struct PrintPanelService {
|
|||||||
let resolvedQueue = boundViaPM
|
let resolvedQueue = boundViaPM
|
||||||
? PMTicketBridge.currentPrinterID(
|
? PMTicketBridge.currentPrinterID(
|
||||||
session: PMTicketBridge.session(printInfo),
|
session: PMTicketBridge.session(printInfo),
|
||||||
fallback: queue)
|
fallback: request.queue)
|
||||||
: queue
|
: request.queue
|
||||||
// ⑦ Serialise the native ticket — the payload `lp -o` could
|
// ⑦ Serialise the native ticket — the payload `lp -o` could
|
||||||
// never carry (#201). Warn-only via `try?`: a serialise
|
// never carry (#201). Warn-only via `try?`: a serialise
|
||||||
// failure must not lose the Stage 2 mirror above.
|
// failure must not lose the Stage 2 mirror above.
|
||||||
@@ -243,4 +314,35 @@ struct PrintPanelService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `ICCERY_DEBUG_PANEL_DUMP=1` — log the unfiltered
|
||||||
|
/// `PMPrintSettingsToOptions` output and the serialised
|
||||||
|
/// settings/page-format byte counts (#217 diagnostics for the
|
||||||
|
/// Epson RC3 question: does the PDE source pane state from the
|
||||||
|
/// session ticket or lpoptions?). DEBUG-only, warn-only.
|
||||||
|
private func debugDump(_ printInfo: NSPrintInfo, when phase: String) {
|
||||||
|
#if DEBUG
|
||||||
|
guard ProcessInfo.processInfo.environment[
|
||||||
|
"ICCERY_DEBUG_PANEL_DUMP"] == "1" else { return }
|
||||||
|
let raw = suppressor.rawOptionsDump(
|
||||||
|
from: PMTicketBridge.settings(printInfo)) ?? "<unavailable>"
|
||||||
|
var settingsSize = -1
|
||||||
|
var ref: Unmanaged<CFData>?
|
||||||
|
if PMPrintSettingsCreateDataRepresentation(
|
||||||
|
PMTicketBridge.settings(printInfo), &ref,
|
||||||
|
kPMDataFormatXMLDefault) == noErr, let ref {
|
||||||
|
settingsSize = (ref.takeRetainedValue() as Data).count
|
||||||
|
}
|
||||||
|
var formatSize = -1
|
||||||
|
var formatRef: Unmanaged<CFData>?
|
||||||
|
if PMPageFormatCreateDataRepresentation(
|
||||||
|
PMTicketBridge.pageFormat(printInfo), &formatRef,
|
||||||
|
kPMDataFormatXMLDefault) == noErr, let formatRef {
|
||||||
|
formatSize = (formatRef.takeRetainedValue() as Data).count
|
||||||
|
}
|
||||||
|
AppLogger.shared.info(
|
||||||
|
"Print panel dump (\(phase)): settings=\(settingsSize)B "
|
||||||
|
+ "pageFormat=\(formatSize)B options: \(raw)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,13 +231,15 @@ final class PrintSessionViewModel: ObservableObject {
|
|||||||
quality: selectedQuality,
|
quality: selectedQuality,
|
||||||
mediaType: selectedMediaType,
|
mediaType: selectedMediaType,
|
||||||
orientation: printOrientation)
|
orientation: printOrientation)
|
||||||
|
let ticket = capturedTickets[queue]
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
guard let result = try await PrintPanelService()
|
guard let result = try await PrintPanelService()
|
||||||
.showProperties(
|
.showProperties(
|
||||||
queue: queue, displayName: displayName,
|
queue: queue, displayName: displayName,
|
||||||
cupsService: cups,
|
cupsService: cups,
|
||||||
initialSelections: selections)
|
initialSelections: selections,
|
||||||
|
ticket: ticket)
|
||||||
else {
|
else {
|
||||||
printNotice = Notice(
|
printNotice = Notice(
|
||||||
kind: .info,
|
kind: .info,
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
@testable import ICCery
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
) -> PrintPanelRequest {
|
||||||
|
PrintPanelRequest(
|
||||||
|
queue: "Panel_Q", displayName: nil, optionKeys: [],
|
||||||
|
initialSelections: selections, ticket: ticket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user