Repository navigation
fix(apple): refuse captures whose window or panel identity is unknown #3761
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
thymikee
wants to merge
11
commits into
main
Choose a base branch
from
fix/adr-drift-apple-capture-identity
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
3fdd3ae
fix(macos): refuse an app screenshot whose front window is not shareable
thymikee dec7c9b
fix(apple): refuse a foldable capture when CoreDevice names no panel
thymikee fa3fe4f
fix(macos): skip invisible windows when selecting a screenshot
thymikee a77578f
fix(macos): clarify screenshot capture exclusion hint
thymikee f8e9688
docs(foldable): clarify primary panel capture ambiguity
thymikee dd5a35e
fix(macos): share front-window eligibility across capture and actions
thymikee c326caf
docs(macos): define the native front-window target
thymikee 0de4a1e
fix(macos): distinguish windows excluded from screen capture
thymikee bb69e6a
refactor(macos): centralize front window selection
thymikee 5e16f81
docs(capture): clarify alert exclusion and refusal remedy
thymikee 70acf88
fix(macos): distinguish unreadable window lists from missing windows
thymikee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/AppWindowScreenshotTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import CoreGraphics | ||
| import XCTest | ||
|
|
||
| @testable import AgentDeviceMacOSHelper | ||
|
|
||
| private struct Candidate: Equatable { | ||
| let id: Int | ||
| let area: Double | ||
| } | ||
|
|
||
| /// An app screenshot captures the front window or refuses; it never substitutes another window. | ||
| final class AppWindowScreenshotTests: XCTestCase { | ||
| func testCaptureIdentityAndScrollAndClickFallbackFrameSelectTheSameEligibleFrontWindow() throws { | ||
| let windows = [ | ||
| windowInfo(id: 1, pid: 99), | ||
| windowInfo(id: 2, layer: 1), | ||
| windowInfo(id: 3, onScreen: false), | ||
| windowInfo(id: 4, alpha: 0), | ||
| windowInfo(id: 5, width: 0), | ||
| windowInfo(id: 6, height: 0), | ||
| windowInfo(id: 7, alpha: 0.5, width: 20, height: 20), | ||
| windowInfo(id: 8), | ||
| ] | ||
|
|
||
| let frontID = try frontWindow(pid: 42, windowInfo: windows)?.number | ||
| let selected = frontCaptureWindow([7, 8], frontWindowID: frontID, sharingState: 1, windowID: { $0 }) | ||
|
|
||
| // Scroll and the click actionWindow fallback both consume frontWindowFrame. | ||
| XCTAssertEqual( | ||
| try frontWindowFrame(pid: 42, windowInfo: windows), | ||
| CGRect(x: 10, y: 10, width: 20, height: 20)) | ||
| XCTAssertEqual(frontID, 7) | ||
| XCTAssertEqual(try selected.get(), 7) | ||
| } | ||
|
|
||
| func testSkipsAppModalAlertInFrontOfNormalLayerWindow() throws { | ||
| let windows = [windowInfo(id: 8, layer: 8), windowInfo(id: 3)] | ||
| let front = try frontWindow(pid: 42, windowInfo: windows) | ||
| let selected = frontCaptureWindow( | ||
| [8, 3], frontWindowID: front?.number, sharingState: front?.sharingState, windowID: { $0 }) | ||
|
|
||
| XCTAssertEqual(front?.number, 3) | ||
| XCTAssertEqual(try selected.get(), 3) | ||
| XCTAssertEqual(try frontWindowFrame(pid: 42, windowInfo: windows), front?.bounds) | ||
| } | ||
|
|
||
| func testRefusesWhenOnlyInvisibleOrEmptyWindowsRemain() throws { | ||
| let windows = [ | ||
| windowInfo(id: 1, onScreen: false), | ||
| windowInfo(id: 2, alpha: 0), | ||
| windowInfo(id: 3, width: 0), | ||
| windowInfo(id: 4, height: 0), | ||
| ] | ||
| let frontID = try frontWindow(pid: 42, windowInfo: windows)?.number | ||
| let selected = frontCaptureWindow([5], frontWindowID: frontID, sharingState: 1, windowID: { $0 }) | ||
|
|
||
| XCTAssertNil(try frontWindowFrame(pid: 42, windowInfo: windows)) | ||
| XCTAssertNil(frontID) | ||
| XCTAssertThrowsError(try selected.get()) { error in | ||
| XCTAssertEqual(error as? AppWindowScreenshotRefusal, .windowNotFound) | ||
| } | ||
| } | ||
|
|
||
| func testUnreadableWindowListRefusesBeforeCaptureOrScrollCanReportNoWindow() { | ||
| XCTAssertThrowsError(try frontWindow(pid: 42, windowInfo: nil)) { error in | ||
| guard case HelperError.commandFailed(_, let details) = error else { | ||
| return XCTFail("expected a typed window-list refusal, got \(error)") | ||
| } | ||
| XCTAssertEqual(details["reason"], "window-list-unreadable") | ||
| } | ||
| XCTAssertThrowsError(try frontWindowFrame(pid: 42, windowInfo: nil)) { error in | ||
| guard case HelperError.commandFailed(_, let details) = error else { | ||
| return XCTFail("expected a typed window-list refusal, got \(error)") | ||
| } | ||
| XCTAssertEqual(details["reason"], "window-list-unreadable") | ||
| } | ||
| XCTAssertNil(try frontWindow(pid: 42, windowInfo: [])) | ||
| XCTAssertNil(try frontWindowFrame(pid: 42, windowInfo: [])) | ||
| } | ||
|
|
||
| func testSelectsTheFrontWindowEvenWhenAnotherIsLarger() { | ||
| let candidates = [Candidate(id: 7, area: 10_000), Candidate(id: 3, area: 100)] | ||
|
|
||
| let selected = frontCaptureWindow(candidates, frontWindowID: 3, sharingState: 1, windowID: \.id) | ||
|
|
||
| XCTAssertEqual(try selected.get(), Candidate(id: 3, area: 100)) | ||
| } | ||
|
|
||
| func testRefusesWhenTheFrontWindowIsNotShareable() { | ||
| let candidates = [Candidate(id: 7, area: 10_000), Candidate(id: 8, area: 100)] | ||
|
|
||
| let selected = frontCaptureWindow(candidates, frontWindowID: 3, sharingState: 1, windowID: \.id) | ||
|
|
||
| XCTAssertThrowsError(try selected.get()) { error in | ||
| XCTAssertEqual(error as? AppWindowScreenshotRefusal, .frontWindowNotShareable) | ||
| } | ||
| } | ||
|
|
||
| func testCaptureExclusionSurvivesWindowSelectionAndNeverSubstitutesAnotherWindow() throws { | ||
| let windows = [windowInfo(id: 3, sharingState: 0), windowInfo(id: 7)] | ||
| let front = try frontWindow(pid: 42, windowInfo: windows) | ||
| XCTAssertEqual(front?.number, 3) | ||
| XCTAssertEqual(front?.sharingState, 0) | ||
|
|
||
| for candidates in [[3, 7], [7]] { | ||
| let selected = frontCaptureWindow( | ||
| candidates, frontWindowID: front?.number, sharingState: front?.sharingState, windowID: { $0 }) | ||
| XCTAssertThrowsError(try selected.get()) { error in | ||
| XCTAssertEqual(error as? AppWindowScreenshotRefusal, .frontWindowCaptureExcluded) | ||
| } | ||
| } | ||
|
|
||
| var entry = windowInfo(id: 3) | ||
| entry.removeValue(forKey: kCGWindowSharingState as String) | ||
| let unknown = try frontWindow(pid: 42, windowInfo: [entry]) | ||
| let selected = frontCaptureWindow( | ||
| [3], frontWindowID: unknown?.number, sharingState: unknown?.sharingState, windowID: { $0 }) | ||
| XCTAssertEqual(try selected.get(), 3) | ||
| } | ||
|
|
||
| func testRefusesWhenTheAppHasNoOnScreenWindow() { | ||
| let selected = frontCaptureWindow( | ||
| [Candidate(id: 7, area: 10_000)], frontWindowID: nil, sharingState: nil, windowID: \.id) | ||
|
|
||
| XCTAssertThrowsError(try selected.get()) { error in | ||
| XCTAssertEqual(error as? AppWindowScreenshotRefusal, .windowNotFound) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func windowInfo( | ||
| id: Int, pid: Int32 = 42, layer: Int = 0, onScreen: Bool = true, | ||
| alpha: Double = 1, sharingState: Int = 1, width: Double = 100, height: Double = 100 | ||
| ) -> [String: Any] { | ||
| [ | ||
| kCGWindowNumber as String: id, | ||
| kCGWindowOwnerPID as String: pid, | ||
| kCGWindowLayer as String: layer, | ||
| kCGWindowIsOnscreen as String: onScreen, | ||
| kCGWindowAlpha as String: alpha, | ||
| kCGWindowSharingState as String: sharingState, | ||
| kCGWindowBounds as String: CGRect(x: 10, y: 10, width: width, height: height).dictionaryRepresentation, | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.