Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ typedef NS_ENUM(NSInteger, RunnerTapSynthesisStatus) {
@interface RunnerSynthesizedGesture : NSObject

// `resolvedWindow` is the app window the caller already resolved for the gesture's reference
// frame. The record's display ID is read from that same window so geometry and routing can never
// name different windows; pass nil to fall back to the application's first window.
// frame, so the record's display ID is read from that same window. Pass nil for the sole-screen
// route: the app's tree could not name its window and the screen is proven instead.
+ (NSString * _Nullable)synthesizeSwipeWithApplication:(id)application
resolvedWindow:(id _Nullable)resolvedWindow
x:(double)x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ typedef id (*RunnerDragPointerPathFactory)(
CGPoint,
double
);
typedef double (*RunnerDragProgressFunction)(double);

static NSString * _Nullable RunnerResolveGestureEventBridge(
id application,
Expand Down Expand Up @@ -58,14 +57,6 @@ static id RunnerControlledScrollPointerPath(
CGPoint end,
double durationMs
);
static id RunnerSampledDragPointerPath(
const RunnerGestureEventBridge *bridge,
CGPoint start,
CGPoint end,
double durationMs,
RunnerDragProgressFunction progress,
NSInteger frameCount
);
static NSString * _Nullable RunnerTrySynthesizeDrag(
id application,
id _Nullable resolvedWindow,
Expand Down Expand Up @@ -332,8 +323,10 @@ static RunnerTapSynthesisStatus RunnerTrySynthesizeTap(

NSUInteger displayID = 0;
// A nil window is the caller's sole-screen route: the app tree could not name its window.
// Re-asking here, at dispatch, is what keeps the record's display the one the app fills now.
id soleScreen = nil;
NSString *displayError = resolvedWindow == nil
? RunnerResolveSoleScreenDisplayID(application, &displayID)
? RunnerResolveSoleScreen(application, &soleScreen, &displayID)
: RunnerResolveWindowDisplayID(resolvedWindow, &displayID);
if (displayError != nil) return displayError;
NSInteger interfaceOrientation =
Expand Down Expand Up @@ -418,34 +411,17 @@ static id RunnerControlledScrollPointerPath(
// A stationary hold is not a reliable zero-velocity release in XCTest. Cubic ease-out decreases
// velocity from the first movement and reaches near zero before lift. Profiles that accelerate
// after pan recognition can still be classified as flings despite a slow final sample.
return RunnerSampledDragPointerPath(
bridge,
start,
end,
durationMs,
RunnerControlledScrollProgress,
RunnerControlledScrollFrameCount(durationMs)
);
}

static id RunnerSampledDragPointerPath(
const RunnerGestureEventBridge *bridge,
CGPoint start,
CGPoint end,
double durationMs,
RunnerDragProgressFunction progress,
NSInteger frameCount
) {
id path =
((RunnerMsgSendInitPath)objc_msgSend)([bridge->core.pathClass alloc], bridge->initPathSelector, start, 0.0);
if (path == nil) {
return nil;
}

NSInteger frameCount = RunnerControlledScrollFrameCount(durationMs);
NSTimeInterval durationSeconds = durationMs / 1000.0;
for (NSInteger index = 1; index <= frameCount; index += 1) {
double t = (double)index / (double)frameCount;
double easedT = progress(t);
double easedT = RunnerControlledScrollProgress(t);
CGPoint point = CGPointMake(
start.x + (end.x - start.x) * easedT,
start.y + (end.y - start.y) * easedT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,10 @@

NS_ASSUME_NONNULL_BEGIN

// Shared reflection surface for XCTest's private event-synthesis API
// (XCSynthesizedEventRecord / XCPointerEventPath). Both gesture synthesis
// (RunnerSynthesizedGesture) and text-entry synthesis (RunnerSynthesizedTextEntry)
// reflect into this same private API; resolving the part they have in common here
// means a selector rename only has to be found in one place.
//
// Each caller additionally resolves whichever XCSynthesizedEventRecord init
// overload and XCPointerEventPath factory/mutator selectors its own event kind
// needs: gesture uses `initWithName:displayID:interfaceOrientation:` plus
// touch-path selectors (`initForTouchAtPoint:offset:`, `moveToPoint:atOffset:`,
// `liftUpAtOffset:`); text entry uses the 1-arg `initWithName:` plus text-input
// selectors (`initForTextInput`, `typeText:atOffset:typingSpeed:shouldRedact:`,
// `typeKey:modifiers:atOffset:`). Those genuinely differ and are resolved by
// each caller on top of this shared core, not forced into one shape here.
// Shared reflection core for XCTest's private event-synthesis API (XCSynthesizedEventRecord /
// XCPointerEventPath). Gesture and text-entry synthesis each resolve on top of this core the
// record-init overload and mutator selectors their own event kind needs, so a rename in the part
// they share is found in one place.
typedef struct {
Class recordClass; // XCSynthesizedEventRecord
Class pathClass; // XCPointerEventPath
Expand All @@ -32,7 +22,7 @@ typedef void (*RunnerMsgSendAddPath)(id, SEL, id);
typedef BOOL (*RunnerMsgSendSynthesize)(id, SEL, NSError **);

// Why the display hosting the target app could not be named. Callers branch on this rather than
// on the message, so a refusal keeps its reason across the language boundary (#2728).
// on the message, so a refusal keeps its reason across the language boundary.
typedef NS_ENUM(NSUInteger, RunnerApplicationScreenFailure) {
RunnerApplicationScreenFailureNone,
// No window with a usable frame, so nothing resolves and its screen would only report main.
Expand All @@ -41,56 +31,40 @@ typedef NS_ENUM(NSUInteger, RunnerApplicationScreenFailure) {
RunnerApplicationScreenFailureUnresolvedScreen,
};

// Resolves the shared core described above against the live XCTest runtime.
// Returns nil and populates `bridge` on success. Returns a non-nil
// "private XCTest <surface> synthesis unavailable: ..." message and leaves
// `bridge` unspecified when a class or selector the private API is expected to
// expose is missing (the API changed, or is unavailable on this OS/Xcode).
// `surface` names the calling feature ("event" or "text") so the message text
// matches what that caller has always reported.
// Resolves the shared core against the live XCTest runtime. Returns nil on success, or a
// "private XCTest <surface> synthesis unavailable: ..." message when a class or selector the
// private API is expected to expose is missing. `surface` names the calling feature.
FOUNDATION_EXPORT NSString * _Nullable RunnerResolveXCTestEventBridge(
id application,
NSString *surface,
RunnerXCTestEventBridge *bridge
);

// Resolves the `XCUIScreen` hosting the application's window and that screen's `displayID`, which
// is the only display identity that follows a foldable app to whichever panel it currently lights.
// Reading the window frame must precede reading its screen: an unresolved element still reports the
// main screen. Never selects a screen by number or by position in `XCUIScreen.screens`.
// Returns YES and writes the screen to capture into `screen` and its identity into `displayID`.
// Otherwise returns NO, leaves both empty, and names the reason in `failure` so callers branch on
// the reason rather than on message text (#2728). Private KVC raises rather than returns nil when
// XCTest has no such value, so both reads are guarded here and surface as a typed failure instead
// of an exception reaching the caller.
// Resolves the `XCUIScreen` hosting the application's window and its `displayID`, the only display
// identity that follows an app to the panel it lights. Reading the window frame must precede
// reading its screen: an unresolved element still reports the main screen, and neither read is
// ever selected by number or position. Private KVC raises rather than returns nil, so both reads
// are guarded here and any failure surfaces as the typed `failure`, never an exception.
FOUNDATION_EXPORT BOOL RunnerResolveApplicationScreen(
id application,
id _Nullable *screen,
NSUInteger *displayID,
RunnerApplicationScreenFailure *failure
);

// Reads the display ID off an already-resolved window. When a caller has resolved the app window
// for geometry, routing the synthesized gesture by this window keeps the record's display and the
// booked reference frame on the same window instead of re-walking `windows.firstMatch`.
// Reads the display ID off an already-resolved window, keeping a synthesized record's display and
// the caller's booked reference frame on the same window. The frame is still read before the
// screen; a window that answers empty or raises fails closed with a refusal message naming the
// same reason the application walk gives.
FOUNDATION_EXPORT NSString * _Nullable RunnerResolveWindowDisplayID(
id window,
NSUInteger *displayID
);

// Reads the display ID of the device's only screen, for a gesture whose app window XCTest cannot
// resolve because the app's accessibility tree is too deep to serialize. On an iPhone with exactly
// one screen that screen hosts the full-screen app, so no window is needed to name it. On an iPad
// the same route opens only while the app provably fills that screen, decided in-process by the
// evidence seam below without the app's tree. Anywhere else this returns a message and leaves
// `displayID` untouched: a windowed iPad app sits in part of its screen, and only a window can say
// which panel lights a foldable's app.
FOUNDATION_EXPORT NSString * _Nullable RunnerResolveSoleScreenDisplayID(
id application,
NSUInteger *displayID
);

// Resolves the screen and display identity under the same sole-screen rule used by synthesis.
// Resolves the device's only screen and its `displayID` under the sole-screen rule that synthesis
// and capture share: an iPhone with one screen always, an iPad only while the AXRuntime probe
// proves the app fills that screen. Returns nil on success; on refusal it returns a message,
// empties `screen`, and leaves `displayID` untouched.
FOUNDATION_EXPORT NSString * _Nullable RunnerResolveSoleScreen(
id application,
id _Nullable *screen,
Expand All @@ -111,12 +85,9 @@ typedef NS_ENUM(NSUInteger, RunnerFullScreenEvidence) {
RunnerFullScreenEvidenceUnknown,
};

// Decides the evidence from the values the AXRuntime probe reads. AXRuntime reports the primary
// app's window and the system app, which spans the screen, in one native space; this is the same
// comparison the Simulator snapshot bridge refuses a capture on (`app-window-not-full-screen`,
// ADR 0004). Neither read walks the app's tree, so the decision survives a tree XCTest cannot
// serialize. A pid that cannot name a process (<= 0) or an unreadable frame answers Unknown, never
// a guess.
// Decides the evidence from the values the AXRuntime probe reads, which AXRuntime reports in one
// native space (ADR 0004). Neither read walks the app's tree, so the decision survives a tree
// XCTest cannot serialize. An unreadable pid or frame answers Unknown, never a guess.
FOUNDATION_EXPORT RunnerFullScreenEvidence RunnerFullScreenEvidenceFromForeground(
pid_t targetProcessID,
pid_t primaryProcessID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,10 @@
}
}

// One window read. XCTest answers with a value or raises. The window object is handed back because
// resolving this instance is what its `screen` will answer from — re-querying `firstMatch` would
// return an unresolved element that still names the main screen.
//
// Both answers are required. An app with no window answers `frame` with `(0,0 0x0)` without raising,
// which geometry alone would read as a window the runtime has not measured, and only the query
// itself says whether it has one. Measured on a live Duo: the runner host process, which has no
// window, answers `exists` NO and `frame` `(0,0 0x0)` (#2728).
//
// The frame the read produced is reported even when it is refused: a capture that fails closed has
// to say what it looked at, or the reason code cannot be told apart from a stale device.
// One window read. Both answers are required: an app with no window answers `frame` with
// `(0,0 0x0)` without raising, so only `exists` says whether the window is there. The window
// object is handed back because resolving this instance is what its `screen` will answer from.
// The frame is reported even when refused, so a refusal names what it looked at.
static RunnerApplicationScreenFailure RunnerResolveUsableWindow(
id application,
id _Nullable *window,
Expand Down Expand Up @@ -158,9 +151,8 @@ static RunnerApplicationScreenFailure RunnerResolveUsableWindow(
return RunnerApplicationScreenFailureNone;
}

// The screen read that must follow window resolution on that same window instance. Before the
// frame above is asked for, the window's `screen` can still name main, so this is never merged
// into the read ahead of it.
// The screen read that must follow window resolution on that same window instance: before the frame
// is asked for, `screen` can still name main, so this never merges into the read ahead of it.
static RunnerApplicationScreenFailure RunnerReadResolvedWindowScreen(
id window,
CGRect windowFrame,
Expand Down Expand Up @@ -212,31 +204,18 @@ BOOL RunnerResolveApplicationScreen(
return *failure == RunnerApplicationScreenFailureNone;
}

// Reads the display ID off a window the caller already resolved, so a synthesized record can be
// routed by the same window its reference frame was measured on. The window still resolves here:
// its frame is read before its screen, and a window that answers empty or raises fails closed with
// the same reasons the application walk gives.
NSString * _Nullable RunnerResolveSoleScreenDisplayID(id application, NSUInteger *displayID) {
id screen = nil;
return RunnerResolveSoleScreen(application, &screen, displayID);
}

RunnerFullScreenEvidence RunnerFullScreenEvidenceFromForeground(
pid_t targetProcessID,
pid_t primaryProcessID,
CGRect appFrame,
CGRect screenFrame
) {
// A pid that could not be read (or cannot own a process) proves nothing about ownership, so the
// answer is Unknown, never a guess that the app is not the primary app.
if (targetProcessID <= 0 || primaryProcessID <= 0) {
return RunnerFullScreenEvidenceUnknown;
}
if (!AgentDeviceForegroundOwnershipMatches(primaryProcessID, targetProcessID)) {
if (primaryProcessID != targetProcessID) {
return RunnerFullScreenEvidenceNotPrimaryApp;
}
// A frame that is not a usable measurement is a runtime that said nothing, not a window that
// differs, so the answer is Unknown, never the Windowed a comparison of the numbers would read as.
if (!AgentDeviceForegroundFramesAreMeasurable(appFrame, screenFrame)) {
return RunnerFullScreenEvidenceUnknown;
}
Expand All @@ -255,9 +234,8 @@ RunnerFullScreenEvidence RunnerFullScreenEvidenceFromForeground(
@"/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework/XCTAutomationSupport";

static Class<AgentDeviceForegroundElement> _Nullable RunnerForegroundElementClass(void) {
// Cache the answer including its absence: a runtime without the class would otherwise pay two
// RTLD_NOW lookups per window-less gesture forever. Code availability is the only thing cached
// here — the probe re-measures every attempt, because a resize changes the answer.
// Cache the answer including its absence; code availability is all that is cached — the probe
// re-measures every attempt because a resize changes the answer.
static BOOL resolved = NO;
static Class<AgentDeviceForegroundElement> _Nullable cached = Nil;
if (!resolved) {
Expand All @@ -273,11 +251,9 @@ RunnerFullScreenEvidence RunnerFullScreenEvidenceFromForeground(
return cached;
}

// One contained probe: it checks each selector before sending it and reads the target pid, the
// primary app's frame, and the system app's frame inside one exception scope, so any failed read
// answers Unknown — the same typed nothing-a-guess the decision seam already reports. The target
// pid comes from the existing runner owner for that read (RunnerAXSnapshotBridge), which picks the
// correctly sized send for the pid_t return through the method signature.
// One contained probe: it checks each selector before sending it and reads the target pid and both
// frames inside one exception scope, so any failed read answers Unknown. The target pid comes from
// the existing runner owner for that read (RunnerAXSnapshotBridge).
static RunnerFullScreenEvidence RunnerProbeApplicationFillsScreen(id application) {
if (application == nil) {
return RunnerFullScreenEvidenceUnknown;
Expand Down Expand Up @@ -346,9 +322,7 @@ static RunnerFullScreenEvidence RunnerProbeApplicationFillsScreen(id application
return @"private XCTest event synthesis unavailable: no resolved application window and no sole screen to route to";
}
// An iPhone keeps every app full-screen, so its idiom answers the rule and the route never pays
// for the probe. Off the phone idiom the route opens only on the fills-screen proof — an iPad can
// show an app in part of its only screen — and windowed or unknown evidence keeps requiring a
// window. The probe answers only what it proved or failed to prove, never "not probed".
// for the probe. Off the phone idiom the route opens only on the fills-screen proof.
if (UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPhone &&
RunnerProbeApplicationFillsScreen(application) != RunnerFullScreenEvidenceFillsScreen) {
return @"private XCTest event synthesis unavailable: no resolved application window, and the sole screen is not provably the app's full-screen frame";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,11 @@ extension RunnerTests {
return
}

var screen: AnyObject?
var displayID: UInt = 0
let refusal = RunnerResolveSoleScreenDisplayID(springboard, &displayID)
let refusal = RunnerResolveSoleScreen(springboard, &screen, &displayID)
XCTAssertNotNil(refusal, "an iPad app that does not own the screen names no sole screen for its taps")
XCTAssertNil(screen)
XCTAssertEqual(displayID, 0)
}

Expand Down
20 changes: 4 additions & 16 deletions apple/snapshot-bridge/ForegroundApplicationGeometry.h
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
// ForegroundApplicationGeometry.h — the one full-screen rule two native targets ask.
//
// The Simulator snapshot bridge refuses a capture whose app window does not fill the screen
// (`app-window-not-full-screen`, #3844), and the iOS runner refuses to route a window-less
// gesture onto a screen it cannot prove the app fills. Both include this header, so the rule is
// one compilation, not a pinned copy. Each caller keeps its own unknown policy: the bridge keeps
// a capture it cannot measure, the runner refuses authorization it cannot prove.
//
// Both consumers include this file by bare name: the snapshot bridge compiles it from its own
// directory, the runner's UITests target finds it through HEADER_SEARCH_PATHS.
// ForegroundApplicationGeometry.h — the one full-screen rule the Simulator snapshot bridge and
// the iOS runner both include, by bare name, so the rule is one compilation and not a pinned
// copy. Each keeps its own unknown policy: the bridge keeps a capture it cannot measure, the
// runner refuses to authorize a route it cannot prove.

#import <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
Expand All @@ -22,12 +16,6 @@ NS_ASSUME_NONNULL_BEGIN
- (CGRect)frame;
@end

// While a system surface owns the foreground, no screen is the target app's to fill and no tree is
// the target app's to capture (ADR 0004, foreground ownership).
static inline BOOL AgentDeviceForegroundOwnershipMatches(pid_t primaryProcessID, pid_t targetProcessID) {
return primaryProcessID == targetProcessID;
}

// Sub-pixel layout rounding is not a window; anything past this is one that shows the app in part
// of the screen (Windowed Apps or Stage Manager).
#define AGENT_DEVICE_FULL_SCREEN_FRAME_TOLERANCE 0.5
Expand Down
Loading
Loading