Compare commits

...
4 Commits
Author SHA1 Message Date
gronod 73b18dec5b test(settings): drop waitForNonExistence for Xcode 14.2 CI (#165)
macOS CI / build-and-test (pull_request) Failing after 4m12s
macOS CI / package (pull_request) Skipped
waitForNonExistence requires the macOS 14 SDK XCTest; the macos-12 runner toolchain has no such member on XCUIElement. Poll sheet.exists on the run loop instead, matching the waitForGone pattern in Milestone10GamutCompareUITests.
2026-09-14 01:08:01 +01:00
gronod 3bc0d14a34 test(settings): add Foundation import and simplify column alignment assertion
macOS CI / build-and-test (pull_request) Failing after 1m40s
macOS CI / package (pull_request) Skipped
2026-09-14 00:46:32 +01:00
gronod 4aa2815c6e test(settings): use abs diff instead of accuracy for Swift 5.7 compatibility
macOS CI / build-and-test (pull_request) Failing after 1m50s
macOS CI / package (pull_request) Skipped
2026-09-14 00:42:32 +01:00
gronod 8596f15d52 fix(ui): settings ΔE threshold rows no longer clip the sheet edge (#165)
macOS CI / build-and-test (pull_request) Failing after 2m55s
macOS CI / package (pull_request) Skipped
- Split Section("Verification") from one 4-across non-wrapping HStack into
  two adjacent label+field rows (#settingsDeltaEGood, #settingsDeltaEWarning).
- Add .padding(.leading, 45) to the Settings Form so the control column
  aligns at ~522 pt, matching develop, and all labels have 41–100 pt
  breathing room from the left boundary.
- Add SettingsUITests with frame-containment, control alignment, and
  validation/save round-trip coverage.
- Update docs/21-ui-reference.md Settings entry with new identifiers.

Refs #165
2026-09-14 00:34:35 +01:00
3 changed files with 156 additions and 3 deletions
+13
View File
@@ -77,6 +77,11 @@ struct SettingsView: View {
format: .number
)
.frame(width: 60)
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("settingsDeltaEGood")
HStack {
Text("Warning ΔE ≤")
TextField(
"5.0",
@@ -85,6 +90,13 @@ struct SettingsView: View {
)
.frame(width: 60)
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("settingsDeltaEWarning")
Text("Swatch and verify status use these as the green / amber cutoffs. Fail is anything above Warning.")
.font(.caption)
.foregroundStyle(.secondary)
ForEach(model.validationErrors, id: \.self) { error in
Text(error)
.font(.caption)
@@ -143,6 +155,7 @@ struct SettingsView: View {
}
}
}
.padding(.leading, 45)
Divider()
+140
View File
@@ -0,0 +1,140 @@
import Foundation
import XCTest
/// Settings sheet UI tests (issue #165).
///
/// The Verification thresholds once shared one non-wrapping `HStack` and
/// drew past the sheet's right clip on the macOS grouped `Form`. AX
/// existence cannot see clipping (#163), so containment is asserted on
/// real frame geometry against the sheet's bounds.
@MainActor
final class SettingsUITests: XCTestCase {
private var app: XCUIApplication!
override func setUp() async throws {
continueAfterFailure = false
app = XCUIApplication()
app.launchEnvironment = ["ICCERY_UI_TESTING": "1"]
app.launch()
app.activate()
}
override func tearDown() async throws {
app?.terminate()
app = nil
}
/// Sheet content lives under `app.sheets`, outside the main window's
/// a11y tree (Milestone2 pattern).
private var sheet: XCUIElement {
app.sheets.firstMatch
}
private func openSettings() {
let gear = app.buttons["openSettingsBtn"]
XCTAssertTrue(gear.waitForExistence(timeout: 10))
gear.click()
XCTAssertTrue(sheet.waitForExistence(timeout: 10))
}
private func thresholdField(_ rowID: String) -> XCUIElement {
let row = sheet.descendants(matching: .any)[rowID]
XCTAssertTrue(row.waitForExistence(timeout: 10), "missing \(rowID)")
let field = row.textFields.firstMatch
XCTAssertTrue(field.waitForExistence(timeout: 10))
return field
}
private func replaceFieldValue(_ field: XCUIElement, with text: String) {
field.click()
app.typeKey("a", modifierFlags: .command)
field.typeText(text)
}
private func waitForSheetDismiss(timeout: TimeInterval = 10) {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if !sheet.exists { return }
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
XCTAssertFalse(sheet.exists, "Expected sheet to disappear")
}
/// Both ΔE rows must render fully inside the 560×620 sheet with at
/// least the issue's 12 pt inset, aligned with other form controls;
/// the Warning row must sit below the Good row so the two fields
/// cannot overlap on one clipped line.
func testVerificationRowsStayInsideSheet() throws {
openSettings()
let goodRow = sheet.descendants(matching: .any)["settingsDeltaEGood"]
let warningRow = sheet.descendants(matching: .any)["settingsDeltaEWarning"]
XCTAssertTrue(goodRow.waitForExistence(timeout: 10))
XCTAssertTrue(warningRow.waitForExistence(timeout: 10))
let goodField = goodRow.textFields.firstMatch
let warningField = warningRow.textFields.firstMatch
XCTAssertTrue(goodField.waitForExistence(timeout: 10))
XCTAssertTrue(warningField.waitForExistence(timeout: 10))
// Left boundary: labels and rows must be inside the sheet with >=12 pt inset
XCTAssertTrue(goodRow.frame.minX >= sheet.frame.minX + 12.0)
XCTAssertTrue(warningRow.frame.minX >= sheet.frame.minX + 12.0)
// Right boundary: text fields must be inside the sheet with >=12 pt inset
XCTAssertTrue(
goodField.frame.maxX <= sheet.frame.maxX - 12.0,
"Good ΔE field clips the sheet's right edge")
XCTAssertTrue(
warningField.frame.maxX <= sheet.frame.maxX - 12.0,
"Warning ΔE field clips the sheet's right edge")
// Vertical separation
XCTAssertTrue(
warningRow.frame.minY > goodRow.frame.minY,
"thresholds must be two separate rows")
// Both threshold fields align at the same control column margin
XCTAssertTrue(
abs(goodField.frame.minX - warningField.frame.minX) <= 1.0,
"Good and Warning ΔE fields should align at the same column margin")
// Other labels must not overflow the left boundary
let defaultInstLabel = sheet.staticTexts["Default instrument"]
XCTAssertTrue(defaultInstLabel.waitForExistence(timeout: 5))
XCTAssertTrue(
defaultInstLabel.frame.minX >= sheet.frame.minX + 12.0,
"Default instrument label must not overflow left edge")
let bundledSidecarsLabel = sheet.staticTexts["Bundled sidecars"]
XCTAssertTrue(bundledSidecarsLabel.waitForExistence(timeout: 5))
XCTAssertTrue(
bundledSidecarsLabel.frame.minX >= sheet.frame.minX + 12.0,
"Bundled sidecars label must not overflow left edge")
sheet.buttons["Cancel"].click()
waitForSheetDismiss(timeout: 5)
}
/// `warning <= good` fails `AppSettings.validate()` and keeps the
/// sheet open with the contract error text; restoring valid values
/// lets Save dismiss (issue #165 acceptance, strings are the #5
/// contract).
func testDeltaEValidationBlocksSaveThenValidSaveDismisses() throws {
openSettings()
replaceFieldValue(thresholdField("settingsDeltaEWarning"), with: "1")
sheet.buttons["Save"].click()
let error = sheet.staticTexts[
"Good ΔE threshold must be strictly less than the warning threshold."
]
XCTAssertTrue(error.waitForExistence(timeout: 10))
XCTAssertTrue(sheet.exists, "invalid ΔE must not dismiss the sheet")
replaceFieldValue(thresholdField("settingsDeltaEWarning"), with: "5")
sheet.buttons["Save"].click()
waitForSheetDismiss(timeout: 10)
}
}
File diff suppressed because one or more lines are too long