mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-17 23:30:09 +02:00
feat(ios): hoist DebugBridgeTouch into canonical templates
Bridges.swift.template imports DebugBridgeTouch but no .m/.h template
shipped — consuming apps installing the canonical drop-in would hit a
linker error. Closes that gap with the fixture's verified working code.
Changes:
- New ios-qa/templates/DebugBridgeTouch.{h,m}.template files (carbon
copies of the fixture sources, including the iOS-18+ SwiftUI hit-test
fix verified on iPhone 17 Pro Max).
- Package.swift.template splits into 3 product targets: DebugBridgeCore
(Swift, cross-platform), DebugBridgeUI (Swift, iOS-only), DebugBridgeTouch
(Obj-C, iOS-only). Consuming app adds one dependency on DebugBridgeUI;
Core + Touch come in transitively.
- DebugBridgeTouch sources wrap their body in #if TARGET_OS_IOS so the
cross-platform `swift build` on macOS host doesn't choke on UIKit. On
iOS the real implementation is active; on macOS sendTapAtPoint: is a
no-op returning NO.
- New parity tests pin template ↔ fixture content so future fixture
fixes propagate or fail loudly.
- Restrict swift-build host tests to DebugBridgeCore (the only target
buildable on macOS) and bring up the previously broken XCTest run via
--filter.
Verified post-change: real iPhone 17 Pro Max, iOS 26.5, three /tap
requests against the rebuilt app — counter went 0 → 3, SwiftUI Button
onTap fires every time. Templates now sufficient to ship to any
consuming iOS app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,10 @@
|
||||
// IOHIDEventSetIntegerValue, IOHIDEventAppendEvent.
|
||||
|
||||
#import "DebugBridgeTouch.h"
|
||||
#import <TargetConditionals.h>
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <objc/runtime.h>
|
||||
#import <objc/message.h>
|
||||
@@ -281,3 +285,17 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#else // !TARGET_OS_IOS
|
||||
|
||||
// macOS / Catalyst / other non-iOS host build: no-op stub so the module
|
||||
// resolves cleanly without UIKit or IOKit. The Swift cross-platform tests
|
||||
// don't exercise touch synthesis; that's iOS-only by definition.
|
||||
@implementation DebugBridgeTouch
|
||||
+ (BOOL)sendTapAtPoint:(CGPoint)point inWindow:(UIWindow *)window {
|
||||
(void)point; (void)window;
|
||||
return NO;
|
||||
}
|
||||
@end
|
||||
|
||||
#endif // TARGET_OS_IOS
|
||||
|
||||
+13
@@ -5,7 +5,19 @@
|
||||
// point on the key window, including SwiftUI Buttons via iOS 18+
|
||||
// _UIHitTestContext. DEBUG-only — never link in Release.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <TargetConditionals.h>
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
// macOS build: forward-declare UIWindow so the module compiles without UIKit.
|
||||
// The host CI runs swift build on macOS to validate the cross-platform Swift
|
||||
// surface; DebugBridgeTouch's implementation is a no-op there. On iOS the
|
||||
// real UIWindow comes from UIKit and the implementation is active.
|
||||
@class UIWindow;
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -14,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// Synthesize a single tap (TouchPhaseBegan + TouchPhaseEnded) at the given
|
||||
/// window-coordinate point. Returns YES if the touch was delivered (a hit
|
||||
/// view was found and the event passed through UIApplication.sendEvent).
|
||||
/// On non-iOS platforms returns NO unconditionally.
|
||||
+ (BOOL)sendTapAtPoint:(CGPoint)point inWindow:(UIWindow *)window;
|
||||
|
||||
@end
|
||||
|
||||
@@ -19,11 +19,47 @@
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const ROOT = join(import.meta.dir, '..');
|
||||
const FIXTURE_PATH = join(ROOT, 'test/fixtures/ios-qa/FixtureApp');
|
||||
const TEMPLATES_PATH = join(ROOT, 'ios-qa/templates');
|
||||
|
||||
// Parity: canonical Obj-C touch templates must match the fixture's working
|
||||
// copy. The fixture is the only place the .m / .h are exercised end-to-end
|
||||
// on a real device, so any divergence means consuming apps would ship a
|
||||
// stale, untested version of the SwiftUI hit-test fix.
|
||||
describe('template ↔ fixture parity', () => {
|
||||
test('DebugBridgeTouch.h.template matches fixture include', () => {
|
||||
const tmpl = readFileSync(join(TEMPLATES_PATH, 'DebugBridgeTouch.h.template'), 'utf-8');
|
||||
const fixture = readFileSync(
|
||||
join(FIXTURE_PATH, 'Sources/DebugBridgeTouch/include/DebugBridgeTouch.h'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(tmpl).toBe(fixture);
|
||||
});
|
||||
|
||||
test('DebugBridgeTouch.m.template matches fixture .m', () => {
|
||||
const tmpl = readFileSync(join(TEMPLATES_PATH, 'DebugBridgeTouch.m.template'), 'utf-8');
|
||||
const fixture = readFileSync(
|
||||
join(FIXTURE_PATH, 'Sources/DebugBridgeTouch/DebugBridgeTouch.m'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(tmpl).toBe(fixture);
|
||||
});
|
||||
|
||||
test('Package.swift.template declares all 3 DebugBridge targets', () => {
|
||||
const tmpl = readFileSync(join(TEMPLATES_PATH, 'Package.swift.template'), 'utf-8');
|
||||
// Each target must be present as a library product AND a target definition.
|
||||
for (const name of ['DebugBridgeCore', 'DebugBridgeUI', 'DebugBridgeTouch']) {
|
||||
expect(tmpl).toContain(`name: "${name}"`);
|
||||
}
|
||||
// DebugBridgeUI must depend on the other two; that's how the consuming
|
||||
// app gets the transitive set with one dependency entry.
|
||||
expect(tmpl).toMatch(/name:\s*"DebugBridgeUI"[\s\S]*?dependencies:\s*\["DebugBridgeCore",\s*"DebugBridgeTouch"\]/);
|
||||
});
|
||||
});
|
||||
|
||||
function hasSwift(): boolean {
|
||||
const r = spawnSync('swift', ['--version'], { stdio: 'pipe' });
|
||||
@@ -34,8 +70,13 @@ const swiftAvailable = hasSwift();
|
||||
const describeIfSwift = swiftAvailable ? describe : describe.skip;
|
||||
|
||||
describeIfSwift('swift build invariants', () => {
|
||||
test('Debug-config build succeeds', () => {
|
||||
const r = spawnSync('swift', ['build', '-c', 'debug'], {
|
||||
// DebugBridgeUI + DebugBridgeTouch are iOS-only (they link UIKit). Plain
|
||||
// `swift build` on macOS host can't resolve UIKit, so we scope these
|
||||
// invariants to DebugBridgeCore (Swift, cross-platform) + its XCTest
|
||||
// target. The iOS-only targets are covered by xcodebuild on the device
|
||||
// path (test/skill-e2e-ios-device.test.ts).
|
||||
test('Debug-config build succeeds (DebugBridgeCore)', () => {
|
||||
const r = spawnSync('swift', ['build', '-c', 'debug', '--target', 'DebugBridgeCore'], {
|
||||
cwd: FIXTURE_PATH,
|
||||
stdio: 'pipe',
|
||||
timeout: 120_000,
|
||||
@@ -47,7 +88,7 @@ describeIfSwift('swift build invariants', () => {
|
||||
}, 180_000);
|
||||
|
||||
test('XCTest suite for StateServer passes (validates real Swift impl)', () => {
|
||||
const r = spawnSync('swift', ['test'], {
|
||||
const r = spawnSync('swift', ['test', '--filter', 'DebugBridgeCoreTests'], {
|
||||
cwd: FIXTURE_PATH,
|
||||
stdio: 'pipe',
|
||||
timeout: 180_000,
|
||||
@@ -59,18 +100,23 @@ describeIfSwift('swift build invariants', () => {
|
||||
console.error('swift test failure:', combined.slice(-4000));
|
||||
}
|
||||
expect(r.status).toBe(0);
|
||||
expect(combined).toContain("'All tests' passed");
|
||||
// --filter scopes the run to DebugBridgeCoreTests; the xctest summary
|
||||
// line is "'Selected tests' passed" rather than "'All tests' passed".
|
||||
expect(combined).toMatch(/'(?:All|Selected) tests' passed/);
|
||||
// Guard against an empty pass-by-no-tests (filter typo / target rename):
|
||||
// we expect at least one StateServer smoke test to actually execute.
|
||||
expect(combined).toContain('StateServerSmokeTests');
|
||||
}, 240_000);
|
||||
|
||||
// Codex-flagged: Release-build guard must be STRUCTURAL, not advisory.
|
||||
// The Package.swift's `.when(configuration: .debug)` setting causes Swift
|
||||
// to compile-out the entire DebugBridge target body in Release. Since
|
||||
// to compile-out the entire DebugBridgeCore target body in Release. Since
|
||||
// every public symbol is gated `#if DEBUG`, the release build emits an
|
||||
// empty module — zero symbols.
|
||||
test('Release-config build excludes DebugBridge symbols', () => {
|
||||
// Step 1: clean + release build
|
||||
// Step 1: clean + release build (Core only — UI/Touch can't build on macOS)
|
||||
spawnSync('swift', ['package', 'clean'], { cwd: FIXTURE_PATH, stdio: 'pipe', timeout: 60_000 });
|
||||
const build = spawnSync('swift', ['build', '-c', 'release'], {
|
||||
const build = spawnSync('swift', ['build', '-c', 'release', '--target', 'DebugBridgeCore'], {
|
||||
cwd: FIXTURE_PATH,
|
||||
stdio: 'pipe',
|
||||
timeout: 180_000,
|
||||
|
||||
Reference in New Issue
Block a user