Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cba9476af7 | ||
|
|
288b08e3d9 | ||
|
|
18f1d1ff33 | ||
|
|
9dfa79ebd0 | ||
|
|
1b91b0a94e | ||
|
|
890e7281eb |
@@ -0,0 +1,229 @@
|
|||||||
|
# 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 (the Gitea runner label).
|
||||||
|
# The Gitea workflow already notes macos-14 works for this pipeline.
|
||||||
|
# - 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-26-intel
|
||||||
|
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
|
||||||
|
|||||||
@@ -220,6 +220,15 @@ struct ManageMediaDialog: View {
|
|||||||
.accessibilityIdentifier("mediaLibraryList")
|
.accessibilityIdentifier("mediaLibraryList")
|
||||||
.frame(minHeight: 260)
|
.frame(minHeight: 260)
|
||||||
|
|
||||||
|
if let notice = media.manageApplyNotice {
|
||||||
|
Text(notice)
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.accessibilityIdentifier("manageMediaNotice")
|
||||||
|
.accessibilityValue(notice)
|
||||||
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Button("Apply selected") {
|
Button("Apply selected") {
|
||||||
if let id = selection,
|
if let id = selection,
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
@Published var saveMediaApplyCal = false
|
@Published var saveMediaApplyCal = false
|
||||||
/// Inline caption inside the capture sheet (no a11y id — roster complete).
|
/// Inline caption inside the capture sheet (no a11y id — roster complete).
|
||||||
@Published var saveMediaError: String?
|
@Published var saveMediaError: String?
|
||||||
|
/// Last failed Apply while Manage is open. The window banner sits
|
||||||
|
/// behind the sheet on Monterey, so the dialog shows this too (#170).
|
||||||
|
@Published var manageApplyNotice: String?
|
||||||
|
|
||||||
/// Pure flow flag — the manage sheet's "Capture current…" asks the
|
/// Pure flow flag — the manage sheet's "Capture current…" asks the
|
||||||
/// sheet's `onDismiss` to open the capture sheet, avoiding a
|
/// sheet's `onDismiss` to open the capture sheet, avoiding a
|
||||||
@@ -120,24 +123,21 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
/// with warning; the refusal is permanent so re-clicking can't help).
|
/// with warning; the refusal is permanent so re-clicking can't help).
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func apply(_ recipe: MediaRecipe) async -> Bool {
|
func apply(_ recipe: MediaRecipe) async -> Bool {
|
||||||
|
manageApplyNotice = nil
|
||||||
guard let r = try? recipe.validated() else {
|
guard let r = try? recipe.validated() else {
|
||||||
workflow.wizard.showNotice(
|
return failApply("Media recipe is invalid — not applied.", kind: .error)
|
||||||
"Media recipe is invalid — not applied.", kind: .error)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
guard let preset = environment.presetStore.all()
|
guard let preset = environment.presetStore.all()
|
||||||
.first(where: { $0.id == r.presetID })
|
.first(where: { $0.id == r.presetID })
|
||||||
else {
|
else {
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Preset \(r.presetID) no longer exists — recipe not applied.",
|
"Preset \(r.presetID) no longer exists — recipe not applied.",
|
||||||
kind: .error)
|
kind: .error)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
guard preset.colourSpace.lowercased() == r.colourSpace.lowercased() else {
|
guard preset.colourSpace.lowercased() == r.colourSpace.lowercased() else {
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Recipe colour space does not match its preset — not applied.",
|
"Recipe colour space does not match its preset — not applied.",
|
||||||
kind: .error)
|
kind: .error)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing #82 mapping: presetSelect jumps, Stage 1/2/4 fields.
|
// Existing #82 mapping: presetSelect jumps, Stage 1/2/4 fields.
|
||||||
@@ -145,8 +145,6 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
// Literal per issue: displayName, not the queue id.
|
// Literal per issue: displayName, not the queue id.
|
||||||
workflow.wizard.printerName = r.printerDisplayName
|
workflow.wizard.printerName = r.printerDisplayName
|
||||||
|
|
||||||
var succeeded = true
|
|
||||||
|
|
||||||
// Queue: enumerate fresh via the session's serialized path —
|
// Queue: enumerate fresh via the session's serialized path —
|
||||||
// listPrinters uses fixed process ids, so an overlapping
|
// listPrinters uses fixed process ids, so an overlapping
|
||||||
// enumeration would throw duplicateID. An empty result is a
|
// enumeration would throw duplicateID. An empty result is a
|
||||||
@@ -156,16 +154,14 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
workflow.print.selectedPrinter = r.printerID
|
workflow.print.selectedPrinter = r.printerID
|
||||||
await workflow.print.reloadSelectedCapabilities()
|
await workflow.print.reloadSelectedCapabilities()
|
||||||
} else {
|
} else {
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Printer \(r.printerDisplayName) is not installed.",
|
"Printer \(r.printerDisplayName) is not installed.",
|
||||||
kind: .warning)
|
kind: .warning)
|
||||||
succeeded = false
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Could not enumerate printers — queue left unchanged.",
|
"Could not enumerate printers — queue left unchanged.",
|
||||||
kind: .warning)
|
kind: .warning)
|
||||||
succeeded = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calibration — the recipe is authoritative and runs after
|
// Calibration — the recipe is authoritative and runs after
|
||||||
@@ -195,10 +191,8 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
guard FileManager.default.fileExists(atPath: calPath) else {
|
guard FileManager.default.fileExists(atPath: calPath) else {
|
||||||
workflow.profile.applyCalibration = false
|
workflow.profile.applyCalibration = false
|
||||||
workflow.profile.calibrationFile = calPath
|
workflow.profile.calibrationFile = calPath
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Calibration file is missing: \(calPath)", kind: .error)
|
"Calibration file is missing: \(calPath)", kind: .error)
|
||||||
refreshStaleness()
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let staleDays = environment.settingsStore.load().calibrationStaleDays
|
let staleDays = environment.settingsStore.load().calibrationStaleDays
|
||||||
@@ -215,23 +209,26 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
workflow.profile.applyCalibration = false
|
workflow.profile.applyCalibration = false
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Could not load calibration: \(error.localizedDescription)",
|
"Could not load calibration: \(error.localizedDescription)",
|
||||||
kind: .error)
|
kind: .error)
|
||||||
refreshStaleness()
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
workflow.profile.applyCalibration = false
|
workflow.profile.applyCalibration = false
|
||||||
workflow.profile.calibrationFile = calPath
|
workflow.profile.calibrationFile = calPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if succeeded {
|
selectedRecipeID = r.id
|
||||||
selectedRecipeID = r.id
|
workflow.wizard.showNotice("Applied \(r.name)")
|
||||||
workflow.wizard.showNotice("Applied \(r.name)")
|
|
||||||
}
|
|
||||||
refreshStaleness()
|
refreshStaleness()
|
||||||
return succeeded
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func failApply(_ text: String, kind: Notice.Kind) -> Bool {
|
||||||
|
manageApplyNotice = text
|
||||||
|
workflow.wizard.showNotice(text, kind: kind)
|
||||||
|
refreshStaleness()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Capture
|
// MARK: - Capture
|
||||||
|
|||||||
@@ -148,7 +148,9 @@ final class Milestone10MediaLibraryUITests: XCTestCase {
|
|||||||
XCTAssertTrue(apply.waitForExistence(timeout: 10))
|
XCTAssertTrue(apply.waitForExistence(timeout: 10))
|
||||||
apply.click()
|
apply.click()
|
||||||
|
|
||||||
let notice = waitFor("noticeText")
|
// The window banner (`noticeText`) sits behind this sheet on
|
||||||
|
// Monterey (#170). Assert the in-sheet copy instead.
|
||||||
|
let notice = waitFor("manageMediaNotice", timeout: 15)
|
||||||
let text = (notice.value as? String) ?? notice.label
|
let text = (notice.value as? String) ?? notice.label
|
||||||
XCTAssertTrue(
|
XCTAssertTrue(
|
||||||
text.contains("is not installed"),
|
text.contains("is not installed"),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user