Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions macTEMUITests/EDXRegionsRealFileJourney.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//
// EDXRegionsRealFileJourney.swift
// The EDS room's per-region composition on the owner's real Velox spectrum images (1339, 1840 and the 1.7 GB 1456): draw regions on
// the colour mix, Quantify each, read its composition and the region table, export the results. Opt-in like EDXRealFileJourney:
// TEST_RUNNER_MACTEM_REAL_DATA=1 TEST_RUNNER_MACEM_DATA=<data folder> tools/run-tests.sh ui EDXRegionsRealFileJourney
// Regions are drawn by the room's keyboard route (XCUITest's clicks and drags do not reach the drawing gestures); the compositions are attached, never asserted (no truth for these files yet).
//

import XCTest

nonisolated final class EDXRegionsRealFileJourney: Journey {
static let exportName = "macTEM-edx-regions-export"
static func edx(_ name: String) -> String { Journey.dataFolder + "stem-edx/" + name }

override func setUpWithError() throws {
try super.setUpWithError()
try XCTSkipUnless(ProcessInfo.processInfo.environment["MACTEM_REAL_DATA"] == "1",
"set MACTEM_REAL_DATA=1 (and TEST_RUNNER_MACTEM_REAL_DATA=1 for xcodebuild) to run on the real files")
}

@MainActor func attach(_ name: String, _ string: String) {
let a = XCTAttachment(string: string); a.name = name; a.lifetime = .keepAlways; add(a)
}

@MainActor func text(_ e: XCUIElement) -> String {
if !e.label.isEmpty { return e.label }
if let v = e.value as? String, !v.isEmpty { return v }
return e.staticTexts.firstMatch.label
}

@MainActor func mixTile() -> XCUIElement { app.groups.matching(NSPredicate(format: "label BEGINSWITH 'Colour mix map'")).firstMatch }
@MainActor func resultsHeader() -> XCUIElement {
app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH 'Results ·' OR value BEGINSWITH 'Results ·'")).firstMatch
}

@MainActor func open(_ file: String) {
launch(["--open", file, "--export-dir", Self.exportName])
if !element("edx.room").waitForExistence(timeout: 60) {
let sidebar = element("workspace.edx")
if sidebar.waitForExistence(timeout: 300), sidebar.isEnabled { sidebar.click() }
}
waitFor("edx.room", timeout: 300)
XCTAssertTrue(mixTile().waitForExistence(timeout: 600), "the colour mix tile never appeared")
waitFor("edx.spectrum", timeout: 120)
}

@MainActor func showSettings(waitingFor id: String) {
let picker = waitFor("inspector.tabPicker", timeout: 30)
let tab = picker.buttons["Settings"]
if tab.exists, !tab.isSelected { tab.click() }
waitFor(id, timeout: 20)
}

@MainActor func chooseTool(_ index: Int) {
showSettings(waitingFor: "edx.tool")
element("edx.tool").radioButtons.element(boundBy: index).click()
}

@MainActor func quantify() {
let verb = waitFor("workspace.primaryAction", timeout: 60)
let enabled = XCTNSPredicateExpectation(predicate: NSPredicate(format: "isEnabled == true"), object: verb)
XCTAssertEqual(XCTWaiter().wait(for: [enabled], timeout: 300), .completed, "Quantify never became enabled")
verb.click()
waitFor("edx.compositionTable", timeout: 300)
XCTAssertTrue(element("toolbar.display.idle").waitForExistence(timeout: 600), "the room did not come back to idle after Quantify")
}

/// Every composition row of the table as "El wt%" text, for the attachment.
@MainActor func composition() -> String {
let rows = app.descendants(matching: .any).matching(NSPredicate(format: "identifier BEGINSWITH 'edx.composition.' AND identifier ENDSWITH '.wt'")).allElementsBoundByIndex
return rows.map { "\($0.identifier.replacingOccurrences(of: "edx.composition.", with: "")): \(text($0))" }.joined(separator: "\n")
}

@MainActor func waitHeader(_ suffix: String, timeout: TimeInterval = 30) -> Bool {
let p = NSPredicate(format: "label == %@ OR value == %@", "Results · \(suffix)", "Results · \(suffix)")
return app.staticTexts.matching(p).firstMatch.waitForExistence(timeout: timeout)
}

/// Chooses the tool (its index in the Tools control), focuses the colour mix with a click and presses Return; then `nudges` shift-arrow
/// presses move the new region ten pixels each to the lower right, so a second region does not sit on the first.
@MainActor func drawByKeyboard(tool: Int, nudges: Int) {
chooseTool(tool)
app.activate()
mixTile().click()
app.typeKey(.return, modifierFlags: [])
for _ in 0..<nudges {
app.typeKey(.rightArrow, modifierFlags: .shift)
app.typeKey(.downArrow, modifierFlags: .shift)
}
}

@MainActor func drive(_ name: String) throws {
let file = Self.edx(name)
try XCTSkipUnless(FileManager.default.fileExists(atPath: file), "the real file is not at \(file)")
try step("open \(name)") { open(file) }
try step("Quantify the whole map") {
quantify()
attach("whole map", composition())
}
// XCUITest's clicks and drags do not reach the drawing layer's gestures (EDXJourney: drawn shapes are checked by hand), so the regions
// are drawn by the room's keyboard route: a click on the colour mix gives it the focus, Return draws the chosen tool's shape in the
// middle of the scan, the arrows move it (shift: ten pixels).
try step("Rectangle tool, Return on the colour mix: Region 1") {
drawByKeyboard(tool: 1, nudges: 0)
XCTAssertTrue(waitHeader("Region 1"), "after Return the results read \"\(text(resultsHeader()))\"")
}
try step("Quantify Region 1") {
quantify()
attach("Region 1", composition())
XCTAssertFalse(composition().isEmpty, "no composition rows for Region 1")
}
try step("Ellipse tool, Return, moved off the first: Region 2") {
drawByKeyboard(tool: 2, nudges: 6)
XCTAssertTrue(waitHeader("Region 2"), "after Return the results read \"\(text(resultsHeader()))\"")
}
try step("Quantify Region 2") {
quantify()
attach("Region 2", composition())
}
try step("the region table lists both regions") {
let table = element("edx.regionTable")
if !table.waitForExistence(timeout: 20) {
attach("region table", "no edx.regionTable on screen")
return
}
for region in ["Region 1", "Region 2"] {
XCTAssertTrue(element("edx.regionTable.\(region).pixels").waitForExistence(timeout: 30), "the region table has no row for \(region)")
attach("\(region) pixels", (element("edx.regionTable.\(region).pixels").value as? String) ?? "?")
}
}
try step("export Results CSV") {
showSettings(waitingFor: "edx.export.results")
let button = element("edx.export.results")
XCTAssertEqual(XCTWaiter().wait(for: [XCTNSPredicateExpectation(predicate: NSPredicate(format: "isEnabled == true"), object: button)], timeout: 120), .completed, "Results CSV stayed disabled")
button.click()
let bar = waitFor("status.bar")
let exported = XCTNSPredicateExpectation(predicate: NSPredicate { _, _ in self.text(bar).hasPrefix("Exported ") }, object: nil)
XCTAssertEqual(XCTWaiter().wait(for: [exported], timeout: 60), .completed, "Results CSV: status line \"\(text(bar))\"")
attach("status after export", text(bar))
}
}

@MainActor func test01_Regions1339() throws { try drive("SI HAADF 1339 65000 x 20260508.emd") }
@MainActor func test02_Regions1840() throws { try drive("SI HAADF 1840 23000 x 20260506.emd") }
@MainActor func test03_Regions1456() throws { try drive("SI HAADF 1456 77000 x 20260420.emd") }
}
99 changes: 99 additions & 0 deletions macTEMUITests/ParticlesRealFileJourney.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// ParticlesRealFileJourney.swift
// The Image room's Particles section on the owner's real Ceta images (TIFF and Velox EMD, the low-magnification ones and the HRTEM
// 630 kx and 1.05 Mx). Opt-in like the other real-file journeys: skipped unless MACTEM_REAL_DATA=1 reaches the runner and the file
// exists. xcodebuild hands the runner only variables named TEST_RUNNER_*, so run it as
// TEST_RUNNER_MACTEM_REAL_DATA=1 TEST_RUNNER_MACEM_DATA=<data folder> tools/run-tests.sh ui ParticlesRealFileJourney
// Each method opens one file, shows objects with the defaults, reads the counts, opens the object table and exports the CSV into
// --export-dir. A finding (the section is wrong) fails the step with its words; the counts are attached, never asserted (no truth yet).
//

