From a2dc0bef02686cf235d5e85489dd6175ca800e79 Mon Sep 17 00:00:00 2001 From: Paullo9 Date: Thu, 5 Nov 2026 12:11:32 +0100 Subject: [PATCH] test: real-file journeys for Particles on the Ceta images and EDS per-region composition ParticlesRealFileJourney opens each Ceta TIFF/EMD in data/tem (17.5 kx to 1.05 Mx), shows objects, attaches the counts, size and density lines, opens the object table and exports the CSV into --export-dir. EDXRegionsRealFileJourney opens the 1339, 1840 and 1456 Velox spectrum images, quantifies the whole map, draws two regions through the room's keyboard route, quantifies each and exports the results. Both are opt-in (MACTEM_REAL_DATA=1) and find the data through $MACEM_DATA. Done: the precipitation workflow's macTEM half is driven on the real files; next: re-run after #98 and MacEMUI#69 land. Co-Authored-By: Claude Opus 5.5 --- macTEMUITests/EDXRegionsRealFileJourney.swift | 146 ++++++++++++++++++ macTEMUITests/ParticlesRealFileJourney.swift | 99 ++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 macTEMUITests/EDXRegionsRealFileJourney.swift create mode 100644 macTEMUITests/ParticlesRealFileJourney.swift diff --git a/macTEMUITests/EDXRegionsRealFileJourney.swift b/macTEMUITests/EDXRegionsRealFileJourney.swift new file mode 100644 index 0000000..866ef10 --- /dev/null +++ b/macTEMUITests/EDXRegionsRealFileJourney.swift @@ -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= 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.. 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") } +}