Compare commits

..
Author SHA1 Message Date
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> f5c57649e1 test(m10): combine gamut layer toggle AX; harden gallery wait on macOS 12
Run 33803 still reported the layer toggles disabled after a real
waitUntilEnabled: on macOS 12 the identifier lands on the Toggle's
container element, which never reports isEnabled — combine the
Toggle's children so the a11y leaf is the checkbox itself (checkbox
style unchanged). testPrinttargFixtureGalleryAndStubbedPrint flaked
once on the same runner: wait for btnCreateLayout enabled before
clicking and retry once when the gallery never materialises.

Refs #147 #149 #146

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-13 15:52:53 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> eb3ae08cf9 test(m10): wait for gamut layer enable; drive New via File menu on macOS 12
Actions run 33718 (Xcode 14.2, macOS 12) failed four UI tests that
pass locally. Gamut layer toggles render as disabled placeholders
until the async layer load lands, so waitFor(id) won the race on the
placeholder — added waitUntilEnabled and gate on isEnabled. The
project New test's ⌘N may not reach the CommandGroup and alert
buttons often lack identifiers on macOS 12 — drive New through the
File menu "New Project" item and confirm the alert by button title
with id fallback.

Refs #147 #149 #146

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-13 14:59:11 +01:00
gronod 3d8e70be62 Merge pull request 'fix(ui): split ViewBuilders for Swift 5.7 ten-child limit (#146)' (#157) from fix/146-viewbuilder-ten-child into milestone/m10-studio
macOS CI / build-and-test (pull_request) Failing after 29m46s
macOS CI / package (pull_request) Skipped
2026-09-13 13:35:36 +01:00
4 changed files with 106 additions and 17 deletions
+4
View File
@@ -144,6 +144,10 @@ struct GamutView: View {
.toggleStyle(.checkbox) .toggleStyle(.checkbox)
.disabled(layer == nil || viewModel.viewerUnavailable) .disabled(layer == nil || viewModel.viewerUnavailable)
.help(layer.map { $0.sourceURL.lastPathComponent } ?? "No profile .gam loaded") .help(layer.map { $0.sourceURL.lastPathComponent } ?? "No profile .gam loaded")
// macOS 12 puts the identifier on the Toggle's container, an
// element that never reports isEnabled combine so the a11y
// leaf is the checkbox itself.
.accessibilityElement(children: .combine)
.accessibilityIdentifier("gamutLayer-\(id)") .accessibilityIdentifier("gamutLayer-\(id)")
} }
@@ -75,6 +75,22 @@ final class Milestone10GamutCompareUITests: XCTestCase {
return el return el
} }
/// Exists **and** `isEnabled`. Layer toggles render as disabled
/// placeholders until the async layer load lands on the macOS 12
/// runner `waitFor` alone wins the race against `parse`.
private func waitUntilEnabled(_ id: String, timeout: TimeInterval = 15) -> XCUIElement {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
let el = element(id)
if el.exists && el.isEnabled { return el }
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
let el = element(id)
XCTAssertTrue(
el.exists && el.isEnabled, "Expected enabled element \(id)")
return el
}
private func launchApp() { private func launchApp() {
app.launch() app.launch()
if !app.wait(for: .runningForeground, timeout: 10) { if !app.wait(for: .runningForeground, timeout: 10) {
@@ -91,9 +107,8 @@ final class Milestone10GamutCompareUITests: XCTestCase {
func testLayerTogglesExistWithSRGB() throws { func testLayerTogglesExistWithSRGB() throws {
openGamutSheet() openGamutSheet()
let srgb = waitFor("gamutLayer-sRGB") let srgb = waitUntilEnabled("gamutLayer-sRGB")
XCTAssertTrue(srgb.exists) XCTAssertTrue(srgb.exists)
XCTAssertTrue(srgb.isEnabled)
// NSButton checkbox value is 1 when checked. // NSButton checkbox value is 1 when checked.
XCTAssertEqual(srgb.value as? Int, 1, "sRGB layer should be on") XCTAssertEqual(srgb.value as? Int, 1, "sRGB layer should be on")
@@ -124,7 +139,8 @@ final class Milestone10GamutCompareUITests: XCTestCase {
waitFor("btnGamutAddCompare").click() waitFor("btnGamutAddCompare").click()
waitFor("btnGamutOpenGam").click() waitFor("btnGamutOpenGam").click()
let compare = waitFor("gamutLayer-compare") // The pre-load placeholder also exists wait for enabled.
let compare = waitUntilEnabled("gamutLayer-compare")
XCTAssertTrue(compare.isEnabled, "Compare toggle should enable after load") XCTAssertTrue(compare.isEnabled, "Compare toggle should enable after load")
let status = waitFor("gamutStatusText") let status = waitFor("gamutStatusText")
@@ -146,7 +162,7 @@ final class Milestone10GamutCompareUITests: XCTestCase {
waitFor("btnGamutAddCompare").click() waitFor("btnGamutAddCompare").click()
waitFor("btnGamutOpenProfile").click() waitFor("btnGamutOpenProfile").click()
let compare = waitFor("gamutLayer-compare") let compare = waitUntilEnabled("gamutLayer-compare")
XCTAssertTrue(compare.isEnabled, "Compare toggle should enable after iccgamut") XCTAssertTrue(compare.isEnabled, "Compare toggle should enable after iccgamut")
} }
@@ -3,8 +3,9 @@ import XCTest
/// Milestone 10 UI tests issue #149 project file. Panels are never /// Milestone 10 UI tests issue #149 project file. Panels are never
/// real: `ICCERY_TEST_PROJECT_OPEN` / `ICCERY_TEST_PROJECT_SAVE` /// real: `ICCERY_TEST_PROJECT_OPEN` / `ICCERY_TEST_PROJECT_SAVE`
/// inject fixture paths through `UITestHooks`. Menu commands are driven /// inject fixture paths through `UITestHooks`. Menu commands are driven
/// by their keyboard shortcuts (N) or the sidebar chip so the tests do /// through the File menu when it is in the AX tree, else by their
/// not depend on menu AX exposure (R19). All queries by identifier. /// keyboard shortcuts (N) the tests do not depend on menu AX
/// exposure (R19). All queries by identifier.
@MainActor @MainActor
final class Milestone10ProjectUITests: XCTestCase { final class Milestone10ProjectUITests: XCTestCase {
@@ -77,6 +78,39 @@ final class Milestone10ProjectUITests: XCTestCase {
return (el.value as? String) ?? el.label return (el.value as? String) ?? el.label
} }
/// Alert/sheet button by visible title, falling back to the a11y
/// id; nil when neither matches. macOS 12 SwiftUI alerts often
/// drop `accessibilityIdentifier` on their buttons, so the title
/// is the reliable handle there.
private func alertButton(title: String, id: String) -> XCUIElement? {
let inDialog = app.dialogs.firstMatch.buttons[title].firstMatch
if inDialog.exists { return inDialog }
let inSheet = app.sheets.firstMatch.buttons[title].firstMatch
if inSheet.exists { return inSheet }
let byId = element(id)
return byId.exists ? byId : nil
}
/// Fires File New Project via the menu when it is in the AX
/// tree, else N. On macOS 12 `typeKey` may not reach the
/// `CommandGroup`, and menu item ids are unreliable the menu
/// item is matched by its "New Project" label first.
private func triggerNewProject() {
let fileMenu = app.menuBarItems["File"]
if fileMenu.waitForExistence(timeout: 5) {
fileMenu.click()
let byTitle = app.menuItems["New Project"].firstMatch
let byId = app.menuItems["menuProjectNew"].firstMatch
let item = byTitle.exists ? byTitle : byId
if item.waitForExistence(timeout: 5) {
item.click()
return
}
app.typeKey(XCUIKeyboardKey.escape, modifierFlags: [])
}
app.typeKey("n", modifierFlags: .command)
}
/// Writes a `.icceryproj` fixture under `testRoot` and points the /// Writes a `.icceryproj` fixture under `testRoot` and points the
/// open-picker hook at it. /// open-picker hook at it.
private func stageProjectFixture( private func stageProjectFixture(
@@ -133,23 +167,26 @@ final class Milestone10ProjectUITests: XCTestCase {
XCTAssertTrue(basenameField.waitForExistence(timeout: 10)) XCTAssertTrue(basenameField.waitForExistence(timeout: 10))
XCTAssertEqual(basenameField.value as? String, "ui149job") XCTAssertEqual(basenameField.value as? String, "ui149job")
// N fires the File-menu New command even when the menu is not // File New Project when the menu is in the AX tree, else
// in the AX tree. Mock CUPS may have enumerated a queue that the // N. Mock CUPS may have enumerated a queue that the fixture
// fixture does not record, making the session dirty in that // does not record, making the session dirty in that case
// case the dirty alert gates New first. // the dirty alert gates New first. macOS 12 alerts often lack
app.typeKey("n", modifierFlags: .command) // button identifiers, so confirm by title with id fallback.
triggerNewProject()
let deadline = Date().addingTimeInterval(10) let deadline = Date().addingTimeInterval(10)
var confirmed = false var confirmed = false
while Date() < deadline { while Date() < deadline {
// Dirty sessions show the dirty alert first; discarding it // Dirty sessions show the dirty alert first; discarding it
// runs the New reset directly (no second confirm). // runs the New reset directly (no second confirm).
if element("btnProjectDirtyDiscard").exists { if let discard = alertButton(
element("btnProjectDirtyDiscard").click() title: "Don't Save", id: "btnProjectDirtyDiscard") {
discard.click()
confirmed = true confirmed = true
break break
} }
if element("btnProjectNewConfirm").exists { if let start = alertButton(
element("btnProjectNewConfirm").click() title: "Start", id: "btnProjectNewConfirm") {
start.click()
confirmed = true confirmed = true
break break
} }
+34 -2
View File
@@ -85,6 +85,31 @@ final class Milestone2UITests: XCTestCase {
return el return el
} }
/// Exists **and** `isEnabled` guards clicks against buttons that
/// appear a beat before their `.disabled` condition clears.
private func waitUntilEnabled(_ id: String, timeout: TimeInterval = 10) -> XCUIElement {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
let el = element(id)
if el.exists && el.isEnabled { return el }
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
let el = element(id)
XCTAssertTrue(
el.exists && el.isEnabled, "Expected enabled element \(id)")
return el
}
/// Non-asserting existence poll for the retry-or-fail pattern.
private func existsAfter(_ id: String, timeout: TimeInterval) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if element(id).exists { return true }
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
return element(id).exists
}
/// Assert an element stays absent after a short dwell unlike /// Assert an element stays absent after a short dwell unlike
/// `waitForExistence`, which always burns its full timeout on the /// `waitForExistence`, which always burns its full timeout on the
/// negative path. /// negative path.
@@ -181,8 +206,15 @@ final class Milestone2UITests: XCTestCase {
XCTAssertTrue(element("tiffDpi").exists) XCTAssertTrue(element("tiffDpi").exists)
XCTAssertTrue(element("targetLabelPreview").exists) XCTAssertTrue(element("targetLabelPreview").exists)
app.buttons["btnCreateLayout"].click() // On the slow macOS 12 runner a synthesized click can land
XCTAssertTrue(waitFor("galleryPage-0", timeout: 20).exists) // while the button is still rebuilding retry once if the
// gallery never materialises, then allow a generous window
// for the fixture printtarg + PNG render.
waitUntilEnabled("btnCreateLayout").click()
if !existsAfter("galleryPage-0", timeout: 15) {
waitUntilEnabled("btnCreateLayout").click()
}
XCTAssertTrue(waitFor("galleryPage-0", timeout: 30).exists)
XCTAssertTrue(FileManager.default.fileExists( XCTAssertTrue(FileManager.default.fileExists(
atPath: workDir.appendingPathComponent("mytarget.ti2").path)) atPath: workDir.appendingPathComponent("mytarget.ti2").path))