fix(ui): calibrate dashboard no longer overflows window; Escape exits (#163) #164
@@ -7,48 +7,90 @@ struct CalibrationView: View {
|
|||||||
@ObservedObject var wizard: WizardViewModel
|
@ObservedObject var wizard: WizardViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
Text("Calibrate Printer")
|
Text("Calibrate Printer")
|
||||||
.font(.title2.bold())
|
.font(.title2.bold())
|
||||||
.padding(.horizontal, 16)
|
.foregroundStyle(Theme.text)
|
||||||
.padding(.top, 16)
|
wedgeSection
|
||||||
|
workflowSection
|
||||||
|
if !model.calibrationLog.isEmpty { logSection }
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
.background(Theme.background)
|
||||||
|
|
||||||
Form {
|
Divider().overlay(Theme.border)
|
||||||
Section("Wedge Settings") {
|
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button("Return to Profiling", role: .cancel) { model.returnToProfiling() }
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
.accessibilityIdentifier("btnCalReturn")
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
}
|
||||||
|
.accessibilityElement(children: .contain)
|
||||||
|
.accessibilityIdentifier("stage-cal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Wedge settings
|
||||||
|
|
||||||
|
private var wedgeSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Wedge Settings").font(.headline).foregroundStyle(Theme.text)
|
||||||
Picker("Colour Space", selection: $model.colourSpace) {
|
Picker("Colour Space", selection: $model.colourSpace) {
|
||||||
Text("RGB").tag(ColourSpace.rgb)
|
Text("RGB").tag(ColourSpace.rgb)
|
||||||
Text("CMYK").tag(ColourSpace.cmyk)
|
Text("CMYK").tag(ColourSpace.cmyk)
|
||||||
}
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
.frame(maxWidth: 220)
|
||||||
|
|
||||||
HStack {
|
HStack(spacing: 12) {
|
||||||
Text("Steps per channel")
|
Text("Steps per channel")
|
||||||
Spacer()
|
.foregroundStyle(Theme.text)
|
||||||
|
.frame(width: 140, alignment: .leading)
|
||||||
TextField("", value: $model.steps, format: .number)
|
TextField("", value: $model.steps, format: .number)
|
||||||
.frame(width: 60)
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.frame(width: 70)
|
||||||
.accessibilityIdentifier("calSteps")
|
.accessibilityIdentifier("calSteps")
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack(spacing: 12) {
|
||||||
Text("White patches")
|
Text("White patches")
|
||||||
Spacer()
|
.foregroundStyle(Theme.text)
|
||||||
|
.frame(width: 140, alignment: .leading)
|
||||||
TextField("", value: $model.whitePatches, format: .number)
|
TextField("", value: $model.whitePatches, format: .number)
|
||||||
.frame(width: 60)
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.frame(width: 70)
|
||||||
}
|
}
|
||||||
|
|
||||||
if model.colourSpace == .cmyk {
|
if model.colourSpace == .cmyk {
|
||||||
HStack {
|
HStack(spacing: 12) {
|
||||||
Text("Ink-limit exploration")
|
Text("Ink-limit exploration")
|
||||||
Spacer()
|
.foregroundStyle(Theme.text)
|
||||||
|
.frame(width: 140, alignment: .leading)
|
||||||
TextField("", text: $model.inkLimit)
|
TextField("", text: $model.inkLimit)
|
||||||
.frame(width: 60)
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.frame(width: 70)
|
||||||
.accessibilityIdentifier("calInkExplore")
|
.accessibilityIdentifier("calInkExplore")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Toggle("Neutral emphasis", isOn: $model.includeNeutralEmphasis)
|
Toggle("Neutral emphasis", isOn: $model.includeNeutralEmphasis)
|
||||||
|
.toggleStyle(.checkbox)
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
.accessibilityIdentifier("calNeutralEmphasis")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Workflow") {
|
// MARK: - Workflow
|
||||||
|
|
||||||
|
private var workflowSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Workflow").font(.headline).foregroundStyle(Theme.text)
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Button("Generate Target") { model.generateTarget() }
|
Button("Generate Target") { model.generateTarget() }
|
||||||
.accessibilityIdentifier("btnCalGenerate")
|
.accessibilityIdentifier("btnCalGenerate")
|
||||||
@@ -73,6 +115,8 @@ struct CalibrationView: View {
|
|||||||
|
|
||||||
if let url = model.computedCalURL {
|
if let url = model.computedCalURL {
|
||||||
Toggle("Apply calibration to next profile", isOn: $model.applyToProfile)
|
Toggle("Apply calibration to next profile", isOn: $model.applyToProfile)
|
||||||
|
.toggleStyle(.checkbox)
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
.onChange(of: model.applyToProfile) { _ in model.updateApplyToProfile() }
|
.onChange(of: model.applyToProfile) { _ in model.updateApplyToProfile() }
|
||||||
.accessibilityIdentifier("calApplyToggle")
|
.accessibilityIdentifier("calApplyToggle")
|
||||||
|
|
||||||
@@ -81,28 +125,17 @@ struct CalibrationView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !model.calibrationLog.isEmpty {
|
|
||||||
Section("Log") {
|
|
||||||
ScrollView {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
ForEach(model.calibrationLog, id: \.self) { line in
|
|
||||||
Text(line)
|
|
||||||
.font(.system(.caption, design: .monospaced))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(minHeight: 80, maxHeight: 120)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
// MARK: - Log
|
||||||
Spacer()
|
|
||||||
Button("Return to Profiling") { model.returnToProfiling() }
|
private var logSection: some View {
|
||||||
.accessibilityIdentifier("btnCalReturn")
|
ProcessLogView(
|
||||||
}
|
lines: model.calibrationLog,
|
||||||
.padding(16)
|
minHeight: 80,
|
||||||
}
|
maxHeight: 120,
|
||||||
|
containerId: "calLogContainer",
|
||||||
|
logId: "calLog"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ struct RootView: View {
|
|||||||
}
|
}
|
||||||
WizardStageContent(model: model, workflow: workflow)
|
WizardStageContent(model: model, workflow: workflow)
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
.frame(minWidth: 1100, minHeight: 700)
|
.frame(minWidth: 1100, minHeight: 700)
|
||||||
.background(Theme.background)
|
.background(Theme.background)
|
||||||
|
|||||||
@@ -86,6 +86,60 @@ final class Milestone6CalibrationUITests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stage 0 must not push the sidebar off-screen: the macOS `Form`
|
||||||
|
/// rows with expanding spacers once gave the stage an unbounded ideal
|
||||||
|
/// width, and window centering shifted the 270 pt sidebar into
|
||||||
|
/// negative X (issue #163). AX-tree existence checks cannot see that,
|
||||||
|
/// so assert real frame geometry.
|
||||||
|
func testCalibrationViewDoesNotOverflowWindow() throws {
|
||||||
|
let calButton = app.buttons["btnCalibratePrinter"]
|
||||||
|
XCTAssertTrue(calButton.waitForExistence(timeout: 10))
|
||||||
|
calButton.tap()
|
||||||
|
|
||||||
|
XCTAssertTrue(app.staticTexts["Calibrate Printer"].waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
XCTAssertTrue(window.exists)
|
||||||
|
XCTAssertGreaterThanOrEqual(calButton.frame.minX, 0)
|
||||||
|
XCTAssertLessThanOrEqual(calButton.frame.maxX, window.frame.maxX)
|
||||||
|
let ret = app.buttons["btnCalReturn"]
|
||||||
|
XCTAssertTrue(ret.waitForExistence(timeout: 5))
|
||||||
|
XCTAssertTrue(ret.isHittable)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Return to Profiling" is the single Stage 0 exit and carries the
|
||||||
|
/// cancel-action shortcut, so Escape must dismiss the dashboard too
|
||||||
|
/// (issue #163). `typeKey` delivery is unreliable on the macOS 12 CI
|
||||||
|
/// runner (m10 phase-08), so the Escape check falls back to the
|
||||||
|
/// deterministic button tap.
|
||||||
|
func testCalibrationReturnButtonAndEscapeDismiss() throws {
|
||||||
|
let calButton = app.buttons["btnCalibratePrinter"]
|
||||||
|
XCTAssertTrue(calButton.waitForExistence(timeout: 10))
|
||||||
|
calButton.tap()
|
||||||
|
XCTAssertTrue(app.staticTexts["Calibrate Printer"].waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
let returnButton = app.buttons["btnCalReturn"]
|
||||||
|
XCTAssertTrue(returnButton.waitForExistence(timeout: 5))
|
||||||
|
XCTAssertTrue(returnButton.isHittable)
|
||||||
|
returnButton.tap()
|
||||||
|
|
||||||
|
let stage1 = app.descendants(matching: .any)["stage-1"]
|
||||||
|
XCTAssertTrue(stage1.waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
// Re-enter and try Escape; fall back to the button where the
|
||||||
|
// runtime does not deliver typeKey.
|
||||||
|
XCTAssertTrue(calButton.waitForExistence(timeout: 5))
|
||||||
|
calButton.tap()
|
||||||
|
XCTAssertTrue(app.staticTexts["Calibrate Printer"].waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
app.typeKey(XCUIKeyboardKey.escape, modifierFlags: [])
|
||||||
|
if !stage1.waitForExistence(timeout: 4) {
|
||||||
|
XCTAssertTrue(returnButton.waitForExistence(timeout: 5))
|
||||||
|
returnButton.tap()
|
||||||
|
XCTAssertTrue(stage1.waitForExistence(timeout: 5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A failing calibration targen surfaces the error through the
|
/// A failing calibration targen surfaces the error through the
|
||||||
/// wizard notice and restores the original basename (issue #80).
|
/// wizard notice and restores the original basename (issue #80).
|
||||||
func testCalibrationTargenFailureRestoresBasename() throws {
|
func testCalibrationTargenFailureRestoresBasename() throws {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user