Compare commits

..
Author SHA1 Message Date
gronod 67fb3a9b37 Merge pull request 'test(m9): collapse ArgyllRunner waits under ICCERY_UI_TESTING' (#144) from feat/141-ui-test-timing into milestone/m9-monterey
macOS CI / build-and-test (push) Successful in 19m52s
macOS CI / package (push) Successful in 3m12s
2026-09-12 17:00:47 +01:00
gronod eb1503aba9 Merge pull request 'test(m9): replace full-timeout negative waits with dwell assertion' (#143) from feat/140-ui-test-absence-waits into milestone/m9-monterey 2026-09-12 17:00:40 +01:00
gronod db47a975f0 Merge pull request 'ci(m9): skip probe test in full UI suite leg' (#142) from feat/139-skip-probe-rerun into milestone/m9-monterey 2026-09-12 17:00:32 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 990c49a436 test(m9): collapse ArgyllRunner waits under ICCERY_UI_TESTING (#141)
Add a DEBUG-gated testAwareDelay helper (same ICCERY_UI_TESTING=1
convention as AppPaths.testRoot) and apply it to the ensureNotRunning
100ms poll and the XY chartread 500ms pre-kill wait. Under UI tests the
intervals shrink ~10x; release builds compile the branch out entirely,
so production timing is unchanged. No current UI test exercises XY
cancel, so the 500ms site is future-proofing at zero risk today.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 16:55:56 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 60972a0704 test(m9): replace full-timeout negative waits with dwell assertion (#140)
Two XCTAssertFalse(el.waitForExistence(timeout: 3)) sites in
Milestone2UITests always burned the full 3s on the expected-absent
path. Add assertAbsent(_:dwell:) (0.5s dwell + .exists) and use it at
both sites — same coverage, ~6s back per suite run.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 16:53:30 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> aa32cc45d3 ci(m9): skip probe test in full UI suite leg (#139)
The attach probe already runs testAboutDialogShowsVersionAndBuildDate;
the full-suite leg reran it for ~30s of zero extra coverage. Add
-skip-testing so it only runs in the probe leg.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 16:50:57 +01:00
gronod 17c47af9f1 ci(m9): build tests for host arch only; reserve universal for release packaging
macOS CI / build-and-test (push) Successful in 20m59s
macOS CI / package (push) Successful in 3m24s
The CI runner only executes tests on its own architecture, so building
both slices wastes time. Override ARCHS="$(uname -m)" in the workflow
and update all docs to match. Universal builds remain required for
release verification and dmgbuild packaging.
2026-09-12 15:59:20 +01:00
gronod 5bea928042 Merge pull request 'test(m9): query presetSelect as PopUpButton to avoid duplicate match (#137)' (#138) from feat/137-presetselect-dup into milestone/m9-monterey
macOS CI / build-and-test (push) Successful in 19m6s
macOS CI / package (push) Successful in 3m4s
2026-09-12 14:33:41 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 5c8973de73 test(m9): query presetSelect as PopUpButton to avoid duplicate match (#137)
On the runner SDK the Picker label inherits the control accessibility
identifier, so the .any query for presetSelect matches both a
StaticText and the PopUpButton and .frame resolution fails.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 14:33:26 +01:00
gronod 29ecc79585 Merge pull request 'test(m9): drive scroller + state-driven triggers in UI suite (#135)' (#136) from feat/135-uitest-scroller-trigger into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 20m7s
macOS CI / package (push) Skipped
2026-09-12 14:04:30 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 7c5a5e7798 test(m9): drive scroller + state-driven triggers in UI suite (#135)
btnPrintPage-0 reports an in-viewport a11y frame but stays unhittable
and scroll-wheel events on the LazyVGrid are inert; drag the vertical
AXScrollBar thumb to perform a real scroll before clicking.

The handheld chartread test could drop a btnTrigger click racing the
strip state transition; poll btnTrigger until btnDoneRead appears
instead of assuming exactly two strips.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 14:03:59 +01:00
gronod 607a4e0df4 Merge pull request 'test(m9): fall back to coordinate click for per-page print button (#132)' (#134) from feat/132-perpage-scroll2 into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 20m28s
macOS CI / package (push) Skipped
2026-09-12 13:30:19 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 67199877a2 test(m9): fall back to coordinate click for per-page print button (#132)
Explicit stage-2 scrolls are synthesized but never move the LazyVGrid
cell's a11y frame (run 32235). If still not hittable, dump the a11y
tree and click the reported point directly — the lp argv assertion
verifies the click landed.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 13:30:04 +01:00
gronod 9a6da4648f Merge pull request 'test(m9): scroll stage-2 explicitly before per-page print click (#132)' (#133) from feat/132-perpage-scroll into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 22m43s
macOS CI / package (push) Skipped
2026-09-12 13:02:38 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> b5250591ea test(m9): scroll stage-2 explicitly before per-page print click (#132)
btnPrintPage-0 sits at the window's bottom edge on the 1280x800 runner;
XCUI's automatic scroll-to-visible runs but never moves the frame
(run 32209). Scroll the stage-2 ScrollView manually until hittable.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 13:02:24 +01:00
gronod c1ec07f726 Merge pull request 'fix(m9): revert sidebar container a11y id; measure presetSelect frame (#130)' (#131) from feat/130-sidebar-a11y-revert into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 18m59s
macOS CI / package (push) Skipped
2026-09-12 12:37:39 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> a975f1a4e4 fix(m9): revert sidebar container a11y id; measure presetSelect frame (#130)
On SDK 13.1 the 'sidebar' identifier on the root VStack propagates to
every child, erasing openSettingsBtn/openAboutBtn/btnToggleAllHelp
(a11y dump, run 32201). Revert it; the test now compares presetSelect
frame before/after toggling help overlays.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 12:37:21 +01:00
gronod 7c2282a7dd Merge pull request 'test(m9): dump a11y hierarchy to log on UI probe failure (#128)' (#129) from feat/128-axtree-dump into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 3m43s
macOS CI / package (push) Skipped
2026-09-12 12:31:00 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> caeafb22be test(m9): dump a11y hierarchy to log on UI probe failure (#128)
The probe fails on the runner with an apparently empty accessibility
tree; the xcresult artifact is not retrievable via the API. Print
app.debugDescription + window count so the tree lands in the job log.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 12:30:44 +01:00
gronod aac4f10d23 Merge pull request 'ci(m9): upload UI-test xcresult on failure for a11y triage (#126)' (#127) from feat/126-xcresult-on-failure into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 4m8s
macOS CI / package (push) Skipped
2026-09-12 12:23:12 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> b7ac4f92ed ci(m9): upload UI-test xcresult on failure for a11y triage (#126)
UI failures on the self-hosted runner are hard to triage from stdout
alone — the xcresult already contains the a11y hierarchy snapshot and
screenshots. Upload Logs/Test when build-and-test fails.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 12:22:53 +01:00
gronod 3244efd366 Merge pull request 'ci(m9): accept Xcode >= 14 in toolchain assert (#123)' (#124) from feat/123-xcode-ge-14 into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 5m0s
macOS CI / package (push) Skipped
2026-09-12 11:39:47 +01:00
gronod 7a05da3a35 Merge pull request 'fix(m9): stable 'sidebar' a11y identifier for AboutHelp UI test (#122)' (#125) from feat/122-sidebar-a11y-id into milestone/m9-monterey 2026-09-12 11:39:35 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 5134e82e63 fix(m9): stable 'sidebar' a11y identifier for AboutHelp UI test (#122)
Xcode 14.2 / SDK 13.1 does not surface the sidebar root as an AXGroup,
so the groups-containing query in testHelpOverlaysDoNotChangeSidebarHeight
found nothing (run 32029). Give the sidebar root a stable identifier and
query it directly.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 11:39:16 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 7fd93247cb ci(m9): accept Xcode >= 14 in toolchain assert (#123)
The grep on "Xcode 14." pinned CI to the 14.x series, blocking builds
on macOS 14 / Xcode 16 runners. Parse the major version and require
>= 14 instead.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 11:31:43 +01:00
gronod a0fcede454 Merge pull request 'fix(m9): Debug entitlements disable library validation for test host (#119)' (#121) from feat/119-debug-entitlements into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 21m3s
macOS CI / package (push) Skipped
2026-09-12 09:42:16 +01:00
gronod 97eafd11fb fix(m9): Debug entitlements file to disable library validation for test host (#119) 2026-09-12 09:41:11 +01:00
gronod ed7487c2b6 Merge pull request 'fix(m9): ad-hoc sign embedded ICCeryCore framework so test host launches (#119)' (#120) from feat/119-debug-disable-lib-validation into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 3m15s
macOS CI / package (push) Skipped
Reviewed-on: #120
2026-09-12 09:30:54 +01:00
gronod a2e3f11e70 fix(m9): ad-hoc sign embedded ICCeryCore framework so test host launches (#119) 2026-09-12 09:29:40 +01:00
gronod c3c9bbc5ba Merge pull request 'fix(m9): link ICCeryCore statically so the app launches standalone (#117)' (#118) from feat/117-static-iccerycore into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 26s
macOS CI / package (push) Skipped
Reviewed-on: #118
2026-09-12 09:17:22 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> b5683aa36a fix(m9): link ICCeryCore statically so the app launches standalone (#117)
When the app and test bundles share the local ICCeryCore package, Xcode
builds it as a dynamic package-product framework that is not embedded
in ICCery.app — the app crashes in dyld at launch when run outside the
test host (macOS 12 VM crash, run 31985 products). Declare the product
.static so each consumer links it directly.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 09:14:54 +01:00
gronod 707455dcfb Merge pull request 'test(m9): replace MainActor.assertIsolated for Swift 5.7 (#115)' (#116) from feat/115-assertisolated-swift57 into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 2m53s
macOS CI / package (push) Skipped
Reviewed-on: #116
2026-09-12 09:07:06 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 17ee5d6717 test(m9): replace MainActor.assertIsolated for Swift 5.7 (#115)
MainActor.assertIsolated() requires Swift 5.9 (Xcode 15); the macOS 12
CI runner compiles the test bundle with Swift 5.7 and failed run 31985.
Assert Thread.isMainThread instead — it checks the same contract (the
coalesced log sink hops back to the main actor).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 09:06:45 +01:00
gronod 2295bcdbae Merge pull request 'fix(m9): clear printTask without defer for Swift 5.7 (#113)' (#114) from feat/113-printtask-defer-swift57 into milestone/m9-monterey
macOS CI / build-and-test (push) Failing after 1m59s
macOS CI / package (push) Skipped
Reviewed-on: #114
2026-09-12 09:01:37 +01:00
13 changed files with 186 additions and 39 deletions
+52 -5
View File
@@ -21,8 +21,14 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Assert Xcode 14 toolchain - name: Assert Xcode 14+ toolchain
run: xcodebuild -version | grep -E "Xcode 14." || (echo "Unexpected Xcode version" && exit 1) 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"
# Homebrew's xcodegen formula requires Xcode 15.3, which cannot be # Homebrew's xcodegen formula requires Xcode 15.3, which cannot be
# installed on macOS 12 (#109). The script installs a pinned # installed on macOS 12 (#109). The script installs a pinned
@@ -33,18 +39,34 @@ jobs:
- name: Generate Xcode project - name: Generate Xcode project
run: xcodegen generate --spec project.yml run: xcodegen generate --spec project.yml
- name: Build for testing (universal) # 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)
run: | run: |
xcodebuild build-for-testing \ xcodebuild build-for-testing \
-scheme ICCery \ -scheme ICCery \
-destination 'platform=macOS' \ -destination 'platform=macOS' \
-derivedDataPath "$DERIVED" \ -derivedDataPath "$DERIVED" \
-configuration Debug \ -configuration Debug \
ARCHS='arm64 x86_64' \ ARCHS="$(uname -m)" \
ONLY_ACTIVE_ARCH=NO \ ONLY_ACTIVE_ARCH=NO \
CODE_SIGNING_ALLOWED=YES \ CODE_SIGNING_ALLOWED=YES \
CODE_SIGN_IDENTITY='-' 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
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) - name: Test unit (ICCeryCoreTests)
run: | run: |
XCTESTRUN="$(find "$DERIVED" -name 'ICCery*.xctestrun' | head -n 1)" XCTESTRUN="$(find "$DERIVED" -name 'ICCery*.xctestrun' | head -n 1)"
@@ -111,7 +133,8 @@ jobs:
echo "error: UI probe failed with a real test error" >&2 echo "error: UI probe failed with a real test error" >&2
exit 1 exit 1
fi fi
if run_ui "full suite attempt $attempt" -only-testing:ICCeryUITests; then if run_ui "full suite attempt $attempt" -only-testing:ICCeryUITests \
-skip-testing:ICCeryUITests/AboutHelpUITests/testAboutDialogShowsVersionAndBuildDate; then
exit 0 exit 0
fi fi
if is_runner_attach_failure; then if is_runner_attach_failure; then
@@ -127,6 +150,30 @@ jobs:
echo "warning: skipping UI tests after repeated runner attach/activate failures" echo "warning: skipping UI tests after repeated runner attach/activate failures"
exit 0 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: Prepare Node CA bundle (failure path)
if: failure()
run: |
NODE_CA_FILE="/tmp/macos-ca-bundle.pem"
security find-certificate -a -p \
/System/Library/Keychains/SystemRootCertificates.keychain \
/Library/Keychains/System.keychain \
> "$NODE_CA_FILE" 2>/dev/null || true
if [ ! -s "$NODE_CA_FILE" ] && [ -f /etc/ssl/cert.pem ]; then
cp /etc/ssl/cert.pem "$NODE_CA_FILE"
fi
- name: Upload UI test xcresult
if: failure()
uses: actions/upload-artifact@v3
env:
NODE_EXTRA_CA_CERTS: /tmp/macos-ca-bundle.pem
with:
name: ui-test-xcresult
path: build/DerivedData-test/Logs/Test
package: package:
needs: build-and-test needs: build-and-test
runs-on: macos-12 runs-on: macos-12
+2 -1
View File
@@ -45,9 +45,10 @@ PRs via Gitea MCP. Every issue/PR: `Project/ICCery-v2` + `Feature/*` or `Bug/*`
## Verify ## Verify
``` ```
xcodebuild test -scheme ICCery -destination 'platform=macOS' ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO xcodebuild test -scheme ICCery -destination 'platform=macOS' ARCHS="$(uname -m)"
codesign -dvv <sidecar> codesign -dvv <sidecar>
``` ```
Universal (`ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO`) is still required for release verification / packaging.
## Private ColorSync SPI ## Private ColorSync SPI
2-arg `(PMPrintSession, CFStringRef) -> OSStatus`. Never pass integer `1`. 2-arg `(PMPrintSession, CFStringRef) -> OSStatus`. Never pass integer `1`.
@@ -192,6 +192,18 @@ public struct ArgyllRunner: Sendable {
// MARK: - Shared collection // MARK: - Shared collection
/// DEBUG-only fast path: under `ICCERY_UI_TESTING=1` polling/wait
/// intervals shrink ~10x same env convention as `AppPaths.testRoot`.
/// Release builds compile the branch out entirely; no static state.
private static func testAwareDelay(_ nanos: UInt64) -> UInt64 {
#if DEBUG
if ProcessInfo.processInfo.environment["ICCERY_UI_TESTING"] == "1" {
return nanos / 10
}
#endif
return nanos
}
/// Cancels any previous child with the same id and waits for it to /// Cancels any previous child with the same id and waits for it to
/// finalize, so `runStreaming` / `runCaptured` never sees a /// finalize, so `runStreaming` / `runCaptured` never sees a
/// `duplicateID` from a leftover process (#50, #52). /// `duplicateID` from a leftover process (#50, #52).
@@ -200,7 +212,7 @@ public struct ArgyllRunner: Sendable {
await processManager.kill(id: id) await processManager.kill(id: id)
var attempts = 0 var attempts = 0
while await processManager.isRunning(id), attempts < 30 { while await processManager.isRunning(id), attempts < 30 {
try? await Task.sleep(nanoseconds: 100_000_000) try? await Task.sleep(nanoseconds: Self.testAwareDelay(100_000_000))
attempts += 1 attempts += 1
} }
} }
@@ -592,7 +604,7 @@ public struct ArgyllRunner: Sendable {
await processManager.setPreKillHook(id: processId) { [processManager] in await processManager.setPreKillHook(id: processId) { [processManager] in
if isXY { if isXY {
try? await processManager.sendStdin(id: processId, bytes: ChartreadInput.quit.bytes) try? await processManager.sendStdin(id: processId, bytes: ChartreadInput.quit.bytes)
try? await Task.sleep(nanoseconds: 500_000_000) try? await Task.sleep(nanoseconds: Self.testAwareDelay(500_000_000))
} }
} }
+8 -3
View File
@@ -57,9 +57,11 @@ Equivalent without Make:
xcodegen generate xcodegen generate
xcodebuild test -scheme ICCery \ xcodebuild test -scheme ICCery \
-destination 'platform=macOS' \ -destination 'platform=macOS' \
ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO ARCHS="$(uname -m)"
``` ```
`project.yml` sets `ARCHS: "$(ARCHS_STANDARD)"`, so a plain `xcodebuild test` (and `make test`) builds universal; the `ARCHS="$(uname -m)"` override narrows it to the host slice.
Sidecars are **not** in git. `scripts/fetch-argyll.sh` pulls the latest (or `ARGYLL_RELEASE_TAG`) macOS-universal release from `gronod/argyllcms`, extracts to `Vendor/Argyll/macos-universal/`, ad-hoc signs every Mach-O, and fails if `codesign -dvv` or the `instlist` marker is missing. Sidecars are **not** in git. `scripts/fetch-argyll.sh` pulls the latest (or `ARGYLL_RELEASE_TAG`) macOS-universal release from `gronod/argyllcms`, extracts to `Vendor/Argyll/macos-universal/`, ad-hoc signs every Mach-O, and fails if `codesign -dvv` or the `instlist` marker is missing.
```bash ```bash
@@ -124,10 +126,13 @@ docs/ functional spec + v2 ticket plan
## Tests ## Tests
```bash ```bash
# full suite (universal) # full suite (host arch)
xcodebuild test -scheme ICCery \ xcodebuild test -scheme ICCery \
-destination 'platform=macOS' \ -destination 'platform=macOS' \
ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO ARCHS="$(uname -m)"
# to compile-check both slices instead:
# ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO
# examples # examples
xcodebuild test -scheme ICCery -destination 'platform=macOS' \ xcodebuild test -scheme ICCery -destination 'platform=macOS' \
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- App Sandbox intentionally absent: ICCery must spawn Argyll tools,
read/write user-chosen working directories, and talk to lp/CUPS. -->
<key>com.apple.security.device.usb</key>
<true/>
<!-- Debug only: the shared ICCeryCore package framework embedded in the
test products is ad-hoc signed with no Team ID, so hardened-runtime
library validation kills the test host at launch (run 31992, #119).
Release uses ICCery.entitlements and links the package statically. -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
@@ -21,7 +21,9 @@ final class ProcessRunSupportTests: XCTestCase {
setRunning: { running.append($0) }, setRunning: { running.append($0) },
resetLog: { resets += 1 }, resetLog: { resets += 1 },
onLog: { batch in onLog: { batch in
MainActor.assertIsolated() // MainActor.assertIsolated() needs Swift 5.9; the runner is
// on Xcode 14.2 (Swift 5.7) (#115).
XCTAssertTrue(Thread.isMainThread)
received.append(contentsOf: batch) received.append(contentsOf: batch)
} }
) { onLog in ) { onLog in
+17 -6
View File
@@ -46,7 +46,12 @@ final class AboutHelpUITests: XCTestCase {
launchApp() launchApp()
let openAbout = app.buttons["openAboutBtn"] let openAbout = app.buttons["openAboutBtn"]
XCTAssertTrue(openAbout.waitForExistence(timeout: 10)) if !openAbout.waitForExistence(timeout: 10) {
// CI triage (#128): print the a11y tree so an empty or
// unexpected hierarchy shows up directly in the job log.
print("AXTREE-BEGIN windows=\(app.windows.count)\n\(app.debugDescription)\nAXTREE-END")
}
XCTAssertTrue(openAbout.exists)
openAbout.click() openAbout.click()
_ = waitFor("aboutVersion", timeout: 10) _ = waitFor("aboutVersion", timeout: 10)
@@ -64,14 +69,20 @@ final class AboutHelpUITests: XCTestCase {
let toggle = app.buttons["btnToggleAllHelp"] let toggle = app.buttons["btnToggleAllHelp"]
XCTAssertTrue(toggle.waitForExistence(timeout: 10)) XCTAssertTrue(toggle.waitForExistence(timeout: 10))
let sidebar = app.groups.containing(.button, identifier: "openSettingsBtn").element // SDK 13.1 emits no AXGroup for the sidebar root, and an
let before = sidebar.frame // identifier on the container clobbers child identifiers
// (#130) measure a stable sidebar child instead. Query the
// pop-up by type: the Picker's "Preset" label inherits the same
// identifier, so an .any query matches twice.
let sidebarChild = app.popUpButtons["presetSelect"]
XCTAssertTrue(sidebarChild.waitForExistence(timeout: 10))
let before = sidebarChild.frame
toggle.click() toggle.click()
let after = sidebar.frame let after = sidebarChild.frame
XCTAssertEqual(before.size.height, after.size.height, XCTAssertEqual(before, after,
"Toggling global help must not reflow the sidebar height.") "Toggling global help must not reflow the sidebar.")
XCTAssertTrue(app.descendants(matching: .any)["openSettingsBtn"].exists) XCTAssertTrue(app.descendants(matching: .any)["openSettingsBtn"].exists)
} }
} }
+12 -2
View File
@@ -85,6 +85,16 @@ final class Milestone2UITests: XCTestCase {
return el return el
} }
/// Assert an element stays absent after a short dwell unlike
/// `waitForExistence`, which always burns its full timeout on the
/// negative path.
private func assertAbsent(_ el: XCUIElement, dwell: TimeInterval = 0.5,
_ message: String = "expected element to stay absent",
file: StaticString = #filePath, line: UInt = #line) {
RunLoop.current.run(until: Date().addingTimeInterval(dwell))
XCTAssertFalse(el.exists, message, file: file, line: line)
}
private func staticText(_ exact: String) -> XCUIElement { private func staticText(_ exact: String) -> XCUIElement {
let inApp = app.staticTexts[exact] let inApp = app.staticTexts[exact]
if inApp.exists { return inApp } if inApp.exists { return inApp }
@@ -316,7 +326,7 @@ final class Milestone2UITests: XCTestCase {
"identifier BEGINSWITH 'btnDeletePreset-'") "identifier BEGINSWITH 'btnDeletePreset-'")
XCTAssertTrue(deleteButtons.firstMatch.waitForExistence(timeout: 5)) XCTAssertTrue(deleteButtons.firstMatch.waitForExistence(timeout: 5))
deleteButtons.firstMatch.click() deleteButtons.firstMatch.click()
XCTAssertFalse(staticText("UI Test Preset").waitForExistence(timeout: 3)) assertAbsent(staticText("UI Test Preset"))
} }
/// Export a preset to JSON and re-import it (issue #11). /// Export a preset to JSON and re-import it (issue #11).
@@ -348,7 +358,7 @@ final class Milestone2UITests: XCTestCase {
let deleteButtons = buttonsMatching( let deleteButtons = buttonsMatching(
"identifier BEGINSWITH 'btnDeletePreset-'") "identifier BEGINSWITH 'btnDeletePreset-'")
deleteButtons.firstMatch.click() deleteButtons.firstMatch.click()
XCTAssertFalse(staticText("RoundTrip").waitForExistence(timeout: 3)) assertAbsent(staticText("RoundTrip"))
// Copy the export to the import path so the hook picks it up. // Copy the export to the import path so the hook picks it up.
try FileManager.default.copyItem(at: exportURL, to: importURL) try FileManager.default.copyItem(at: exportURL, to: importURL)
+31 -1
View File
@@ -198,7 +198,37 @@ final class Milestone3UITests: XCTestCase {
} }
XCTAssertTrue(app.buttons["btnPrintAll"].isEnabled) XCTAssertTrue(app.buttons["btnPrintAll"].isEnabled)
app.buttons["btnPrintPage-0"].click() // The gallery cell's Print button sits at the window's bottom
// edge where synthesized scroll-wheel events are inert on the
// LazyVGrid (#132). Drag the NSScrollView's vertical AXScrollBar
// thumb instead a real scroll that re-renders the cell onscreen.
var printPage = app.buttons["btnPrintPage-0"]
let scrollDeadline = Date().addingTimeInterval(15)
while !printPage.isHittable, Date() < scrollDeadline {
let scroller = app.scrollBars.allElementsBoundByIndex
.first { $0.frame.height > $0.frame.width }
if let scroller {
scroller.coordinate(withNormalizedOffset:
CGVector(dx: 0.5, dy: 0.1))
.press(forDuration: 0.1, thenDragTo:
scroller.coordinate(withNormalizedOffset:
CGVector(dx: 0.5, dy: 0.6)))
} else {
app.scrollViews["stage-2"].scroll(byDeltaX: 0, deltaY: -1)
}
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
printPage = app.buttons["btnPrintPage-0"]
}
if printPage.isHittable {
printPage.click()
} else {
// LazyVGrid cells can report a stale a11y frame click the
// point directly; the lp argv assert below still verifies.
print("AXTREE-BEGIN frame=\(printPage.frame)\n" +
"\(app.debugDescription)\nAXTREE-END")
printPage.coordinate(withNormalizedOffset:
CGVector(dx: 0.5, dy: 0.5)).click()
}
let argv = waitForLpLine() let argv = waitForLpLine()
XCTAssertTrue(argv.contains("AP_ColorMatchingMode"), argv) XCTAssertTrue(argv.contains("AP_ColorMatchingMode"), argv)
XCTAssertTrue(argv.contains("page1.tif"), argv) XCTAssertTrue(argv.contains("page1.tif"), argv)
+18 -15
View File
@@ -111,16 +111,9 @@ final class Milestone4UITests: XCTestCase {
} }
app.buttons["btnCalibrate"].click() app.buttons["btnCalibrate"].click()
// Trigger strip A. // Trigger each strip until all are read Done & Save appears.
_ = waitFor("btnTrigger", timeout: 20) driveStripsUntilDone()
app.buttons["btnTrigger"].click() XCTAssertTrue(element("btnDoneRead").exists)
// Trigger strip B.
_ = waitFor("btnTrigger", timeout: 20)
app.buttons["btnTrigger"].click()
// All strips read Done & Save appears.
_ = waitFor("btnDoneRead", timeout: 20)
app.buttons["btnDoneRead"].firstMatch.click() app.buttons["btnDoneRead"].firstMatch.click()
// Averaging panel appears with one pass snapshot. // Averaging panel appears with one pass snapshot.
@@ -186,11 +179,21 @@ final class Milestone4UITests: XCTestCase {
start.click() start.click()
_ = waitFor("btnCalibrate", timeout: 25) _ = waitFor("btnCalibrate", timeout: 25)
app.buttons["btnCalibrate"].click() app.buttons["btnCalibrate"].click()
_ = waitFor("btnTrigger", timeout: 20) driveStripsUntilDone()
app.buttons["btnTrigger"].click() XCTAssertTrue(element("btnDoneRead").exists)
_ = waitFor("btnTrigger", timeout: 20)
app.buttons["btnTrigger"].click()
_ = waitFor("btnDoneRead", timeout: 20)
app.buttons["btnDoneRead"].firstMatch.click() app.buttons["btnDoneRead"].firstMatch.click()
} }
/// Clicks Trigger for each remaining strip until `btnDoneRead`
/// appears the button is re-polled each pass so a click that
/// races a state transition isn't lost.
private func driveStripsUntilDone() {
let deadline = Date().addingTimeInterval(40)
while !element("btnDoneRead").exists, Date() < deadline {
if app.buttons["btnTrigger"].waitForExistence(timeout: 10) {
app.buttons["btnTrigger"].click()
}
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
}
}
} }
+2 -2
View File
@@ -66,7 +66,7 @@ Do not create issues until labels and milestones exist.
- Artefact gating on disk; atomic writes (`.tmp` + rename); user strings via SwiftUI `Text` only. - Artefact gating on disk; atomic writes (`.tmp` + rename); user strings via SwiftUI `Text` only.
- Branching: `develop``milestone/mN-<name>``feat/<issue#>-<slug>`; PRs via Gitea MCP. - Branching: `develop``milestone/mN-<name>``feat/<issue#>-<slug>`; PRs via Gitea MCP.
- Labels: every issue/PR has `Project/ICCery-v2` + one `Feature/*` or `Bug/*` + `Priority/*`. - Labels: every issue/PR has `Project/ICCery-v2` + one `Feature/*` or `Bug/*` + `Priority/*`.
- Verify: `xcodebuild test -scheme ICCery -destination 'platform=macOS' ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO`; sidecar `codesign -dvv`. - Verify: `xcodebuild test -scheme ICCery -destination 'platform=macOS' ARCHS="$(uname -m)"` (host arch; universal reserved for release packaging); sidecar `codesign -dvv`.
- Private ColorSync SPI: 2-arg `(PMPrintSession, CFStringRef) -> OSStatus`. Never pass integer `1`. - Private ColorSync SPI: 2-arg `(PMPrintSession, CFStringRef) -> OSStatus`. Never pass integer `1`.
### `BUILD-PLAN.md` ### `BUILD-PLAN.md`
@@ -566,7 +566,7 @@ Labels: `Feature/DevOps`, `Priority/High`
Milestone: M6 Milestone: M6
- **Self-hosted Mac runner** (Gitea has no `macos-latest` unless you attach one). Optional GitHub Actions mirror. - **Self-hosted Mac runner** (Gitea has no `macos-latest` unless you attach one). Optional GitHub Actions mirror.
- Pipeline: `fetch-argyll` → ad-hoc `codesign -s -` + `codesign -dvv` on every sidecar Mach-O (hard fail) → `xcodebuild build test -scheme ICCery ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO` → unit + mock fixtures (#215) → **dmgbuild** with background art (**not** Finder AppleScript, #189) → upload artefact. - Pipeline: `fetch-argyll` → ad-hoc `codesign -s -` + `codesign -dvv` on every sidecar Mach-O (hard fail) → `xcodebuild build test -scheme ICCery ARCHS="$(uname -m)"` (host arch; the dmgbuild leg still builds universal) → unit + mock fixtures (#215) → **dmgbuild** with background art (**not** Finder AppleScript, #189) → upload artefact.
- App signing: Developer ID + **notarize/staple** for the `.app` / `.dmg`. Sidecars remain **ad-hoc** inside the bundle (#165). These are two different gates — do not conflate. - App signing: Developer ID + **notarize/staple** for the `.app` / `.dmg`. Sidecars remain **ad-hoc** inside the bundle (#165). These are two different gates — do not conflate.
- Confirm entitlements: sandbox **false**. - Confirm entitlements: sandbox **false**.
- Spec: [04](04-argyll-binaries.md) §0.6, [05](05-argyll-fork.md) §89, [23](23-assets.md), [24](24-issues-invariants.md). - Spec: [04](04-argyll-binaries.md) §0.6, [05](05-argyll-fork.md) §89, [23](23-assets.md), [24](24-issues-invariants.md).
+1 -1
View File
@@ -17,7 +17,7 @@ Unsigned CI artefacts (e.g. a `.zip` from a non-notarized workflow run) are **no
scripts/fetch-argyll.sh # populates Vendor/Argyll and signs sidecars scripts/fetch-argyll.sh # populates Vendor/Argyll and signs sidecars
xcodegen generate --project . xcodegen generate --project .
xcodebuild test -scheme ICCery -destination 'platform=macOS' \ xcodebuild test -scheme ICCery -destination 'platform=macOS' \
ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO \ ARCHS="$(uname -m)" \
CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY='-' CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY='-'
``` ```
+10
View File
@@ -19,6 +19,7 @@ targets:
- path: Resources - path: Resources
excludes: excludes:
- ICCery.entitlements - ICCery.entitlements
- ICCery.Debug.entitlements
- Argyll - Argyll
- path: Resources/Argyll - path: Resources/Argyll
type: folder type: folder
@@ -62,6 +63,15 @@ targets:
OTHER_SWIFT_FLAGS: ["$(inherited)", "-strict-concurrency=minimal"] OTHER_SWIFT_FLAGS: ["$(inherited)", "-strict-concurrency=minimal"]
MACOSX_DEPLOYMENT_TARGET: "12.0" MACOSX_DEPLOYMENT_TARGET: "12.0"
ARCHS: "$(ARCHS_STANDARD)" ARCHS: "$(ARCHS_STANDARD)"
# Debug builds sign ad-hoc; hardened-runtime library validation would
# reject the embedded ICCeryCore package framework (no Team ID) when
# the test host launches (run 31992, #119). DISABLE_LIBRARY_VALIDATION
# does not inject the entitlement on Xcode 14.2, so use a dedicated
# Debug entitlements file. Release keeps validation and links the
# package statically anyway.
configs:
Debug:
CODE_SIGN_ENTITLEMENTS: Resources/ICCery.Debug.entitlements
ICCeryCoreTests: ICCeryCoreTests:
type: bundle.unit-test type: bundle.unit-test