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).
184 lines
8.0 KiB
Swift
184 lines
8.0 KiB
Swift
import Foundation
|
|
import ICCeryCore
|
|
|
|
/// App dependency container (docs/02). Production builds resolve the
|
|
/// user's `argyll_binary_dir` override or bundled sidecars; DEBUG UI
|
|
/// tests inject fixture binaries via `ICCERY_ARGYLL_BINARY_DIR` and
|
|
/// redirect `AppPaths` via `ICCERY_TEST_ROOT`, so tests never touch the
|
|
/// developer's settings, wizard state, or real Argyll install.
|
|
struct AppEnvironment: Sendable {
|
|
let stateStore: WizardStateStore
|
|
let settingsStore: SettingsStore
|
|
let presetStore: PresetStore
|
|
let runner: ArgyllRunner
|
|
let cupsService: CupsService
|
|
let historyStore: VerificationHistoryStore
|
|
let mediaStore: MediaLibraryStore
|
|
let recentProjectsStore: RecentProjectsStore
|
|
|
|
static func live(
|
|
environment: [String: String] = ProcessInfo.processInfo.environment
|
|
) -> AppEnvironment {
|
|
let settingsStore = SettingsStore()
|
|
var overrideDir = settingsStore.load().argyllBinaryDir
|
|
.map { URL(fileURLWithPath: $0) }
|
|
var bundledRoot = AppPaths.bundledArgyllDir
|
|
var cupsDir = URL(fileURLWithPath: "/usr/bin")
|
|
var ppdDir = URL(fileURLWithPath: "/etc/cups/ppd")
|
|
#if DEBUG
|
|
if let dir = environment["ICCERY_ARGYLL_BINARY_DIR"], !dir.isEmpty {
|
|
overrideDir = URL(fileURLWithPath: dir)
|
|
}
|
|
if let dir = environment["ICCERY_ARGYLL_BUNDLED_ROOT"], !dir.isEmpty {
|
|
bundledRoot = URL(fileURLWithPath: dir)
|
|
}
|
|
if let dir = environment["ICCERY_CUPS_BIN_DIR"], !dir.isEmpty {
|
|
cupsDir = URL(fileURLWithPath: dir)
|
|
}
|
|
// #214 — fixture PPD dir lets UI tests exercise the media→
|
|
// quality constraint resolver without installed drivers.
|
|
if let dir = environment["ICCERY_CUPS_PPD_DIR"], !dir.isEmpty {
|
|
ppdDir = URL(fileURLWithPath: dir)
|
|
}
|
|
#endif
|
|
return AppEnvironment(
|
|
stateStore: WizardStateStore(),
|
|
settingsStore: settingsStore,
|
|
presetStore: PresetStore(settingsStore: settingsStore),
|
|
runner: ArgyllRunner(
|
|
processManager: .shared,
|
|
binaryResolver: BinaryResolver(
|
|
bundledRoot: bundledRoot, overrideDir: overrideDir)
|
|
),
|
|
cupsService: CupsService(
|
|
processManager: .shared,
|
|
binaryDir: cupsDir, ppdDir: ppdDir),
|
|
historyStore: VerificationHistoryStore(),
|
|
mediaStore: MediaLibraryStore(),
|
|
recentProjectsStore: RecentProjectsStore()
|
|
)
|
|
}
|
|
}
|
|
|
|
/// DEBUG-only UI-test hooks. When `ICCERY_UI_TESTING=1` the workflow
|
|
/// honours these env-provided paths instead of presenting modal panels
|
|
/// (XCUITest cannot drive NSOpenPanel/NSSavePanel reliably). These are
|
|
/// compiled out of release builds.
|
|
enum UITestHooks {
|
|
private static var env: [String: String] {
|
|
ProcessInfo.processInfo.environment
|
|
}
|
|
|
|
static var isEnabled: Bool {
|
|
#if DEBUG
|
|
return env["ICCERY_UI_TESTING"] == "1"
|
|
#else
|
|
return false
|
|
#endif
|
|
}
|
|
|
|
/// `select_target_file` result (Stage 1 save picker).
|
|
static var saveTargetURL: URL? { url("ICCERY_TEST_SAVE_TARGET") }
|
|
/// `select_existing_target` result (`.ti1`/`.ti2` resume).
|
|
static var existingTargetURL: URL? { url("ICCERY_TEST_EXISTING_TARGET") }
|
|
/// `select_directory` result (working-directory browse).
|
|
static var workDirURL: URL? { url("ICCERY_TEST_WORKDIR") }
|
|
/// Dataset import file (`.ti3`, `.txt`, `.cgats`, `.csv`).
|
|
static var datasetImportURL: URL? { url("ICCERY_TEST_DATASET_IMPORT") }
|
|
/// Preset import file.
|
|
static var presetImportURL: URL? { url("ICCERY_TEST_PRESET_IMPORT") }
|
|
/// Preset export destination.
|
|
static var presetExportURL: URL? { url("ICCERY_TEST_PRESET_EXPORT") }
|
|
/// Spot-read CSV export destination (`selectCsvSavePath`, #148).
|
|
static var csvExportURL: URL? { url("ICCERY_TEST_CSV_EXPORT") }
|
|
/// `.gam` compare picker result (gamut sheet, #147). Unset → cancel.
|
|
static var gamutFileURL: URL? { url("ICCERY_TEST_GAMUT_FILE") }
|
|
/// `.icc/.icm` compare picker result (gamut sheet, #147). Unset → cancel.
|
|
static var gamutProfileURL: URL? { url("ICCERY_TEST_GAMUT_PROFILE") }
|
|
/// TIFF sample picker result (gamut sheet, #147). Unset → cancel.
|
|
static var gamutTiffURL: URL? { url("ICCERY_TEST_GAMUT_TIFF") }
|
|
/// `selectProjectFile` result (`.icceryproj` open, #149). Unset → cancel.
|
|
static var projectOpenURL: URL? { url("ICCERY_TEST_PROJECT_OPEN") }
|
|
/// `selectProjectSavePath` result (`.icceryproj` save-as, #149).
|
|
static var projectSaveURL: URL? { url("ICCERY_TEST_PROJECT_SAVE") }
|
|
/// Relocate-folder result when a project's `cwd` is missing (#149).
|
|
static var projectRelocateURL: URL? { url("ICCERY_TEST_PROJECT_RELOCATE") }
|
|
/// Forces the gamut sheet into its no-Metal fallback even on a GPU
|
|
/// host (#147). Set per-test only — never in a default launch env,
|
|
/// or CI's future GPU run would skip SceneKit too.
|
|
static var skipSceneKit: Bool {
|
|
isEnabled && env["ICCERY_TEST_SKIP_SCENEKIT"] == "1"
|
|
}
|
|
|
|
// MARK: - Print panel / CUPS stubs (issue 13/17)
|
|
|
|
/// Directory of mock `lpstat`/`lpoptions` fixture scripts —
|
|
/// `CupsService.binaryDir` under UI tests.
|
|
static var cupsBinaryDir: URL? { url("ICCERY_CUPS_BIN_DIR") }
|
|
|
|
/// Path the `RecordingTargetSpooler` appends resolved ticket
|
|
/// lines to, for assertions (#201 D8 — replaces the `lp` argv
|
|
/// log).
|
|
static var spoolLogURL: URL? { url("ICCERY_TEST_SPOOL_LOG") }
|
|
|
|
/// Whether the `NSPrintPanel` should be stubbed under UI testing —
|
|
/// separate from the stub's *result* so "cancel" (`nil`) does not
|
|
/// fall through to the real modal.
|
|
static var printPanelStubbed: Bool { isEnabled }
|
|
|
|
/// Canned `NSPrintPanel` outcome — XCUITest cannot drive the
|
|
/// system modal. `ICCERY_TEST_PRINT_PANEL`:
|
|
/// - `cancel` (or unset while testing) → user cancelled → `nil`
|
|
/// - `ok` → `PrintPropertiesResult` with
|
|
/// `ICCERY_TEST_PANEL_OPTIONS` (captured `k=v` string) and
|
|
/// `ICCERY_TEST_PANEL_PRINTER` (selected queue; default = the
|
|
/// queue the panel was opened for).
|
|
static func printPanelResult(
|
|
forQueue queue: String,
|
|
trays: [PrinterTray] = [],
|
|
preferredTrayKey: String? = nil
|
|
) -> PrintPropertiesResult? {
|
|
switch env["ICCERY_TEST_PRINT_PANEL"] {
|
|
case "ok":
|
|
let options = env["ICCERY_TEST_PANEL_OPTIONS"].flatMap {
|
|
$0.isEmpty ? nil : $0
|
|
}
|
|
let trayToken = options.flatMap {
|
|
CupsParsers.extractTray(
|
|
fromOptionsString: $0, preferredKey: preferredTrayKey)
|
|
}
|
|
let paperSource = trays.first(where: {
|
|
$0.token == trayToken
|
|
}).map(\.id)
|
|
return PrintPropertiesResult(
|
|
selectedPrinter: env["ICCERY_TEST_PANEL_PRINTER"].flatMap {
|
|
$0.isEmpty ? nil : $0
|
|
} ?? queue,
|
|
options: PrintOptions(
|
|
paperSource: paperSource,
|
|
orientation: options.flatMap {
|
|
CupsParsers.extractOrientation(fromOptionsString: $0)
|
|
},
|
|
paperSize: options.flatMap {
|
|
CupsParsers.extractOption(
|
|
named: "PageSize", fromOptionsString: $0)
|
|
},
|
|
mediaType: options.flatMap {
|
|
CupsParsers.extractMediaType(fromOptionsString: $0)
|
|
},
|
|
quality: options.flatMap {
|
|
CupsParsers.extractQuality(fromOptionsString: $0)
|
|
},
|
|
ppdUncorrectedPassthrough: true,
|
|
cupsOptions: options))
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private static func url(_ key: String) -> URL? {
|
|
guard let raw = env[key], !raw.isEmpty else { return nil }
|
|
return URL(fileURLWithPath: raw)
|
|
}
|
|
}
|