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)
.disabled(layer == nil || viewModel.viewerUnavailable)
.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)")
}
@@ -75,6 +75,22 @@ final class Milestone10GamutCompareUITests: XCTestCase {
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() {
app.launch()
if !app.wait(for: .runningForeground, timeout: 10) {
@@ -91,9 +107,8 @@ final class Milestone10GamutCompareUITests: XCTestCase {
func testLayerTogglesExistWithSRGB() throws {
openGamutSheet()
let srgb = waitFor("gamutLayer-sRGB")
let srgb = waitUntilEnabled("gamutLayer-sRGB")
XCTAssertTrue(srgb.exists)
XCTAssertTrue(srgb.isEnabled)
// NSButton checkbox value is 1 when checked.
XCTAssertEqual(srgb.value as? Int, 1, "sRGB layer should be on")
@@ -124,7 +139,8 @@ final class Milestone10GamutCompareUITests: XCTestCase {
waitFor("btnGamutAddCompare").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")
let status = waitFor("gamutStatusText")
@@ -146,7 +162,7 @@ final class Milestone10GamutCompareUITests: XCTestCase {
waitFor("btnGamutAddCompare").click()
waitFor("btnGamutOpenProfile").click()
let compare = waitFor("gamutLayer-compare")
let compare = waitUntilEnabled("gamutLayer-compare")
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
/// real: `ICCERY_TEST_PROJECT_OPEN` / `ICCERY_TEST_PROJECT_SAVE`
/// inject fixture paths through `UITestHooks`. Menu commands are driven
/// by their keyboard shortcuts (N) or the sidebar chip so the tests do
/// not depend on menu AX exposure (R19). All queries by identifier.
/// through the File menu when it is in the AX tree, else by their
/// keyboard shortcuts (N) the tests do not depend on menu AX
/// exposure (R19). All queries by identifier.
@MainActor
final class Milestone10ProjectUITests: XCTestCase {
@@ -77,6 +78,39 @@ final class Milestone10ProjectUITests: XCTestCase {
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
/// open-picker hook at it.
private func stageProjectFixture(
@@ -133,23 +167,26 @@ final class Milestone10ProjectUITests: XCTestCase {
XCTAssertTrue(basenameField.waitForExistence(timeout: 10))
XCTAssertEqual(basenameField.value as? String, "ui149job")
// N fires the File-menu New command even when the menu is not
// in the AX tree. Mock CUPS may have enumerated a queue that the
// fixture does not record, making the session dirty in that
// case the dirty alert gates New first.
app.typeKey("n", modifierFlags: .command)
// File New Project when the menu is in the AX tree, else
// N. Mock CUPS may have enumerated a queue that the fixture
// does not record, making the session dirty in that case
// the dirty alert gates New first. macOS 12 alerts often lack
// button identifiers, so confirm by title with id fallback.
triggerNewProject()
let deadline = Date().addingTimeInterval(10)
var confirmed = false
while Date() < deadline {
// Dirty sessions show the dirty alert first; discarding it
// runs the New reset directly (no second confirm).
if element("btnProjectDirtyDiscard").exists {
element("btnProjectDirtyDiscard").click()
if let discard = alertButton(
title: "Don't Save", id: "btnProjectDirtyDiscard") {
discard.click()
confirmed = true
break
}
if element("btnProjectNewConfirm").exists {
element("btnProjectNewConfirm").click()
if let start = alertButton(
title: "Start", id: "btnProjectNewConfirm") {
start.click()
confirmed = true
break
}
+34 -2
View File
@@ -85,6 +85,31 @@ final class Milestone2UITests: XCTestCase {
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
/// `waitForExistence`, which always burns its full timeout on the
/// negative path.
@@ -181,8 +206,15 @@ final class Milestone2UITests: XCTestCase {
XCTAssertTrue(element("tiffDpi").exists)
XCTAssertTrue(element("targetLabelPreview").exists)
app.buttons["btnCreateLayout"].click()
XCTAssertTrue(waitFor("galleryPage-0", timeout: 20).exists)
// On the slow macOS 12 runner a synthesized click can land
// 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(
atPath: workDir.appendingPathComponent("mytarget.ti2").path))