Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f16825be72 | ||
|
|
9fcd21dc2d | ||
|
|
f5c57649e1 | ||
|
|
e967502ea7 | ||
|
|
eb3ae08cf9 | ||
|
|
3d8e70be62 |
@@ -142,8 +142,12 @@ struct GamutView: View {
|
||||
Text(layer?.displayName ?? fallback)
|
||||
}
|
||||
.toggleStyle(.checkbox)
|
||||
.disabled(layer == nil || viewModel.viewerUnavailable)
|
||||
.disabled(layer == nil)
|
||||
.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,15 +107,18 @@ 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")
|
||||
|
||||
let compare = waitFor("gamutLayer-compare")
|
||||
XCTAssertTrue(compare.exists)
|
||||
XCTAssertFalse(compare.isEnabled, "Compare toggle must be disabled before a load")
|
||||
|
||||
let status = waitFor("gamutStatusText")
|
||||
let statusValue = status.value as? String ?? ""
|
||||
XCTAssertTrue(statusValue.contains("sRGB"), "Status should list the sRGB layer, got: \(statusValue)")
|
||||
}
|
||||
|
||||
func testAddCompareButtonExists() throws {
|
||||
@@ -124,8 +143,10 @@ 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")
|
||||
XCTAssertEqual(compare.value as? Int, 1, "Compare layer should be on after load")
|
||||
|
||||
let status = waitFor("gamutStatusText")
|
||||
let value = status.value as? String ?? ""
|
||||
@@ -146,8 +167,14 @@ 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")
|
||||
XCTAssertEqual(compare.value as? Int, 1, "Compare layer should be on after iccgamut")
|
||||
|
||||
// The compare slot's display name is the .gam stem ("myprinter").
|
||||
let status = waitFor("gamutStatusText")
|
||||
let statusValue = status.value as? String ?? ""
|
||||
XCTAssertTrue(statusValue.contains("myprinter"), "Status should list the compare layer, got: \(statusValue)")
|
||||
}
|
||||
|
||||
func testInspectPanelIdleStableHeight() throws {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user