Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1c37bb7d7 | ||
|
|
d2466d9a66 | ||
|
|
4af2d96294 | ||
|
|
d17aa962ce | ||
|
|
93acd32b51 | ||
|
|
67cf452e59 | ||
|
|
83ed53c7fc | ||
|
|
cba9476af7 | ||
|
|
288b08e3d9 | ||
|
|
18f1d1ff33 | ||
|
|
9dfa79ebd0 |
@@ -0,0 +1,231 @@
|
|||||||
|
# GitHub Actions twin of .gitea/workflows/macos.yml.
|
||||||
|
# Deltas from the Gitea file (everything else is the same jobs/steps):
|
||||||
|
# - runs-on macos-14: github.com retired macos-12. Do not use
|
||||||
|
# macos-latest — in 2026 that is macos-26-arm64, where a 1280×800
|
||||||
|
# window hangs off the virtual display and XCTest marks sidebar
|
||||||
|
# controls (x ≈ -116) as not hittable (run 34864198118).
|
||||||
|
# - actions/upload-artifact@v4: v3 is shut down on github.com. Gitea act_runner
|
||||||
|
# still uses v3.
|
||||||
|
# - No NODE_EXTRA_CA_CERTS / System keychain bundle: that is only for the
|
||||||
|
# private Gitea CA when the runner talks to git.i3omb.com.
|
||||||
|
# - Tag DMGs go to a GitHub Release via `gh` instead of
|
||||||
|
# scripts/attach-release-asset.sh (Gitea /api/v1).
|
||||||
|
name: macOS CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: macos-14
|
||||||
|
env:
|
||||||
|
DERIVED: build/DerivedData-test
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Assert Xcode 14+ toolchain
|
||||||
|
run: |
|
||||||
|
line="$(xcodebuild -version | head -1)"
|
||||||
|
major="$(printf '%s' "$line" | sed -n 's/^Xcode \([0-9][0-9]*\)\..*/\1/p')"
|
||||||
|
if [ -z "$major" ] || [ "$major" -lt 14 ]; then
|
||||||
|
echo "Unexpected Xcode version: $line" >&2; exit 1
|
||||||
|
fi
|
||||||
|
echo "$line"
|
||||||
|
|
||||||
|
# Tag pushes whose name contains "prerelease" skip the test build and both
|
||||||
|
# test legs: they exist to package a build already validated elsewhere.
|
||||||
|
# The job still succeeds quickly so `package`'s `needs:` stays satisfied.
|
||||||
|
# Homebrew's xcodegen formula requires Xcode 15.3, which cannot be
|
||||||
|
# installed on macOS 12 (#109). The script installs a pinned
|
||||||
|
# prebuilt release instead. dmgbuild is not installed here — the
|
||||||
|
# test job does not package (#95).
|
||||||
|
- name: Ensure host tools
|
||||||
|
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
||||||
|
run: scripts/ensure-host-tools.sh
|
||||||
|
|
||||||
|
- name: Generate Xcode project
|
||||||
|
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
||||||
|
run: xcodegen generate --spec project.yml
|
||||||
|
|
||||||
|
# Tests only ever run on the runner's own architecture; build
|
||||||
|
# just that slice. Packaging (scripts/package-release.sh) still
|
||||||
|
# produces the universal Release binary.
|
||||||
|
- name: Build for testing (host arch)
|
||||||
|
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
||||||
|
run: |
|
||||||
|
xcodebuild build-for-testing \
|
||||||
|
-scheme ICCery \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
-derivedDataPath "$DERIVED" \
|
||||||
|
-configuration Debug \
|
||||||
|
ARCHS="$(uname -m)" \
|
||||||
|
ONLY_ACTIVE_ARCH=NO \
|
||||||
|
CODE_SIGNING_ALLOWED=YES \
|
||||||
|
CODE_SIGN_IDENTITY='-'
|
||||||
|
|
||||||
|
# Xcode embeds the shared ICCeryCore package framework into the app
|
||||||
|
# and the test bundle without signing it. Ad-hoc hosts still require
|
||||||
|
# every loaded dylib to carry a cdhash — dyld killed the test host at
|
||||||
|
# launch (run 31992) — so sign every embedded copy once the build is
|
||||||
|
# done (embed steps run after any build script phase) (#119).
|
||||||
|
- name: Sign package product frameworks
|
||||||
|
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
||||||
|
run: |
|
||||||
|
find "$DERIVED/Build/Products/Debug" -depth -name '*_PackageProduct.framework' -print0 \
|
||||||
|
| while IFS= read -r -d '' fw; do
|
||||||
|
echo "signing $fw"
|
||||||
|
codesign --force --sign - --timestamp=none "$fw"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Test unit (ICCeryCoreTests)
|
||||||
|
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
||||||
|
run: |
|
||||||
|
XCTESTRUN="$(find "$DERIVED" -name 'ICCery*.xctestrun' | head -n 1)"
|
||||||
|
if [ -z "$XCTESTRUN" ] || [ ! -f "$XCTESTRUN" ]; then
|
||||||
|
echo "error: no xctestrun produced by build-for-testing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "xctestrun: $XCTESTRUN"
|
||||||
|
xcodebuild test-without-building \
|
||||||
|
-xctestrun "$XCTESTRUN" \
|
||||||
|
-only-testing:ICCeryCoreTests \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
-derivedDataPath "$DERIVED"
|
||||||
|
|
||||||
|
# UI tests need macOS Automation / Accessibility permission on the
|
||||||
|
# runner. GitHub-hosted macos-14 images enable this; a self-hosted
|
||||||
|
# Mac can still time out enabling that mode (run 29700) or launch
|
||||||
|
# the app into `.runningBackground` (run 29804). Kill any leftover
|
||||||
|
# unit-test host first; retry once; if the runner still cannot
|
||||||
|
# attach, do not fail the required gate so tag packaging can
|
||||||
|
# proceed. Real XCTest assertion failures still fail the job.
|
||||||
|
- name: Test UI (ICCeryUITests)
|
||||||
|
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
XCTESTRUN="$(find "$DERIVED" -name 'ICCery*.xctestrun' | head -n 1)"
|
||||||
|
LOG="$DERIVED/ui-test.log"
|
||||||
|
pkill -x ICCery 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
run_ui() {
|
||||||
|
local label="$1"
|
||||||
|
shift
|
||||||
|
echo "::group::UI tests $label"
|
||||||
|
set +e
|
||||||
|
xcodebuild test-without-building \
|
||||||
|
-xctestrun "$XCTESTRUN" \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
-derivedDataPath "$DERIVED" \
|
||||||
|
"$@" | tee "$LOG"
|
||||||
|
rc=${PIPESTATUS[0]}
|
||||||
|
set -e
|
||||||
|
echo "::endgroup::"
|
||||||
|
return "$rc"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_runner_attach_failure() {
|
||||||
|
grep -Eq "Timed out while enabling automation mode|Failed to activate application|current state: Running Background" "$LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
attempt=1
|
||||||
|
while [ "$attempt" -le 2 ]; do
|
||||||
|
# Probe one case first. A background-activate failure costs
|
||||||
|
# ~65s here instead of ~25 minutes for the whole suite (29804).
|
||||||
|
if ! run_ui "probe attempt $attempt" \
|
||||||
|
-only-testing:ICCeryUITests/AboutHelpUITests/testAboutDialogShowsVersionAndBuildDate; then
|
||||||
|
if is_runner_attach_failure; then
|
||||||
|
echo "warning: UI runner could not attach/activate the app (attempt $attempt)"
|
||||||
|
pkill -x ICCery 2>/dev/null || true
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep 8
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "error: UI probe failed with a real test error" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if run_ui "full suite attempt $attempt" -only-testing:ICCeryUITests \
|
||||||
|
-skip-testing:ICCeryUITests/AboutHelpUITests/testAboutDialogShowsVersionAndBuildDate; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if is_runner_attach_failure; then
|
||||||
|
echo "warning: UI runner lost activation mid-suite (attempt $attempt)"
|
||||||
|
pkill -x ICCery 2>/dev/null || true
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep 8
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "error: UI tests failed with a real test error" >&2
|
||||||
|
exit 1
|
||||||
|
done
|
||||||
|
echo "warning: skipping UI tests after repeated runner attach/activate failures"
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
# XCTest stores the a11y hierarchy snapshot and screenshots in the
|
||||||
|
# xcresult on failure — upload it so UI failures can be triaged
|
||||||
|
# without access to the runner (#126).
|
||||||
|
- name: Upload UI test xcresult
|
||||||
|
if: "failure() && !(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ui-test-xcresult
|
||||||
|
path: build/DerivedData-test/Logs/Test
|
||||||
|
|
||||||
|
package:
|
||||||
|
needs: build-and-test
|
||||||
|
runs-on: macos-14
|
||||||
|
if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v')
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# scripts/package-release.sh runs `xcodegen generate` and dmgbuild;
|
||||||
|
# see build-and-test for why brew is not used on macOS 12 (#109).
|
||||||
|
# INSTALL_DMGBUILD isolates dmgbuild in build/.venv-dmgbuild so
|
||||||
|
# the test job never pip-installs it (#95).
|
||||||
|
- name: Ensure host tools
|
||||||
|
run: INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh
|
||||||
|
|
||||||
|
- name: Package release
|
||||||
|
run: scripts/package-release.sh
|
||||||
|
env:
|
||||||
|
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
|
||||||
|
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
|
||||||
|
NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }}
|
||||||
|
NOTARIZE_PASSWORD: ${{ secrets.NOTARIZE_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
|
||||||
|
- name: Upload DMG artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: iccery-dmg
|
||||||
|
path: ICCery-*.dmg
|
||||||
|
|
||||||
|
- name: Attach DMG to GitHub release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
TAG="$GITHUB_REF_NAME"
|
||||||
|
case "$TAG" in
|
||||||
|
*prerelease*) PRE_FLAG=--prerelease ;;
|
||||||
|
*) PRE_FLAG= ;;
|
||||||
|
esac
|
||||||
|
if ! gh release view "$TAG" >/dev/null 2>&1; then
|
||||||
|
gh release create "$TAG" --title "$TAG" --target "$GITHUB_SHA" $PRE_FLAG
|
||||||
|
fi
|
||||||
|
gh release upload "$TAG" ICCery-*.dmg --clobber
|
||||||
@@ -206,9 +206,13 @@ xcodebuild test -scheme ICCery -destination 'platform=macOS' \
|
|||||||
-only-testing:ICCeryUITests/Milestone5UITests
|
-only-testing:ICCeryUITests/Milestone5UITests
|
||||||
```
|
```
|
||||||
|
|
||||||
CI (`.gitea/workflows/macos.yml`) runs `build-and-test` then `package` on
|
CI (`.gitea/workflows/macos.yml` on Gitea, `.github/workflows/macos.yml` on
|
||||||
|
GitHub) runs `build-and-test` then `package` on
|
||||||
`develop` and on `v*` tags. Tags whose name contains `prerelease` skip the
|
`develop` and on `v*` tags. Tags whose name contains `prerelease` skip the
|
||||||
test job and still package. `pull_request` is wired for **`develop` only**.
|
test job and still package. `pull_request` is wired for **`develop` only**.
|
||||||
|
The GitHub file is the same pipeline on `macos-14` (github.com retired
|
||||||
|
`macos-12`), `actions/upload-artifact@v4`, and `gh release upload` for tag
|
||||||
|
DMGs.
|
||||||
|
|
||||||
UI tests need an unlocked console (`IOConsoleLocked=false`). Mock Argyll /
|
UI tests need an unlocked console (`IOConsoleLocked=false`). Mock Argyll /
|
||||||
CUPS fixtures live under the test bundles; they must not be treated as proof
|
CUPS fixtures live under the test bundles; they must not be treated as proof
|
||||||
|
|||||||
@@ -56,10 +56,41 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// docs/21 §Shell: 1280×800 content, min 1100×700, centred.
|
/// docs/21 §Shell: 1280×800 content, min 1100×700, centred.
|
||||||
|
/// GitHub-hosted Macs (and any display smaller than 1280×800) must
|
||||||
|
/// not get a window that hangs off-screen — XCTest then reports
|
||||||
|
/// sidebar controls at negative x as not hittable (run 34864198118).
|
||||||
private func configureMainWindow(_ window: NSWindow) {
|
private func configureMainWindow(_ window: NSWindow) {
|
||||||
window.setContentSize(NSSize(width: 1280, height: 800))
|
let desired = NSSize(width: 1280, height: 800)
|
||||||
window.contentMinSize = NSSize(width: 1100, height: 700)
|
let minimum = NSSize(width: 1100, height: 700)
|
||||||
|
let visible = (window.screen ?? NSScreen.main)?.visibleFrame
|
||||||
|
?? NSRect(origin: .zero, size: desired)
|
||||||
|
|
||||||
|
window.contentMinSize = NSSize(
|
||||||
|
width: min(minimum.width, visible.width),
|
||||||
|
height: min(minimum.height, visible.height)
|
||||||
|
)
|
||||||
|
window.setContentSize(NSSize(
|
||||||
|
width: min(desired.width, visible.width),
|
||||||
|
height: min(desired.height, max(minimum.height, visible.height - 40))
|
||||||
|
))
|
||||||
window.center()
|
window.center()
|
||||||
|
|
||||||
|
var frame = window.frame
|
||||||
|
if frame.width > visible.width {
|
||||||
|
frame.size.width = visible.width
|
||||||
|
}
|
||||||
|
if frame.height > visible.height {
|
||||||
|
frame.size.height = visible.height
|
||||||
|
}
|
||||||
|
frame.origin.x = min(
|
||||||
|
max(frame.origin.x, visible.minX),
|
||||||
|
visible.maxX - frame.width
|
||||||
|
)
|
||||||
|
frame.origin.y = min(
|
||||||
|
max(frame.origin.y, visible.minY),
|
||||||
|
visible.maxY - frame.height
|
||||||
|
)
|
||||||
|
window.setFrame(frame, display: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dock-click reopen: let the WindowGroup re-show or recreate the
|
/// Dock-click reopen: let the WindowGroup re-show or recreate the
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ final class Milestone2UITests: XCTestCase {
|
|||||||
"ICCERY_UI_TESTING": "1",
|
"ICCERY_UI_TESTING": "1",
|
||||||
"ICCERY_TEST_ROOT": testRoot.path,
|
"ICCERY_TEST_ROOT": testRoot.path,
|
||||||
"ICCERY_ARGYLL_BINARY_DIR": binDir.path,
|
"ICCERY_ARGYLL_BINARY_DIR": binDir.path,
|
||||||
|
"ICCERY_CUPS_BIN_DIR": binDir.path,
|
||||||
"ICCERY_TEST_SAVE_TARGET":
|
"ICCERY_TEST_SAVE_TARGET":
|
||||||
workDir.appendingPathComponent("mytarget.ti1").path,
|
workDir.appendingPathComponent("mytarget.ti1").path,
|
||||||
"ICCERY_TEST_WORKDIR": workDir.path,
|
"ICCERY_TEST_WORKDIR": workDir.path,
|
||||||
@@ -218,11 +219,12 @@ final class Milestone2UITests: XCTestCase {
|
|||||||
XCTAssertTrue(FileManager.default.fileExists(
|
XCTAssertTrue(FileManager.default.fileExists(
|
||||||
atPath: workDir.appendingPathComponent("mytarget.ti2").path))
|
atPath: workDir.appendingPathComponent("mytarget.ti2").path))
|
||||||
|
|
||||||
// Print panel is live from M3; a default printer is selected
|
// Print panel is live from M3. GitHub macos-14 has no system
|
||||||
// so both the all-pages and per-page print buttons are enabled.
|
// queues, so mock CUPS (`ICCERY_CUPS_BIN_DIR`) must enumerate
|
||||||
|
// before Print All / per-page enable (run 34867767434).
|
||||||
XCTAssertTrue(element("rawPrintPanel").exists)
|
XCTAssertTrue(element("rawPrintPanel").exists)
|
||||||
XCTAssertTrue(app.buttons["btnPrintAll"].isEnabled)
|
_ = waitUntilEnabled("btnPrintAll", timeout: 15)
|
||||||
XCTAssertTrue(app.buttons["btnPrintPage-0"].isEnabled)
|
_ = waitUntilEnabled("btnPrintPage-0", timeout: 15)
|
||||||
XCTAssertTrue(app.buttons["btnAdvanceToStage3"].isEnabled)
|
XCTAssertTrue(app.buttons["btnAdvanceToStage3"].isEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,23 +98,9 @@ final class Milestone4UITests: XCTestCase {
|
|||||||
app.buttons["btnDetectInstruments"].click()
|
app.buttons["btnDetectInstruments"].click()
|
||||||
_ = waitFor("chartreadInstrumentSelect", timeout: 20)
|
_ = waitFor("chartreadInstrumentSelect", timeout: 20)
|
||||||
|
|
||||||
// Keep Auto (port 1) and start the session.
|
// Wait until Start is enabled before clicking. Existence-only
|
||||||
XCTAssertTrue(app.buttons["btnStartRead"].waitForExistence(timeout: 5))
|
// clicks are no-ops on the disabled control (runs 35251, 35293).
|
||||||
app.buttons["btnStartRead"].click()
|
driveOnePass(startButton: "btnStartRead")
|
||||||
|
|
||||||
// Calibrate.
|
|
||||||
let calibrate = element("btnCalibrate")
|
|
||||||
if !calibrate.waitForExistence(timeout: 25) {
|
|
||||||
let error = element("chartreadLastError").label
|
|
||||||
let value = element("chartreadLastError").value as? String ?? "<nil>"
|
|
||||||
XCTFail("No calibrate button. lastError.label='\(error)' value='\(value)'")
|
|
||||||
}
|
|
||||||
app.buttons["btnCalibrate"].click()
|
|
||||||
|
|
||||||
// Trigger each strip until all are read → Done & Save appears.
|
|
||||||
driveStripsUntilDone()
|
|
||||||
XCTAssertTrue(element("btnDoneRead").exists)
|
|
||||||
app.buttons["btnDoneRead"].firstMatch.click()
|
|
||||||
|
|
||||||
// Averaging panel appears with one pass snapshot.
|
// Averaging panel appears with one pass snapshot.
|
||||||
_ = waitFor("chartreadAveragingPanel", timeout: 20)
|
_ = waitFor("chartreadAveragingPanel", timeout: 20)
|
||||||
@@ -168,15 +154,34 @@ final class Milestone4UITests: XCTestCase {
|
|||||||
|
|
||||||
/// Runs the mock handheld chartread session to completion
|
/// Runs the mock handheld chartread session to completion
|
||||||
/// (start → calibrate → strip A → strip B → Done & Save).
|
/// (start → calibrate → strip A → strip B → Done & Save).
|
||||||
|
///
|
||||||
|
/// `isEnabled` can be true in AX while the SwiftUI action is still
|
||||||
|
/// a no-op (runs 35251, 35443). Re-click until the session is
|
||||||
|
/// actually running (`btnCancel` is shown whenever
|
||||||
|
/// `isChartreadRunning`), then wait for Calibrate.
|
||||||
private func driveOnePass(startButton: String) {
|
private func driveOnePass(startButton: String) {
|
||||||
let start = app.buttons[startButton]
|
let start = app.buttons[startButton]
|
||||||
XCTAssertTrue(start.waitForExistence(timeout: 10))
|
XCTAssertTrue(start.waitForExistence(timeout: 10), startButton)
|
||||||
let deadline = Date().addingTimeInterval(10)
|
let enabledBy = Date().addingTimeInterval(10)
|
||||||
while Date() < deadline, !start.isEnabled {
|
while Date() < enabledBy, !start.isEnabled {
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
|
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
|
||||||
}
|
}
|
||||||
XCTAssertTrue(start.isEnabled)
|
XCTAssertTrue(start.isEnabled, "\(startButton) never enabled")
|
||||||
start.click()
|
|
||||||
|
let sessionBy = Date().addingTimeInterval(25)
|
||||||
|
while Date() < sessionBy, !chartreadSessionRunning {
|
||||||
|
if start.exists, start.isEnabled {
|
||||||
|
start.click()
|
||||||
|
}
|
||||||
|
RunLoop.current.run(until: Date().addingTimeInterval(0.4))
|
||||||
|
}
|
||||||
|
XCTAssertTrue(
|
||||||
|
chartreadSessionRunning,
|
||||||
|
"\(startButton) click never started chartread; "
|
||||||
|
+ "notice=\(element("noticeText").value as? String ?? "") "
|
||||||
|
+ "start.exists=\(start.exists)"
|
||||||
|
)
|
||||||
|
|
||||||
_ = waitFor("btnCalibrate", timeout: 25)
|
_ = waitFor("btnCalibrate", timeout: 25)
|
||||||
app.buttons["btnCalibrate"].click()
|
app.buttons["btnCalibrate"].click()
|
||||||
driveStripsUntilDone()
|
driveStripsUntilDone()
|
||||||
@@ -184,6 +189,14 @@ final class Milestone4UITests: XCTestCase {
|
|||||||
app.buttons["btnDoneRead"].firstMatch.click()
|
app.buttons["btnDoneRead"].firstMatch.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `btnCancel` is in the tree for the whole chartread session;
|
||||||
|
/// Calibrate/Trigger only appear after the first classified prompt.
|
||||||
|
private var chartreadSessionRunning: Bool {
|
||||||
|
element("btnCancel").exists
|
||||||
|
|| element("btnCalibrate").exists
|
||||||
|
|| element("btnTrigger").exists
|
||||||
|
}
|
||||||
|
|
||||||
/// Clicks Trigger for each remaining strip until `btnDoneRead`
|
/// Clicks Trigger for each remaining strip until `btnDoneRead`
|
||||||
/// appears — the button is re-polled each pass so a click that
|
/// appears — the button is re-polled each pass so a click that
|
||||||
/// races a state transition isn't lost.
|
/// races a state transition isn't lost.
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
@startuml ICCery-UI-sheets
|
|
||||||
title Sheets Settings, presets, media, Spot Read, Gamut, project
|
|
||||||
!theme plain
|
|
||||||
skinparam backgroundColor white
|
|
||||||
skinparam defaultFontColor black
|
|
||||||
skinparam defaultFontSize 13
|
|
||||||
skinparam mapBackgroundColor white
|
|
||||||
skinparam mapBorderColor #222222
|
|
||||||
skinparam mapFontColor black
|
|
||||||
skinparam arrowColor #222222
|
|
||||||
skinparam shadowing false
|
|
||||||
hide circle
|
|
||||||
top to bottom direction
|
|
||||||
|
|
||||||
map "SettingsView.swift" as set {
|
|
||||||
Argyll dir => TextField+Browse : empty = bundled
|
|
||||||
Default instrument => Picker : seeds Stage 3 and Spot Read
|
|
||||||
Enable i1Pro 2 LEDs => Toggle
|
|
||||||
settingsDeltaEGood => TextField
|
|
||||||
settingsDeltaEWarning => TextField : must be greater than Good
|
|
||||||
settingsCalStaleDays => TextField
|
|
||||||
Install location => Picker User or System
|
|
||||||
Ask before overwriting => Toggle
|
|
||||||
Open ColorSync after install => Toggle
|
|
||||||
Log level => Picker
|
|
||||||
Open log folder => Button
|
|
||||||
Cancel / Save => Save stays if validation fails
|
|
||||||
}
|
|
||||||
|
|
||||||
map "Presets PresetDialogs.swift" as pre {
|
|
||||||
savePresetName => TextField
|
|
||||||
savePresetDesc => TextField
|
|
||||||
btnCloseSavePresetDialog => Button
|
|
||||||
btnConfirmSavePreset => Button : disabled if name empty
|
|
||||||
presetRow-id => Row : built-in cannot delete
|
|
||||||
btnImportPreset => Button
|
|
||||||
btnExportActivePreset => Button : custom selected
|
|
||||||
btnCloseManagePresetsDialog => Button
|
|
||||||
}
|
|
||||||
|
|
||||||
map "Media MediaLibraryDialogs.swift" as media {
|
|
||||||
saveMediaName/Paper/Ink => TextField
|
|
||||||
saveMediaApplyCal => Toggle : disabled if CAL_ or missing file
|
|
||||||
btnConfirmSaveMedia => Button : name+paper+ink required
|
|
||||||
mediaLibraryList => List
|
|
||||||
btnMediaLibraryApply-id => Button per row
|
|
||||||
manageMediaNotice => Caption : failed Apply in-sheet
|
|
||||||
btnMediaLibraryApply => Button : disabled if no selection
|
|
||||||
btnCloseManageMediaDialog => Button
|
|
||||||
}
|
|
||||||
|
|
||||||
map "spotReadView SpotReadView.swift" as spot {
|
|
||||||
btnSpotDetectInstruments => Button
|
|
||||||
spotInstrumentSelect => Picker : disabled while running
|
|
||||||
spotSetDefault => Toggle
|
|
||||||
btnSpotStart => Button : needs sidecar, cwd, not chartread
|
|
||||||
btnSpotCalibrate => Button : calibrating
|
|
||||||
btnSpotTrigger => Button : Read, awaitingStrip
|
|
||||||
btnSpotStop => Button : while running
|
|
||||||
btnSpotCopyLab => Button : disabled if no sample
|
|
||||||
btnSpotExportCsv => Button : disabled if no history
|
|
||||||
btnCloseSpotRead => Button Esc : dismiss = Stop
|
|
||||||
}
|
|
||||||
|
|
||||||
map "gamutView GamutView.swift" as gam {
|
|
||||||
gamutLayer-srgb => Toggle : can hide, cannot remove
|
|
||||||
gamutLayer-profile => Toggle : disabled if no .gam
|
|
||||||
gamutLayer-compare => Toggle : disabled if no compare
|
|
||||||
btnGamutAddCompare => Menu
|
|
||||||
btnGamutRemoveCompare => Button
|
|
||||||
btnGamutSampleTiff => Button
|
|
||||||
btnResetGamutCamera => Button R
|
|
||||||
gamutLabEntryL/A/B => TextField
|
|
||||||
btnGamutInspectLab => Button
|
|
||||||
btnCloseGamut => Button Esc
|
|
||||||
}
|
|
||||||
|
|
||||||
map "Project alerts" as proj {
|
|
||||||
projectNewAlert => Alert : New Cancel / Confirm
|
|
||||||
Dirty save => Alert : Save / Don't Save / Cancel
|
|
||||||
projectRelocateSheet => Sheet : cwd missing on Open
|
|
||||||
}
|
|
||||||
|
|
||||||
set -[hidden]down- pre
|
|
||||||
pre -[hidden]down- media
|
|
||||||
media -[hidden]down- spot
|
|
||||||
spot -[hidden]down- gam
|
|
||||||
gam -[hidden]down- proj
|
|
||||||
@enduml
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
@startuml ICCery-UI-shell
|
|
||||||
title Shell, sidebar, stepper, File menu
|
|
||||||
!theme plain
|
|
||||||
skinparam backgroundColor white
|
|
||||||
skinparam defaultFontColor black
|
|
||||||
skinparam defaultFontSize 13
|
|
||||||
skinparam mapBackgroundColor white
|
|
||||||
skinparam mapBorderColor #222222
|
|
||||||
skinparam mapFontColor black
|
|
||||||
skinparam arrowColor #222222
|
|
||||||
skinparam shadowing false
|
|
||||||
hide circle
|
|
||||||
top to bottom direction
|
|
||||||
|
|
||||||
map "Window" as win {
|
|
||||||
WindowGroup => ICCeryApp.swift : 1280x800 min 1100x700
|
|
||||||
RootView => RootView.swift : hosts every sheet
|
|
||||||
noticeText => NoticeBanner : shown if wizard.notice set; auto-hide 6s
|
|
||||||
}
|
|
||||||
|
|
||||||
map "Sidebar header" as hdr {
|
|
||||||
openSettingsBtn => Button : always / Settings sheet
|
|
||||||
openAboutBtn => Button : always / About
|
|
||||||
btnToggleAllHelp => Button : toggles yellow help dots
|
|
||||||
}
|
|
||||||
|
|
||||||
map "Presets" as pre {
|
|
||||||
presetSelect => Picker.menu : none + preset id; none is not factory reset
|
|
||||||
btnSavePresetModal => Button : always
|
|
||||||
btnOpenPresetsDialog => Button : always
|
|
||||||
}
|
|
||||||
|
|
||||||
map "Media library" as med {
|
|
||||||
mediaSelect => Picker.menu : never reuses presetSelect
|
|
||||||
mediaRecipeStale => Caption : shown if staleReasons nonempty
|
|
||||||
btnMediaLibraryCapture => Button : disabled if no Stage 2 printer
|
|
||||||
btnMediaLibraryManage => Button : always
|
|
||||||
}
|
|
||||||
|
|
||||||
map "Studio (not stepper)" as stu {
|
|
||||||
btnCalibratePrinter => Button : always; Stage 0
|
|
||||||
btnViewGamut => Button : sidebar always on; same id as Stage 5
|
|
||||||
btnSpotRead => Button : disabled if no cwd or chartread running
|
|
||||||
}
|
|
||||||
|
|
||||||
map "Stepper 1-5 disk is truth" as stp {
|
|
||||||
Stage1 Generate => always
|
|
||||||
Stage2 Lay out => .ti1
|
|
||||||
Stage3 Measure => .ti1 AND .ti2
|
|
||||||
Stage4 Build => .ti3 (not .ti2 alone)
|
|
||||||
Stage5 Verify => .ti3 AND .icc/.icm
|
|
||||||
Calibrate => not a stepper row; always available
|
|
||||||
}
|
|
||||||
|
|
||||||
map "Project chip" as chip {
|
|
||||||
projectChipName => Text : bound name or No project
|
|
||||||
projectChipPath => Text : shown if bound
|
|
||||||
projectChipStale => Caption : diskBehindNotes
|
|
||||||
btnProjectReveal => Button : shown if bound
|
|
||||||
btnProjectSave => Button : canSave = bound + basename + cwd
|
|
||||||
btnProjectOpen => Button : shown if not bound
|
|
||||||
}
|
|
||||||
|
|
||||||
map "File menu" as menu {
|
|
||||||
menuProjectNew => Button Cmd-N
|
|
||||||
menuProjectOpen => Button Cmd-O
|
|
||||||
menuProjectRecents => Menu : disabled if recents empty
|
|
||||||
menuProjectSave => Button Cmd-S : !canSave
|
|
||||||
menuProjectSaveAs => Button Shift-Cmd-S : basename + cwd
|
|
||||||
menuProjectReport => Button : !canReport
|
|
||||||
menuProjectClose => Button : !isBound
|
|
||||||
}
|
|
||||||
|
|
||||||
win -[hidden]down- hdr
|
|
||||||
hdr -[hidden]down- pre
|
|
||||||
pre -[hidden]down- med
|
|
||||||
med -[hidden]down- stu
|
|
||||||
stu -[hidden]down- stp
|
|
||||||
stp -[hidden]down- chip
|
|
||||||
chip -[hidden]down- menu
|
|
||||||
@enduml
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
@startuml ICCery-UI-stage1-2
|
|
||||||
title Stages 1-2 Generate Target / Lay Out and Print
|
|
||||||
!theme plain
|
|
||||||
skinparam backgroundColor white
|
|
||||||
skinparam defaultFontColor black
|
|
||||||
skinparam defaultFontSize 13
|
|
||||||
skinparam mapBackgroundColor white
|
|
||||||
skinparam mapBorderColor #222222
|
|
||||||
skinparam mapFontColor black
|
|
||||||
skinparam arrowColor #222222
|
|
||||||
skinparam shadowing false
|
|
||||||
hide circle
|
|
||||||
top to bottom direction
|
|
||||||
|
|
||||||
map "stage-1 Stage1View.swift" as s1 {
|
|
||||||
colourSpace => Picker.segmented : RGB or CMYK
|
|
||||||
patchCountPreset => Picker
|
|
||||||
patchCountCustom => TextField : shown if preset is custom
|
|
||||||
whitePatches => Stepper 0-50
|
|
||||||
blackPatches => Stepper 0-50
|
|
||||||
targetBasename => TextField
|
|
||||||
btnBrowse => Button
|
|
||||||
btnSelectWorkDir => Button
|
|
||||||
btnOpenExisting => Button : .ti1 or .ti2
|
|
||||||
btn-import-dataset => Button : unlocks stage 4
|
|
||||||
selectedPathDisplay => Text
|
|
||||||
targenAdvancedDetails => DisclosureGroup : UI tests pre-expand
|
|
||||||
btnGenerate => Button : needs basename AND directory
|
|
||||||
targenLog => ProcessLogView
|
|
||||||
}
|
|
||||||
|
|
||||||
map "Stage 1 Advanced (inside disclosure)" as adv {
|
|
||||||
targenGreySteps => Toggle+field : field iff on
|
|
||||||
targenSingleChannelSteps => Toggle+field
|
|
||||||
targenNeutralSteps => Toggle+field
|
|
||||||
targenNeutralConcentration => Toggle+Slider 0-1
|
|
||||||
targenAdaptation => Toggle+Slider 0-1
|
|
||||||
targenPrecondProfile => TextField
|
|
||||||
btnBrowsePrecondProfile => Button
|
|
||||||
targenHighQuality => Toggle
|
|
||||||
targenAlgorithm => Picker
|
|
||||||
targenInkLimitGroup => Toggle+field : CMYK only
|
|
||||||
targenDarkEmphasis => Toggle+Slider 0-3
|
|
||||||
targenDevicePower => Toggle+Slider 0-3
|
|
||||||
}
|
|
||||||
|
|
||||||
map "stage-2 Stage2View.swift" as s2 {
|
|
||||||
cmWarningBanner => Banner : always on stage 2
|
|
||||||
instrumentSelect => Picker : chart code, not USB port
|
|
||||||
pageSizeSelect => Picker
|
|
||||||
customPageW / customPageH => TextField : pageSize custom
|
|
||||||
tiffDpi => Stepper 72-600
|
|
||||||
printtargLayoutOrder => Picker
|
|
||||||
printtargCustomSeed => TextField : custom seed order
|
|
||||||
btnToggleLabelEdit => Button
|
|
||||||
targetMetadataPrinter => TextField
|
|
||||||
targetMetadataInkSet => TextField
|
|
||||||
targetMetadataDriverPaper => TextField
|
|
||||||
targetMetadataActualPaper => TextField
|
|
||||||
targetLabelPreview => TextField or Text
|
|
||||||
btnCreateLayout => Button : disabled if no basename
|
|
||||||
tiffGallery => Grid : after printtarg
|
|
||||||
btnPrintPage-N => Button : disabled if no printer
|
|
||||||
}
|
|
||||||
|
|
||||||
map "rawPrintPanel" as prn {
|
|
||||||
printerSelect => Picker CUPS
|
|
||||||
printerStatusBadge => Caption
|
|
||||||
btnRefreshPrinters => Button
|
|
||||||
btnPrinterProperties => Button : disabled if no printer
|
|
||||||
printerTraySelect => Picker : if trays exist
|
|
||||||
printerMediaTypeSelect => Picker : if media types exist
|
|
||||||
btnOrientPortrait => Button
|
|
||||||
btnOrientLandscape => Button
|
|
||||||
btnPrintAll => Button : needs layout AND printer
|
|
||||||
btnAdvanceToStage3 => Button : needs .ti2 unlock
|
|
||||||
printNotificationText => Caption : if printNotice set
|
|
||||||
}
|
|
||||||
|
|
||||||
s1 -[hidden]down- adv
|
|
||||||
adv -[hidden]down- s2
|
|
||||||
s2 -[hidden]down- prn
|
|
||||||
@enduml
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
@startuml ICCery-UI-stage3-5-cal
|
|
||||||
title Stages 3-5 and Calibrate Printer
|
|
||||||
!theme plain
|
|
||||||
skinparam backgroundColor white
|
|
||||||
skinparam defaultFontColor black
|
|
||||||
skinparam defaultFontSize 13
|
|
||||||
skinparam mapBackgroundColor white
|
|
||||||
skinparam mapBorderColor #222222
|
|
||||||
skinparam mapFontColor black
|
|
||||||
skinparam arrowColor #222222
|
|
||||||
skinparam shadowing false
|
|
||||||
hide circle
|
|
||||||
top to bottom direction
|
|
||||||
|
|
||||||
map "stage-3 Stage3View.swift" as s3 {
|
|
||||||
btnDetectInstruments => Button : disabled while detecting
|
|
||||||
chartreadInstrumentSelect => Picker.menu
|
|
||||||
xyTableHint => Caption : if XY instrument
|
|
||||||
xyTablePanel => Place/Align/Scan/Remove
|
|
||||||
chartreadPrompt => Text
|
|
||||||
chartreadLastError => Caption : if notice set
|
|
||||||
btnStartRead => Button : shown if not running; needs basename+cwd
|
|
||||||
btnCalibrate => Button : running and calibrating
|
|
||||||
btnTrigger => Button : running and awaitingStrip
|
|
||||||
btnDoneReadEarly => Button : awaitingStrip
|
|
||||||
btnAccept => Button : place / align / continue / warning
|
|
||||||
btnRetry => Button : error state
|
|
||||||
btnDoneRead => Button : allStripsRead
|
|
||||||
btnCancel => Button : while running
|
|
||||||
swatchGrid => swatch-rowId-loc
|
|
||||||
btnMeasureAnotherSheet => Button : after a finished pass
|
|
||||||
btnFinishAndAverage => Button : finished AND at least one pass
|
|
||||||
}
|
|
||||||
|
|
||||||
map "stage-4 Stage4View.swift" as s4 {
|
|
||||||
colprofAlgorithm => Picker
|
|
||||||
colprofQuality => Picker
|
|
||||||
colprofFwa => Picker
|
|
||||||
colprofFwaCustomPath => TextField : custom spectrum
|
|
||||||
btnBrowseFwaSp => Button
|
|
||||||
colprofIlluminant => TextField
|
|
||||||
colprofObserver => TextField
|
|
||||||
colprofInputViewCond => TextField
|
|
||||||
colprofOutputViewCond => TextField
|
|
||||||
colprofDescription => TextField
|
|
||||||
colprofCopyright => TextField
|
|
||||||
colprofApplyCalibration => Toggle
|
|
||||||
colprofCalibrationFile => TextField : if applyCalibration
|
|
||||||
btnBrowseCalibrationFile => Button : if applyCalibration
|
|
||||||
btnCreateProfile => Button : basename + cwd, not running
|
|
||||||
}
|
|
||||||
|
|
||||||
map "stage-5 Stage5View.swift" as s5 {
|
|
||||||
btnVerifyProfile => Button : needs created profile
|
|
||||||
btnViewGamut => Button : disabled if no .gam (same id as sidebar)
|
|
||||||
btnInstallProfile => Button : needs created profile
|
|
||||||
driftPrinterFilter => Picker
|
|
||||||
btnExportHistory => Button
|
|
||||||
btnClearHistory => Button
|
|
||||||
verificationHistoryTable => Table
|
|
||||||
driftChart => not interactive
|
|
||||||
profileOverwriteBtn => Alert : install collision
|
|
||||||
profileRenameBtn => Alert
|
|
||||||
profileCancelCollisionBtn => Alert
|
|
||||||
}
|
|
||||||
|
|
||||||
map "stage-cal CalibrationView.swift" as cal {
|
|
||||||
Colour space => Picker.segmented : no a11y id
|
|
||||||
calSteps => TextField
|
|
||||||
White patches => TextField : no a11y id
|
|
||||||
calInkExplore => TextField : CMYK only
|
|
||||||
calNeutralEmphasis => Toggle
|
|
||||||
btnCalGenerate => Button : needs basename + cwd
|
|
||||||
btnCalLayout => Button : same
|
|
||||||
btnCalMeasure => Button : needs .ti3
|
|
||||||
btnCalCompute => Button : .ti3 and not computing
|
|
||||||
calApplyToggle => Toggle : after curves exist
|
|
||||||
btnCalReturn => Button Esc : restores original basename
|
|
||||||
}
|
|
||||||
|
|
||||||
s3 -[hidden]down- s4
|
|
||||||
s4 -[hidden]down- s5
|
|
||||||
s5 -[hidden]down- cal
|
|
||||||
@enduml
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Interactive UI map
|
|
||||||
|
|
||||||
PlantUML **maps** (not class diagrams). Class diagrams laid out 6500px wide, so Gitea on a phone showed a sliver of a black arrow and the UML “C” icon.
|
|
||||||
|
|
||||||
Each file is one stacked column (~560–780px) that Kroki actually fits in the page.
|
|
||||||
|
|
||||||
| Diagram | File | Kroki size |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Shell, sidebar, stepper, File menu | [ui-interactive-map-shell.puml](ui-interactive-map-shell.puml) | 563 × 1367 |
|
|
||||||
| Stages 1–2 | [ui-interactive-map-stage1-2.puml](ui-interactive-map-stage1-2.puml) | 625 × 1366 |
|
|
||||||
| Stages 3–5 + Calibrate | [ui-interactive-map-stage3-5-cal.puml](ui-interactive-map-stage3-5-cal.puml) | 568 × 1386 |
|
|
||||||
| Sheets | [ui-interactive-map-sheets.puml](ui-interactive-map-sheets.puml) | 782 × 1502 |
|
|
||||||
Reference in New Issue
Block a user