import XCTest

nonisolated final class ParticlesRealFileJourney: Journey {
static let exportName = "macTEM-particles-export"
static func tem(_ name: String) -> String { Journey.dataFolder + "tem/" + name }

override func setUpWithError() throws {
try super.setUpWithError()
try XCTSkipUnless(ProcessInfo.processInfo.environment["MACTEM_REAL_DATA"] == "1",
"set MACTEM_REAL_DATA=1 (and TEST_RUNNER_MACTEM_REAL_DATA=1 for xcodebuild) to run on the real files")
}

@MainActor func attach(_ name: String, _ string: String) {
let a = XCTAttachment(string: string); a.name = name; a.lifetime = .keepAlways; add(a)
}

@MainActor func text(_ e: XCUIElement) -> String {
if !e.label.isEmpty { return e.label }
if let v = e.value as? String, !v.isEmpty { return v }
return e.staticTexts.firstMatch.label
}

/// The whole walk on one file: open, Show Objects, counts, the table, the CSV.
@MainActor func drive(_ name: String) throws {
let file = Self.tem(name)
try XCTSkipUnless(FileManager.default.fileExists(atPath: file), "the real file is not at \(file)")
try step("open \(name)") {
launch(["--open", file, "--export-dir", Self.exportName])
waitFor("image.view", timeout: 120)
showInspector(waitingFor: "particles.show")
}
try step("show objects") {
let show = waitFor("particles.show", timeout: 30)
let enabled = XCTNSPredicateExpectation(predicate: NSPredicate(format: "isEnabled == true"), object: show)
XCTAssertEqual(XCTWaiter().wait(for: [enabled], timeout: 60), .completed, "Show Objects stayed disabled")
show.click()
}
try step("counts") {
let counted = waitValue("particles.counts", timeout: 180) { !$0.isEmpty }
attach("counted", counted)
attach("counts line", text(element("particles.counts")))
if element("particles.density").exists { attach("density line", text(element("particles.density"))) }
let sizes = app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH 'median' OR value BEGINSWITH 'median'")).firstMatch
if sizes.exists { attach("size line", text(sizes)) }
XCTAssertFalse(app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'not calibrated'")).firstMatch.exists,
"a calibrated Ceta image reads as not calibrated")
let table = waitFor("particles.table")
XCTAssertTrue(app.windows.firstMatch.frame.contains(table.frame), "Object Table is below the fold: \(table.frame)")
}
// XCUITest's snapshot of a window builds every row of its table: on the HRTEM images (10 000 objects and more under the default
// threshold) one query keeps the app's main thread busy past XCUITest's 30 s (2026-11-05, spindump in NSTableView
// viewAtColumn:row:makeIfNecessary:). A person scrolling the table builds only the rows in view, so the drive stops short of it there.
let counted = Int(((element("particles.counts").value as? String) ?? "").filter(\.isNumber)) ?? 0 // "4.677" in a German locale
if counted > 2000 { attach("object table", "not opened: \(counted) counted objects, more than XCUITest's snapshot of the table can take") }
else { try step("object table") {
waitFor("particles.table").click()
let view = waitFor("particles.tableView", timeout: 30)
let rows = view.tableRows.firstMatch.exists || view.outlineRows.firstMatch.exists ? 1 : 0 // SwiftUI's Table is an outline
attach("table rows", "\(rows)")
XCTAssertTrue(rows > 0 || counted == 0, "the table lists no row")
let tableWindow = app.windows.matching(NSPredicate(format: "title == 'Objects'")).firstMatch
if tableWindow.exists {
let shot = XCTAttachment(screenshot: tableWindow.screenshot()); shot.name = "the Objects window"; shot.lifetime = .keepAlways; add(shot)
tableWindow.typeKey("w", modifierFlags: .command)
}
} }
try step("export CSV") {
let export = waitFor("particles.export", timeout: 20)
export.click()
let bar = waitFor("status.bar", timeout: 20)
let exported = XCTNSPredicateExpectation(predicate: NSPredicate { _, _ in self.text(bar).hasPrefix("Exported ") }, object: nil)
let saved = XCTWaiter().wait(for: [exported], timeout: 30)
if saved != .completed, app.sheets.firstMatch.exists {
let shot = XCTAttachment(screenshot: app.windows.firstMatch.screenshot()); shot.name = "a save panel instead"; shot.lifetime = .keepAlways; add(shot)
app.sheets.firstMatch.buttons["Cancel"].click()
}
XCTAssertEqual(saved, .completed, "Export CSV with --export-dir: status line \"\(text(bar))\"")
attach("status after export", text(bar))
}
}

@MainActor func test01_Ceta1305TIFF() throws { try drive("Camera Ceta 1305 17500 x 20260605 Ceta.tif") }
@MainActor func test02_Ceta1306TIFF() throws { try drive("Camera Ceta 1306 28500 x 20260605 Ceta.tif") }
@MainActor func test03_Ceta1309TIFF() throws { try drive("Camera Ceta 1309 36000 x 20260605 Ceta.tif") }
@MainActor func test04_Ceta1309EMD() throws { try drive("Camera Ceta 1309 36000 x 20260605.emd") }
@MainActor func test05_HRTEM1620TIFF() throws { try drive("Camera Ceta 1620 630 kx 20260605 Ceta.tif") }
@MainActor func test06_HRTEM1623TIFF() throws { try drive("Camera Ceta 1623 1.05 Mx 20260605 Ceta.tif") }
@MainActor func test07_HRTEM1631TIFF() throws { try drive("Camera Ceta 1631 1.05 Mx 20260605 Ceta.tif") }
}
Loading