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 18m4s
macOS CI / package (push) Successful in 3m9s
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
12 changed files with 167 additions and 42 deletions
+39 -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,14 +39,17 @@ 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='-'
@@ -124,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
@@ -140,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>
+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='-'
``` ```
+7 -4
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
@@ -63,12 +64,14 @@ targets:
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 # Debug builds sign ad-hoc; hardened-runtime library validation would
# reject the unsigned embedded ICCeryCore package framework when the # reject the embedded ICCeryCore package framework (no Team ID) when
# test host launches (run 31992, #119). Release keeps validation and # the test host launches (run 31992, #119). DISABLE_LIBRARY_VALIDATION
# links the package statically anyway. # 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: configs:
Debug: Debug:
DISABLE_LIBRARY_VALIDATION: YES CODE_SIGN_ENTITLEMENTS: Resources/ICCery.Debug.entitlements
ICCeryCoreTests: ICCeryCoreTests:
type: bundle.unit-test type: bundle.unit-test