Fixes the no-GPU gamut sheet hang: GamutSceneView.makeNSView used to construct an SCNViewbefore the Metal check flipped viewerUnavailable asynchronously — too late on a Metal-less host, stalling the main thread and blocking app quit behind the open sheet.
Changes
GamutSceneAvailability.isAvailable (UITestHooks.skipSceneKit or MTLCreateSystemDefaultDevice() != nil) is evaluated in GamutViewModel.init, so viewerUnavailable is set before the body mounts GamutSceneView — no SCNView is ever constructed without Metal. The post-makeNSView async check remains only as a safety net.
btnCloseGamut — always-visible footer Close (visible on the fallback banner too), .keyboardShortcut(.cancelAction) so Escape dismisses without SceneKit. Same \.dismiss pattern as btnCloseSpotRead.
hasGPU = MTLCreateSystemDefaultDevice() != nil in the XCTest process; ICCERY_TEST_SKIP_SCENEKIT is not in any default launch env — SceneKit cases run once CI has a GPU with no further patch.
New: testCloseButtonDismissesSheet, testFallbackBannerMatchesGPUAvailability, testForcedSceneKitSkipShowsBannerAndClose (env forced for that one test only), testResetCameraInteractsWithScene (XCTSkip without Metal), testViewerUnavailableMirrorsMetalAvailability.
Every gamut UI test clicks btnCloseGamut before tearDown terminates the app (explicit call + defensive tearDown).
Also in this PR
test(print): scroll gallery until btnPrintPage is hittable (#132) — testPerPagePrint no longer drags app.scrollBars (overlay scrollbars are not in the AX tree) and no longer coordinate-clicks a stale frame (that point was the Dock's Calendar icon). It drags the galleryPage-0 cell content upward until btnPrintPage-0 is hittable above the window-bottom zone; XCTFail otherwise.
Fixes the no-GPU gamut sheet hang: `GamutSceneView.makeNSView` used to construct an `SCNView` **before** the Metal check flipped `viewerUnavailable` asynchronously — too late on a Metal-less host, stalling the main thread and blocking app quit behind the open sheet.
## Changes
- `GamutSceneAvailability.isAvailable` (`UITestHooks.skipSceneKit` or `MTLCreateSystemDefaultDevice() != nil`) is evaluated in `GamutViewModel.init`, so `viewerUnavailable` is set **before** the body mounts `GamutSceneView` — no `SCNView` is ever constructed without Metal. The post-`makeNSView` async check remains only as a safety net.
- `btnCloseGamut` — always-visible footer Close (visible on the fallback banner too), `.keyboardShortcut(.cancelAction)` so Escape dismisses without SceneKit. Same `\.dismiss` pattern as `btnCloseSpotRead`.
- Layer toggles stay `.disabled(layer == nil)` only (Phase 11). `applicationShouldTerminate` / `killAll` untouched.
- `docs/21-ui-reference.md` gains `btnCloseGamut`.
## Tests — runtime GPU branch
- `hasGPU = MTLCreateSystemDefaultDevice() != nil` in the XCTest process; `ICCERY_TEST_SKIP_SCENEKIT` is **not** in any default launch env — SceneKit cases run once CI has a GPU with no further patch.
- New: `testCloseButtonDismissesSheet`, `testFallbackBannerMatchesGPUAvailability`, `testForcedSceneKitSkipShowsBannerAndClose` (env forced for that one test only), `testResetCameraInteractsWithScene` (XCTSkip without Metal), `testViewerUnavailableMirrorsMetalAvailability`.
- Every gamut UI test clicks `btnCloseGamut` before `tearDown` terminates the app (explicit call + defensive tearDown).
## Also in this PR
`test(print): scroll gallery until btnPrintPage is hittable (#132)` — `testPerPagePrint` no longer drags `app.scrollBars` (overlay scrollbars are not in the AX tree) and no longer coordinate-clicks a stale frame (that point was the Dock's Calendar icon). It drags the `galleryPage-0` cell content upward until `btnPrintPage-0` is hittable above the window-bottom zone; `XCTFail` otherwise.
## Local gate (x86_64 host, Metal present)
- Targeted: `Milestone10GamutCompareUITests` 10/10, `Milestone6GamutUITests` 2/2 (SceneKit reset ran, not skipped), `GamutViewModelTests` 7/7, `AboutHelpUITests` 2/2 — 0 failures.
- `Milestone3UITests` 6/6 after the scroll fix; `testPerPagePrint` passes with a real hittable click, Calendar never launches.
Refs #147 #146
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes the no-GPU gamut sheet hang:
GamutSceneView.makeNSViewused to construct anSCNViewbefore the Metal check flippedviewerUnavailableasynchronously — too late on a Metal-less host, stalling the main thread and blocking app quit behind the open sheet.Changes
GamutSceneAvailability.isAvailable(UITestHooks.skipSceneKitorMTLCreateSystemDefaultDevice() != nil) is evaluated inGamutViewModel.init, soviewerUnavailableis set before the body mountsGamutSceneView— noSCNViewis ever constructed without Metal. The post-makeNSViewasync check remains only as a safety net.btnCloseGamut— always-visible footer Close (visible on the fallback banner too),.keyboardShortcut(.cancelAction)so Escape dismisses without SceneKit. Same\.dismisspattern asbtnCloseSpotRead..disabled(layer == nil)only (Phase 11).applicationShouldTerminate/killAlluntouched.docs/21-ui-reference.mdgainsbtnCloseGamut.Tests — runtime GPU branch
hasGPU = MTLCreateSystemDefaultDevice() != nilin the XCTest process;ICCERY_TEST_SKIP_SCENEKITis not in any default launch env — SceneKit cases run once CI has a GPU with no further patch.testCloseButtonDismissesSheet,testFallbackBannerMatchesGPUAvailability,testForcedSceneKitSkipShowsBannerAndClose(env forced for that one test only),testResetCameraInteractsWithScene(XCTSkip without Metal),testViewerUnavailableMirrorsMetalAvailability.btnCloseGamutbeforetearDownterminates the app (explicit call + defensive tearDown).Also in this PR
test(print): scroll gallery until btnPrintPage is hittable (#132)—testPerPagePrintno longer dragsapp.scrollBars(overlay scrollbars are not in the AX tree) and no longer coordinate-clicks a stale frame (that point was the Dock's Calendar icon). It drags thegalleryPage-0cell content upward untilbtnPrintPage-0is hittable above the window-bottom zone;XCTFailotherwise.Local gate (x86_64 host, Metal present)
Milestone10GamutCompareUITests10/10,Milestone6GamutUITests2/2 (SceneKit reset ran, not skipped),GamutViewModelTests7/7,AboutHelpUITests2/2 — 0 failures.Milestone3UITests6/6 after the scroll fix;testPerPagePrintpasses with a real hittable click, Calendar never launches.Refs #147 #146