-
-
Notifications
You must be signed in to change notification settings - Fork 314
feat(apple): add verified simulator screen lock #3001
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
csark0812
wants to merge
11
commits into
callstack:main
Choose a base branch
from
csark0812:chris/agent/screen-lock
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
8d881fc
feat(apple): add verified simulator screen lock
csark0812 768c193
fix(apple): import Darwin notifications for screen lock
csark0812 0518917
fix(apple): harden screen lock verification
csark0812 c91ac5c
fix(apple): recheck lock state while verifying surface
csark0812 756dc13
test(apple): account for lock state verification read
csark0812 bd45e6c
fix(apple): verify visible Lock Screen surface
csark0812 99e9c47
fix(apple): start lock verification after dispatch
csark0812 2d1b117
fix(apple): preserve screen lock verification and cleanup
csark0812 272f8dc
test: align screen-lock coverage evidence
csark0812 9950595
test(apple): clarify screen-lock simulator cleanup
csark0812 f740401
Merge remote-tracking branch 'upstream/main' into chris/agent/screen-…
csark0812 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
2 changes: 2 additions & 0 deletions
2
...ner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h
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
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
202 changes: 202 additions & 0 deletions
202
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenLock.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,202 @@ | ||
| import Darwin | ||
| import XCTest | ||
|
|
||
| extension RunnerTests { | ||
| private static let screenLockStateNotification = "com.apple.springboard.lockstate" | ||
| static let screenLockVerificationTimeout: TimeInterval = 5 | ||
| static let screenLockPollInterval: TimeInterval = 0.05 | ||
|
|
||
| /// Mirrors WebDriverAgent's audited simulator route: XCTest dispatches the private lock-button | ||
| /// primitive and SpringBoard's Darwin notification state independently verifies the transition. | ||
| /// The pre-read makes the operation idempotent; a dispatch alone is never reported as success. | ||
| func executeScreenLockCommand() -> Response { | ||
| #if os(iOS) && targetEnvironment(simulator) | ||
| var deadline = Date().addingTimeInterval(Self.screenLockVerificationTimeout) | ||
| return executeScreenLockTransition( | ||
| readState: currentScreenLockState, | ||
| dispatch: dispatchScreenLock, | ||
| verifyVisibleSurface: verifyLockScreenSurface, | ||
| shouldContinue: { Date() < deadline }, | ||
| wait: { | ||
| RunLoop.current.run( | ||
| until: Date().addingTimeInterval(Self.screenLockPollInterval) | ||
| ) | ||
| }, | ||
| startVerificationWindow: { | ||
| deadline = Date().addingTimeInterval(Self.screenLockVerificationTimeout) | ||
| } | ||
| ) | ||
| #else | ||
| return Response( | ||
| ok: false, | ||
| error: ErrorPayload( | ||
| code: "UNSUPPORTED_OPERATION", | ||
| message: "screenLock is supported only on iPhone and iPad Simulators" | ||
| ) | ||
| ) | ||
| #endif | ||
| } | ||
|
|
||
| #if os(iOS) && targetEnvironment(simulator) | ||
| enum ScreenLockStateRead { | ||
| case success(Bool) | ||
| case failure(Response) | ||
| } | ||
|
|
||
| func executeScreenLockTransition( | ||
| readState: () -> ScreenLockStateRead, | ||
| dispatch: () -> Response?, | ||
| verifyVisibleSurface: () -> Bool, | ||
| shouldContinue: () -> Bool, | ||
| wait: () -> Void, | ||
| startVerificationWindow: () -> Void = {} | ||
| ) -> Response { | ||
| switch readState() { | ||
| case .success(true): | ||
| return screenLockVisibleResponse( | ||
| readState: readState, | ||
| verifyVisibleSurface: verifyVisibleSurface, | ||
| shouldContinue: shouldContinue, | ||
| wait: wait | ||
| ) | ||
| case .failure(let response): | ||
| return response | ||
| case .success(false): | ||
| break | ||
| } | ||
|
|
||
| if let dispatchFailure = dispatch() { return dispatchFailure } | ||
| startVerificationWindow() | ||
|
|
||
| while shouldContinue() { | ||
| switch readState() { | ||
| case .success(true): | ||
| return screenLockVisibleResponse( | ||
| readState: readState, | ||
| verifyVisibleSurface: verifyVisibleSurface, | ||
| shouldContinue: shouldContinue, | ||
| wait: wait | ||
| ) | ||
| case .failure(let response): | ||
| return response | ||
| case .success(false): | ||
| wait() | ||
| } | ||
| } | ||
|
|
||
| // The lock transition may complete during the last wait even when that wait crosses the | ||
| // deadline. Read once more before reporting timeout so a completed transition is not lost. | ||
| switch readState() { | ||
| case .success(true): | ||
| return screenLockVisibleResponse( | ||
| readState: readState, | ||
| verifyVisibleSurface: verifyVisibleSurface, | ||
| shouldContinue: shouldContinue, | ||
| wait: wait | ||
| ) | ||
| case .failure(let response): | ||
| return response | ||
| case .success(false): | ||
| break | ||
| } | ||
|
|
||
| return Response( | ||
| ok: false, | ||
| error: ErrorPayload( | ||
| code: "COMMAND_FAILED", | ||
| message: "XCTest dispatched the simulator lock button, but SpringBoard did not report a locked screen", | ||
| hint: "Verify that the selected Simulator is booted and that its SpringBoard lock-state service is available." | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| private func screenLockVisibleResponse( | ||
| readState: () -> ScreenLockStateRead, | ||
| verifyVisibleSurface: () -> Bool, | ||
| shouldContinue: () -> Bool, | ||
| wait: () -> Void | ||
| ) -> Response { | ||
| while true { | ||
| switch readState() { | ||
| case .success(false): | ||
| return Response( | ||
| ok: false, | ||
| error: ErrorPayload( | ||
| code: "COMMAND_FAILED", | ||
| message: "SpringBoard no longer reports a locked screen while verifying the Lock Screen surface" | ||
| ) | ||
| ) | ||
| case .failure(let response): | ||
| return response | ||
| case .success(true): | ||
| break | ||
| } | ||
| if verifyVisibleSurface() { | ||
| return Response(ok: true, data: DataPayload(message: "Screen locked", state: "locked")) | ||
|
csark0812 marked this conversation as resolved.
|
||
| } | ||
| guard shouldContinue() else { break } | ||
| wait() | ||
| } | ||
| return Response( | ||
| ok: false, | ||
| error: ErrorPayload( | ||
| code: "COMMAND_FAILED", | ||
| message: "SpringBoard reported a locked state, but the Lock Screen surface was not visible" | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| private func dispatchScreenLock() -> Response? { | ||
| let device = XCUIDevice.shared | ||
| let selector = NSSelectorFromString("pressLockButton") | ||
| guard device.responds(to: selector) else { | ||
| return Response( | ||
| ok: false, | ||
| error: ErrorPayload( | ||
| code: "UNSUPPORTED_OPERATION", | ||
| message: "The selected XCTest runtime does not expose simulator screen locking" | ||
| ) | ||
| ) | ||
| } | ||
| device.perform(selector) | ||
| return nil | ||
| } | ||
|
|
||
| private func verifyLockScreenSurface() -> Bool { | ||
| let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") | ||
| let dateView = springboard.descendants(matching: .any) | ||
| .matching(identifier: "lockscreen-date-view") | ||
|
csark0812 marked this conversation as resolved.
|
||
| .firstMatch | ||
| let coverSheet = springboard.windows.matching(identifier: "SBCoverSheetWindow").firstMatch | ||
| return springboard.exists | ||
| && ((dateView.exists && !dateView.frame.isEmpty) | ||
| || (coverSheet.exists && !coverSheet.frame.isEmpty)) | ||
| } | ||
|
|
||
| func currentScreenLockState() -> ScreenLockStateRead { | ||
| var token: Int32 = 0 | ||
| let registerStatus = notify_register_check(Self.screenLockStateNotification, &token) | ||
| guard registerStatus == NOTIFY_STATUS_OK else { | ||
| return .failure(screenLockStateReadFailure("register", status: registerStatus)) | ||
| } | ||
| defer { notify_cancel(token) } | ||
|
|
||
| var state: UInt64 = 0 | ||
| let readStatus = notify_get_state(token, &state) | ||
| guard readStatus == NOTIFY_STATUS_OK else { | ||
| return .failure(screenLockStateReadFailure("read", status: readStatus)) | ||
| } | ||
| return .success(state != 0) | ||
| } | ||
|
|
||
| private func screenLockStateReadFailure(_ phase: String, status: UInt32) -> Response { | ||
| Response( | ||
| ok: false, | ||
| error: ErrorPayload( | ||
| code: "COMMAND_FAILED", | ||
| message: "Unable to \(phase) SpringBoard lock state (notify status \(status))" | ||
| ) | ||
| ) | ||
| } | ||
| #endif | ||
| } | ||
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.
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.