Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
597cce7b60 | ||
|
|
bb4512e129 |
@@ -14,7 +14,9 @@ extension TargenConfig {
|
|||||||
neutralSteps: preset.neutralSteps,
|
neutralSteps: preset.neutralSteps,
|
||||||
neutralConcentration: preset.neutralConcentration,
|
neutralConcentration: preset.neutralConcentration,
|
||||||
preconditioningProfile: preset.preconditioningProfile,
|
preconditioningProfile: preset.preconditioningProfile,
|
||||||
ofpsHighQuality: preset.ofpsHighQuality == true ? true : nil,
|
// An explicit `false` is preserved — distinguishable from a
|
||||||
|
// missing key; `-G` is only emitted for `true` (#82).
|
||||||
|
ofpsHighQuality: preset.ofpsHighQuality,
|
||||||
ofpsAdaptation: preset.ofpsAdaptation,
|
ofpsAdaptation: preset.ofpsAdaptation,
|
||||||
fullSpreadAlgorithm: preset.fullSpreadAlgorithm.flatMap { FullSpreadAlgorithm(presetValue: $0) }.flatMap { $0 == .ofps ? nil : $0 },
|
fullSpreadAlgorithm: preset.fullSpreadAlgorithm.flatMap { FullSpreadAlgorithm(presetValue: $0) }.flatMap { $0 == .ofps ? nil : $0 },
|
||||||
totalInkLimit: preset.totalInkLimit,
|
totalInkLimit: preset.totalInkLimit,
|
||||||
@@ -109,6 +111,51 @@ extension ColprofConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// User-facing FWA selection for the Stage 4 form, plus the two
|
||||||
|
/// directions of `colprof_fwa` conversion centralised here so the view
|
||||||
|
/// models carry no mapping switches of their own (#82).
|
||||||
|
public enum ColprofFwaSelection: String, CaseIterable, Sendable, Equatable {
|
||||||
|
case none = "none"
|
||||||
|
case empty = ""
|
||||||
|
case D50 = "D50"
|
||||||
|
case D65 = "D65"
|
||||||
|
case custom = "custom"
|
||||||
|
|
||||||
|
public var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .none: return "None"
|
||||||
|
case .empty: return "Bare (-f)"
|
||||||
|
case .D50: return "D50"
|
||||||
|
case .D65: return "D65"
|
||||||
|
case .custom: return "Custom .sp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preset `colprof_fwa` → selection. `nil`/`"none"` map to `.none`,
|
||||||
|
/// `""` to `.empty`, `D50`/`D65` case-insensitively, and any other
|
||||||
|
/// string is a custom `.sp` path.
|
||||||
|
public init(presetValue: String?) {
|
||||||
|
switch presetValue?.lowercased() {
|
||||||
|
case nil, "none": self = .none
|
||||||
|
case "": self = .empty
|
||||||
|
case "d50": self = .D50
|
||||||
|
case "d65": self = .D65
|
||||||
|
default: self = .custom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selection → `colprof_fwa` value. `.custom` returns `customPath`.
|
||||||
|
public func presetValue(customPath: String) -> String? {
|
||||||
|
switch self {
|
||||||
|
case .none: return nil
|
||||||
|
case .empty: return ""
|
||||||
|
case .D50: return "D50"
|
||||||
|
case .D65: return "D65"
|
||||||
|
case .custom: return customPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension PageSize {
|
extension PageSize {
|
||||||
/// `"210x297"` custom page parse used by presets (issue #82).
|
/// `"210x297"` custom page parse used by presets (issue #82).
|
||||||
public static func parseCustom(_ raw: String) -> (Double, Double)? {
|
public static func parseCustom(_ raw: String) -> (Double, Double)? {
|
||||||
|
|||||||
@@ -3,25 +3,6 @@ import Observation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ICCeryCore
|
import ICCeryCore
|
||||||
|
|
||||||
/// User-facing FWA selection for the Stage 4 form.
|
|
||||||
enum ColprofFwaSelection: String, CaseIterable, Sendable, Equatable {
|
|
||||||
case none = "none"
|
|
||||||
case empty = ""
|
|
||||||
case D50 = "D50"
|
|
||||||
case D65 = "D65"
|
|
||||||
case custom = "custom"
|
|
||||||
|
|
||||||
var displayName: String {
|
|
||||||
switch self {
|
|
||||||
case .none: return "None"
|
|
||||||
case .empty: return "Bare (-f)"
|
|
||||||
case .D50: return "D50"
|
|
||||||
case .D65: return "D65"
|
|
||||||
case .custom: return "Custom .sp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stage 4/5 workflow: build a profile, verify it, track drift, and install.
|
/// Stage 4/5 workflow: build a profile, verify it, track drift, and install.
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
@@ -110,13 +91,7 @@ final class ProfileWorkflowViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fwaValue: String? {
|
var fwaValue: String? {
|
||||||
switch fwaSelection {
|
fwaSelection.presetValue(customPath: fwaCustomPath)
|
||||||
case .none: return nil
|
|
||||||
case .empty: return ""
|
|
||||||
case .D50: return "D50"
|
|
||||||
case .D65: return "D65"
|
|
||||||
case .custom: return fwaCustomPath
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Preset application
|
// MARK: - Preset application
|
||||||
@@ -131,21 +106,16 @@ final class ProfileWorkflowViewModel {
|
|||||||
algorithm = config.algorithm
|
algorithm = config.algorithm
|
||||||
quality = config.quality
|
quality = config.quality
|
||||||
intent = config.intent ?? ""
|
intent = config.intent ?? ""
|
||||||
if let fwa = config.fwa {
|
fwaSelection = ColprofFwaSelection(presetValue: config.fwa)
|
||||||
switch fwa.lowercased() {
|
fwaCustomPath = fwaSelection == .custom ? (config.fwa ?? "") : ""
|
||||||
case "none": fwaSelection = .none
|
|
||||||
case "": fwaSelection = .empty
|
|
||||||
case "d50": fwaSelection = .D50
|
|
||||||
case "d65": fwaSelection = .D65
|
|
||||||
default:
|
|
||||||
fwaSelection = .custom
|
|
||||||
fwaCustomPath = fwa
|
|
||||||
}
|
|
||||||
}
|
|
||||||
illuminant = config.illuminant ?? ""
|
illuminant = config.illuminant ?? ""
|
||||||
observer = config.observer ?? ""
|
observer = config.observer ?? ""
|
||||||
inputViewingCond = config.inputViewingCond ?? ""
|
inputViewingCond = config.inputViewingCond ?? ""
|
||||||
outputViewingCond = config.outputViewingCond ?? ""
|
outputViewingCond = config.outputViewingCond ?? ""
|
||||||
|
profileDescription = ""
|
||||||
|
copyright = ""
|
||||||
|
applyCalibration = preset.applyCalibration == true
|
||||||
|
calibrationFile = preset.calibrationFile ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stage 4 form values for saving into a custom preset.
|
/// Stage 4 form values for saving into a custom preset.
|
||||||
|
|||||||
@@ -380,14 +380,18 @@ final class TargetWorkflowViewModel {
|
|||||||
func applyPreset(_ preset: ProfilingPreset) {
|
func applyPreset(_ preset: ProfilingPreset) {
|
||||||
let targen = TargenConfig(preset: preset, basename: targetBasename, workingDirectory: targetDirectory)
|
let targen = TargenConfig(preset: preset, basename: targetBasename, workingDirectory: targetDirectory)
|
||||||
applyTargenForm(targen)
|
applyTargenForm(targen)
|
||||||
|
// Stage 4 state (incl. calibration) is applied before Stage 2 so
|
||||||
|
// the layout config receives the preset's calibration path, not
|
||||||
|
// stale live state (#82).
|
||||||
|
profile.applyPreset(preset)
|
||||||
let printtarg = PrinttargConfig(
|
let printtarg = PrinttargConfig(
|
||||||
preset: preset,
|
preset: preset,
|
||||||
basename: wizard.basename,
|
basename: wizard.basename,
|
||||||
workingDirectory: wizard.effectiveWorkingDirectory,
|
workingDirectory: wizard.effectiveWorkingDirectory,
|
||||||
calibrationFile: profile.applyCalibration ? profile.calibrationFile : nil
|
calibrationFile: profile.applyCalibration && !profile.calibrationFile.isEmpty
|
||||||
|
? profile.calibrationFile : nil
|
||||||
)
|
)
|
||||||
applyPrinttargForm(printtarg)
|
applyPrinttargForm(printtarg)
|
||||||
profile.applyPreset(preset)
|
|
||||||
selectedPresetID = preset.id
|
selectedPresetID = preset.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,4 +287,210 @@ struct PresetMappingTests {
|
|||||||
#expect(back.colprofFwa == "D50")
|
#expect(back.colprofFwa == "D50")
|
||||||
#expect(back.greySteps == nil)
|
#expect(back.greySteps == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("Full preset round-trips through all three configs with every field asserted")
|
||||||
|
func fullRoundTrip() {
|
||||||
|
let preset = ProfilingPreset(
|
||||||
|
id: "custom-full",
|
||||||
|
name: "Full",
|
||||||
|
description: "All fields",
|
||||||
|
colourSpace: "cmyk",
|
||||||
|
patchCount: 1500,
|
||||||
|
whitePatches: 6,
|
||||||
|
blackPatches: 8,
|
||||||
|
greySteps: 9,
|
||||||
|
singleChannelSteps: 7,
|
||||||
|
neutralSteps: 4,
|
||||||
|
neutralConcentration: 0.7,
|
||||||
|
preconditioningProfile: "/tmp/pre.icm",
|
||||||
|
ofpsHighQuality: true,
|
||||||
|
ofpsAdaptation: 0.2,
|
||||||
|
fullSpreadAlgorithm: "R",
|
||||||
|
totalInkLimit: 280,
|
||||||
|
darkEmphasis: 1.3,
|
||||||
|
devicePower: 1.2,
|
||||||
|
instrument: "p3",
|
||||||
|
pageSize: "250x300",
|
||||||
|
bitDepth: 16,
|
||||||
|
dpi: 360,
|
||||||
|
randomSeed: 42,
|
||||||
|
noRandomize: false,
|
||||||
|
calibrationFile: "/tmp/a.cal",
|
||||||
|
applyCalibration: true,
|
||||||
|
colprofAlgorithm: "x",
|
||||||
|
colprofQuality: "u",
|
||||||
|
colprofIntent: "p",
|
||||||
|
colprofFwa: "D65",
|
||||||
|
colprofIlluminant: "D65",
|
||||||
|
colprofObserver: "1931_2",
|
||||||
|
colprofInputViewingCond: "D50_2",
|
||||||
|
colprofOutputViewingCond: "D65_2"
|
||||||
|
)
|
||||||
|
|
||||||
|
let targen = TargenConfig(preset: preset, basename: "j", workingDirectory: nil)
|
||||||
|
#expect(targen.colourSpace == .cmyk)
|
||||||
|
#expect(targen.patchCount == 1500)
|
||||||
|
#expect(targen.whitePatches == 6)
|
||||||
|
#expect(targen.blackPatches == 8)
|
||||||
|
#expect(targen.greySteps == 9)
|
||||||
|
#expect(targen.singleChannelSteps == 7)
|
||||||
|
#expect(targen.neutralSteps == 4)
|
||||||
|
#expect(targen.neutralConcentration == 0.7)
|
||||||
|
#expect(targen.preconditioningProfile == "/tmp/pre.icm")
|
||||||
|
#expect(targen.ofpsHighQuality == true)
|
||||||
|
#expect(targen.ofpsAdaptation == 0.2)
|
||||||
|
#expect(targen.fullSpreadAlgorithm == .uniformRandom)
|
||||||
|
#expect(targen.totalInkLimit == 280)
|
||||||
|
#expect(targen.darkEmphasis == 1.3)
|
||||||
|
#expect(targen.devicePower == 1.2)
|
||||||
|
|
||||||
|
let printtarg = PrinttargConfig(
|
||||||
|
preset: preset,
|
||||||
|
basename: "j",
|
||||||
|
workingDirectory: nil,
|
||||||
|
calibrationFile: preset.calibrationFile
|
||||||
|
)
|
||||||
|
#expect(printtarg.instrument == .p3)
|
||||||
|
#expect(printtarg.pageSize == .custom)
|
||||||
|
#expect(printtarg.customPageWidth == 250)
|
||||||
|
#expect(printtarg.customPageHeight == 300)
|
||||||
|
#expect(printtarg.bitDepth == .sixteen)
|
||||||
|
#expect(printtarg.dpi == 360)
|
||||||
|
#expect(printtarg.layoutOrder == .customSeed)
|
||||||
|
#expect(printtarg.customSeed == 42)
|
||||||
|
#expect(printtarg.calibrationFile == "/tmp/a.cal")
|
||||||
|
|
||||||
|
let colprof = ColprofConfig(preset: preset, basename: "j", workingDirectory: nil)
|
||||||
|
#expect(colprof.algorithm == "x")
|
||||||
|
#expect(colprof.quality == "u")
|
||||||
|
#expect(colprof.intent == "p")
|
||||||
|
#expect(colprof.fwa == "D65")
|
||||||
|
#expect(colprof.illuminant == "D65")
|
||||||
|
#expect(colprof.observer == "1931_2")
|
||||||
|
#expect(colprof.inputViewingCond == "D50_2")
|
||||||
|
#expect(colprof.outputViewingCond == "D65_2")
|
||||||
|
|
||||||
|
let back = ProfilingPreset(
|
||||||
|
id: preset.id,
|
||||||
|
name: preset.name,
|
||||||
|
description: preset.description,
|
||||||
|
targen: targen,
|
||||||
|
printtarg: printtarg,
|
||||||
|
colprof: colprof,
|
||||||
|
calibrationFile: preset.calibrationFile,
|
||||||
|
applyCalibration: preset.applyCalibration
|
||||||
|
)
|
||||||
|
#expect(back == preset)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Every full-spread algorithm round-trips", arguments: [
|
||||||
|
("ofps", FullSpreadAlgorithm.ofps),
|
||||||
|
("t", .target),
|
||||||
|
("r", .random),
|
||||||
|
("R", .uniformRandom),
|
||||||
|
("q", .quasiRandom),
|
||||||
|
("Q", .uniformQuasiRandom),
|
||||||
|
("i", .invertedQuasiRandom),
|
||||||
|
("I", .invertedUniformQuasiRandom)
|
||||||
|
])
|
||||||
|
func fullSpreadAlgorithms(value: String, expected: FullSpreadAlgorithm) {
|
||||||
|
var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100)
|
||||||
|
preset.fullSpreadAlgorithm = value
|
||||||
|
let cfg = TargenConfig(preset: preset, basename: "t", workingDirectory: nil)
|
||||||
|
if expected == .ofps {
|
||||||
|
// ofps is the default — no flag emitted, stored value is nil.
|
||||||
|
#expect(cfg.fullSpreadAlgorithm == nil)
|
||||||
|
} else {
|
||||||
|
#expect(cfg.fullSpreadAlgorithm == expected)
|
||||||
|
}
|
||||||
|
let back = ProfilingPreset(
|
||||||
|
id: "x", name: "n", description: "",
|
||||||
|
targen: cfg,
|
||||||
|
printtarg: PrinttargConfig(
|
||||||
|
preset: preset, basename: "t",
|
||||||
|
workingDirectory: nil, calibrationFile: nil
|
||||||
|
),
|
||||||
|
colprof: ColprofConfig(preset: preset, basename: "t", workingDirectory: nil),
|
||||||
|
calibrationFile: nil,
|
||||||
|
applyCalibration: nil
|
||||||
|
)
|
||||||
|
#expect(back.fullSpreadAlgorithm == value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Explicit ofpsHighQuality=false is preserved, distinct from nil")
|
||||||
|
func ofpsHighQualityFalse() {
|
||||||
|
var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100)
|
||||||
|
preset.ofpsHighQuality = false
|
||||||
|
let cfg = TargenConfig(preset: preset, basename: "t", workingDirectory: nil)
|
||||||
|
#expect(cfg.ofpsHighQuality == false)
|
||||||
|
|
||||||
|
preset.ofpsHighQuality = nil
|
||||||
|
let nilCfg = TargenConfig(preset: preset, basename: "t", workingDirectory: nil)
|
||||||
|
#expect(nilCfg.ofpsHighQuality == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("noRandomize/seed layout mapping rules", arguments: [
|
||||||
|
(true, nil, LayoutOrder.raster, 1),
|
||||||
|
(true, 7, .raster, 7),
|
||||||
|
(false, nil, .deterministic, 1),
|
||||||
|
(false, 1, .deterministic, 1),
|
||||||
|
(nil, 1, .deterministic, 1),
|
||||||
|
(false, 5, .customSeed, 5)
|
||||||
|
] as [(Bool?, Int?, LayoutOrder, Int)])
|
||||||
|
func layoutMapping(noRandomize: Bool?, seed: Int?, layout: LayoutOrder, expectedSeed: Int) {
|
||||||
|
var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100)
|
||||||
|
preset.noRandomize = noRandomize
|
||||||
|
preset.randomSeed = seed
|
||||||
|
let cfg = PrinttargConfig(
|
||||||
|
preset: preset, basename: "t",
|
||||||
|
workingDirectory: nil, calibrationFile: nil
|
||||||
|
)
|
||||||
|
#expect(cfg.layoutOrder == layout)
|
||||||
|
#expect(cfg.customSeed == expectedSeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Custom page fallback matrix", arguments: [
|
||||||
|
("250x300", PageSize.custom, 250.0, 300.0),
|
||||||
|
("50x50", .custom, 50.0, 50.0),
|
||||||
|
("foo", .a4, 210.0, 297.0),
|
||||||
|
("30x40", .a4, 210.0, 297.0),
|
||||||
|
("210x", .a4, 210.0, 297.0)
|
||||||
|
] as [(String, PageSize, Double, Double)])
|
||||||
|
func customPageFallback(raw: String, page: PageSize, w: Double, h: Double) {
|
||||||
|
var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100)
|
||||||
|
preset.pageSize = raw
|
||||||
|
let cfg = PrinttargConfig(
|
||||||
|
preset: preset, basename: "t",
|
||||||
|
workingDirectory: nil, calibrationFile: nil
|
||||||
|
)
|
||||||
|
#expect(cfg.pageSize == page)
|
||||||
|
#expect(cfg.customPageWidth == w)
|
||||||
|
#expect(cfg.customPageHeight == h)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("FWA preset value → selection matrix", arguments: [
|
||||||
|
(nil, ColprofFwaSelection.none),
|
||||||
|
("none", .none),
|
||||||
|
("NONE", .none),
|
||||||
|
("", .empty),
|
||||||
|
("D50", .D50),
|
||||||
|
("d50", .D50),
|
||||||
|
("D65", .D65),
|
||||||
|
("d65", .D65),
|
||||||
|
("/tmp/fwa.sp", .custom)
|
||||||
|
] as [(String?, ColprofFwaSelection)])
|
||||||
|
func fwaToSelection(raw: String?, expected: ColprofFwaSelection) {
|
||||||
|
#expect(ColprofFwaSelection(presetValue: raw) == expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("FWA selection → preset value matrix", arguments: [
|
||||||
|
(ColprofFwaSelection.none, nil),
|
||||||
|
(.empty, ""),
|
||||||
|
(.D50, "D50"),
|
||||||
|
(.D65, "D65"),
|
||||||
|
(.custom, "/tmp/fwa.sp")
|
||||||
|
] as [(ColprofFwaSelection, String?)])
|
||||||
|
func fwaToPresetValue(selection: ColprofFwaSelection, expected: String?) {
|
||||||
|
#expect(selection.presetValue(customPath: "/tmp/fwa.sp") == expected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import Testing
|
||||||
|
import Foundation
|
||||||
|
@testable import ICCeryCore
|
||||||
|
@testable import ICCery
|
||||||
|
|
||||||
|
/// Issue #82 — preset application through the live view models, under an
|
||||||
|
/// isolated `TestAppEnvironment` (temp stores, fresh ProcessManager).
|
||||||
|
@Suite("PresetViewModelMapping")
|
||||||
|
@MainActor
|
||||||
|
struct PresetViewModelMappingTests {
|
||||||
|
|
||||||
|
private func makeWorkflow() throws -> (TestAppEnvironment, TargetWorkflowViewModel) {
|
||||||
|
let env = try TestAppEnvironment.make()
|
||||||
|
return (env, TargetWorkflowViewModel(environment: env.environment))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Applying a nil-FWA preset after a custom FWA clears the stale path")
|
||||||
|
func nilFwaClearsCustomPath() throws {
|
||||||
|
let (env, vm) = try makeWorkflow()
|
||||||
|
defer { env.cleanup() }
|
||||||
|
|
||||||
|
var customPreset = ProfilingPreset(
|
||||||
|
id: "c-fwa", name: "FWA", patchCount: 800,
|
||||||
|
colprofFwa: "/tmp/fwa.sp"
|
||||||
|
)
|
||||||
|
vm.applyPreset(customPreset)
|
||||||
|
#expect(vm.profile.fwaSelection == .custom)
|
||||||
|
#expect(vm.profile.fwaCustomPath == "/tmp/fwa.sp")
|
||||||
|
|
||||||
|
customPreset.colprofFwa = nil
|
||||||
|
vm.applyPreset(customPreset)
|
||||||
|
#expect(vm.profile.fwaSelection == .none)
|
||||||
|
#expect(vm.profile.fwaCustomPath == "")
|
||||||
|
#expect(vm.profile.fwaValue == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Custom FWA preset path survives the round-trip to colprof_fwa")
|
||||||
|
func customFwaRoundTrip() throws {
|
||||||
|
let (env, vm) = try makeWorkflow()
|
||||||
|
defer { env.cleanup() }
|
||||||
|
|
||||||
|
let preset = ProfilingPreset(
|
||||||
|
id: "c-fwa2", name: "FWA2", patchCount: 800,
|
||||||
|
colprofFwa: "/tmp/other.sp"
|
||||||
|
)
|
||||||
|
vm.applyPreset(preset)
|
||||||
|
#expect(vm.profile.fwaSelection == .custom)
|
||||||
|
#expect(vm.profile.fwaCustomPath == "/tmp/other.sp")
|
||||||
|
#expect(vm.profile.fwaValue == "/tmp/other.sp")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Preset calibration reaches Stage 2 instead of stale live state")
|
||||||
|
func presetCalibrationReachesStage2() throws {
|
||||||
|
let (env, vm) = try makeWorkflow()
|
||||||
|
defer { env.cleanup() }
|
||||||
|
|
||||||
|
// Stale live state must not leak into the preset-applied layout.
|
||||||
|
vm.profile.applyCalibration = true
|
||||||
|
vm.profile.calibrationFile = "/tmp/stale.cal"
|
||||||
|
|
||||||
|
let preset = ProfilingPreset(
|
||||||
|
id: "c-cal", name: "Cal", patchCount: 800,
|
||||||
|
calibrationFile: "/tmp/preset.cal",
|
||||||
|
applyCalibration: true
|
||||||
|
)
|
||||||
|
vm.applyPreset(preset)
|
||||||
|
|
||||||
|
#expect(vm.profile.applyCalibration)
|
||||||
|
#expect(vm.profile.calibrationFile == "/tmp/preset.cal")
|
||||||
|
#expect(vm.buildPrinttargConfig().calibrationFile == "/tmp/preset.cal")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Preset with calibration disabled clears Stage 2 calibration")
|
||||||
|
func disabledCalibrationClearsStage2() throws {
|
||||||
|
let (env, vm) = try makeWorkflow()
|
||||||
|
defer { env.cleanup() }
|
||||||
|
|
||||||
|
vm.profile.applyCalibration = true
|
||||||
|
vm.profile.calibrationFile = "/tmp/stale.cal"
|
||||||
|
|
||||||
|
let preset = ProfilingPreset(
|
||||||
|
id: "c-nocal", name: "NoCal", patchCount: 800,
|
||||||
|
calibrationFile: "/tmp/preset.cal",
|
||||||
|
applyCalibration: nil
|
||||||
|
)
|
||||||
|
vm.applyPreset(preset)
|
||||||
|
|
||||||
|
#expect(!vm.profile.applyCalibration)
|
||||||
|
#expect(vm.buildPrinttargConfig().calibrationFile == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Preset Stage 1/2 form fields apply to the live form")
|
||||||
|
func formFieldsApply() throws {
|
||||||
|
let (env, vm) = try makeWorkflow()
|
||||||
|
defer { env.cleanup() }
|
||||||
|
|
||||||
|
var preset = ProfilingPreset(
|
||||||
|
id: "c-form", name: "Form",
|
||||||
|
colourSpace: "cmyk", patchCount: 1500,
|
||||||
|
whitePatches: 6,
|
||||||
|
blackPatches: 8,
|
||||||
|
greySteps: 9,
|
||||||
|
fullSpreadAlgorithm: "r",
|
||||||
|
pageSize: "250x300",
|
||||||
|
dpi: 150
|
||||||
|
)
|
||||||
|
vm.applyPreset(preset)
|
||||||
|
|
||||||
|
#expect(vm.colourSpace == .cmyk)
|
||||||
|
#expect(vm.effectivePatchCount == 1500)
|
||||||
|
#expect(vm.whitePatches == 6)
|
||||||
|
#expect(vm.blackPatches == 8)
|
||||||
|
#expect(vm.greyStepsEnabled && vm.greySteps == 9)
|
||||||
|
#expect(vm.algorithm == .random)
|
||||||
|
#expect(vm.tiffDpi == 150)
|
||||||
|
#expect(vm.pageSize == .custom)
|
||||||
|
#expect(vm.customPageW == 250 && vm.customPageH == 300)
|
||||||
|
#expect(vm.selectedPresetID == "c-form")
|
||||||
|
|
||||||
|
// Disabled advanced controls stay nil in the snapshot, not
|
||||||
|
// numeric sentinels.
|
||||||
|
preset.greySteps = nil
|
||||||
|
vm.applyPreset(preset)
|
||||||
|
#expect(!vm.greyStepsEnabled)
|
||||||
|
#expect(vm.buildTargenConfig().greySteps == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import Testing
|
||||||
|
import Foundation
|
||||||
|
@testable import ICCeryCore
|
||||||
|
@testable import ICCery
|
||||||
|
|
||||||
|
/// Shared app-test dependency factory (issue #82).
|
||||||
|
///
|
||||||
|
/// Every store is pointed at a unique temporary directory so tests never
|
||||||
|
/// read or write the user's real Application Support tree, and a fresh
|
||||||
|
/// `ProcessManager` keeps child-process state isolated per test. The
|
||||||
|
/// global process environment is never mutated.
|
||||||
|
struct TestAppEnvironment {
|
||||||
|
|
||||||
|
/// Root temp directory holding all per-test state files.
|
||||||
|
let root: URL
|
||||||
|
let environment: AppEnvironment
|
||||||
|
|
||||||
|
var settingsURL: URL { root.appendingPathComponent("settings.json") }
|
||||||
|
var stateURL: URL { root.appendingPathComponent("wizard_state.json") }
|
||||||
|
var historyURL: URL {
|
||||||
|
root.appendingPathComponent("verification_history.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an isolated environment under `NSTemporaryDirectory()`.
|
||||||
|
/// Call `cleanup()` when finished.
|
||||||
|
static func make() throws -> TestAppEnvironment {
|
||||||
|
let root = FileManager.default.temporaryDirectory
|
||||||
|
.appendingPathComponent("iccery-test-env-\(UUID().uuidString)")
|
||||||
|
try FileManager.default.createDirectory(
|
||||||
|
at: root, withIntermediateDirectories: true
|
||||||
|
)
|
||||||
|
|
||||||
|
let processManager = ProcessManager()
|
||||||
|
let settingsStore = SettingsStore(
|
||||||
|
fileURL: root.appendingPathComponent("settings.json")
|
||||||
|
)
|
||||||
|
let environment = AppEnvironment(
|
||||||
|
stateStore: WizardStateStore(
|
||||||
|
fileURL: root.appendingPathComponent("wizard_state.json")
|
||||||
|
),
|
||||||
|
settingsStore: settingsStore,
|
||||||
|
presetStore: PresetStore(settingsStore: settingsStore),
|
||||||
|
runner: ArgyllRunner(
|
||||||
|
processManager: processManager,
|
||||||
|
binaryResolver: BinaryResolver(overrideDir: nil)
|
||||||
|
),
|
||||||
|
cupsService: CupsService(
|
||||||
|
processManager: processManager,
|
||||||
|
binaryDir: root.appendingPathComponent("cups-bin")
|
||||||
|
),
|
||||||
|
historyStore: VerificationHistoryStore(
|
||||||
|
url: root.appendingPathComponent("verification_history.json")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return TestAppEnvironment(root: root, environment: environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the temporary root directory.
|
||||||
|
func cleanup() {
|
||||||
|
try? FileManager.default.removeItem(at: root)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user