#import "../src-tauri/src/navigation.m" // Exercise the AppKit event adapter deterministically; macos_smoke separately // runs the shared history script against a real WKWebView. @interface TestWindow : NSWindow @end @implementation TestWindow - (BOOL)isKeyWindow { return YES; } @end @interface TestWebView : WKWebView @property(nonatomic, strong) NSWindow *testWindow; @property(nonatomic, strong) NSURL *testURL; @property(nonatomic) BOOL back; @property(nonatomic) BOOL forward; @property(nonatomic) BOOL allowsSwipe; @property(nonatomic) BOOL deferPermission; @property(nonatomic, copy) void (^permission)(id, NSError *); @property(nonatomic, strong) NSMutableArray *steps; @end @implementation TestWebView - (NSWindow *)window { return self.testWindow; } - (NSURL *)URL { return self.testURL; } - (BOOL)canGoBack { return self.back; } - (BOOL)canGoForward { return self.forward; } - (NSPoint)convertPoint:(NSPoint)point fromView:(NSView *)view { return point; } - (void)evaluateJavaScript:(NSString *)script completionHandler:(void (^)(id, NSError *))handler { if ([script containsString:@".step("]) [self.steps addObject:script]; else if (self.deferPermission) self.permission = handler; else handler(@(self.allowsSwipe), nil); } @end @interface TestEvent : NSEvent @property(nonatomic, strong) NSWindow *testWindow; @property(nonatomic) NSEventPhase testPhase; @property(nonatomic) NSEventPhase testMomentum; @property(nonatomic) CGFloat dx; @property(nonatomic) CGFloat dy; @property(nonatomic) BOOL precise; @property(nonatomic) NSInteger trackCount; @property(nonatomic, copy) void (^tracking)(CGFloat, NSEventPhase, BOOL, BOOL *); @end @implementation TestEvent - (NSWindow *)window { return self.testWindow; } - (NSEventPhase)phase { return self.testPhase; } - (NSEventPhase)momentumPhase { return self.testMomentum; } - (CGFloat)scrollingDeltaX { return self.dx; } - (CGFloat)scrollingDeltaY { return self.dy; } - (BOOL)hasPreciseScrollingDeltas { return self.precise; } - (NSPoint)locationInWindow { return NSMakePoint(20, 20); } - (void)trackSwipeEventWithOptions:(NSEventSwipeTrackingOptions)options dampenAmountThresholdMin:(CGFloat)minimum max:(CGFloat)maximum usingHandler:(void (^)(CGFloat, NSEventPhase, BOOL, BOOL *))handler { NSCAssert(options & NSEventSwipeTrackingLockDirection, @"Direction must be locked"); self.trackCount++; self.tracking = handler; } @end @interface TestNavigation : KanbanNavigation @property(nonatomic) BOOL enabled; @end @implementation TestNavigation - (BOOL)swipeTrackingEnabled { return self.enabled; } @end int main(void) { @autoreleasepool { [NSApplication sharedApplication]; TestWindow *window = [[TestWindow alloc] initWithContentRect:NSMakeRect(0, 0, 600, 400) styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]; TestWebView *webview = [[TestWebView alloc] initWithFrame:NSMakeRect(0, 0, 600, 400)]; webview.testWindow = window; webview.testURL = [NSURL URLWithString:@"https://example.com/board"]; webview.back = YES; webview.forward = YES; webview.allowsSwipe = YES; webview.steps = [NSMutableArray new]; TestNavigation *navigation = [TestNavigation new]; navigation.webview = webview; navigation.enabled = YES; TestEvent *(^event)(void) = ^{ TestEvent *value = [TestEvent new]; value.testWindow = window; value.testPhase = NSEventPhaseBegan; value.precise = YES; value.dx = 15; return value; }; BOOL stop = NO; TestEvent *back = event(); [navigation handleEvent:back]; NSCAssert(back.trackCount == 1, @"A horizontal gesture must use AppKit tracking"); back.tracking(0.5, NSEventPhaseChanged, NO, &stop); NSCAssert(webview.steps.count == 0, @"A partial swipe must not navigate"); back.tracking(1, NSEventPhaseNone, YES, &stop); NSCAssert(webview.steps.count == 1 && [webview.steps.lastObject containsString:@"step(-1)"], @"Back must request exactly one step"); TestEvent *forward = event(); forward.dx = -15; [navigation handleEvent:forward]; forward.tracking(-1, NSEventPhaseNone, YES, &stop); NSCAssert(webview.steps.count == 2 && [webview.steps.lastObject containsString:@"step(1)"], @"Forward must request exactly one step"); TestEvent *cancelled = event(); [navigation handleEvent:cancelled]; cancelled.tracking(0, NSEventPhaseCancelled, YES, &stop); NSCAssert(webview.steps.count == 2, @"Cancelled swipes must not navigate"); TestEvent *interrupted = event(); [navigation handleEvent:interrupted]; webview.allowsSwipe = NO; [navigation handleEvent:event()]; interrupted.tracking(1, NSEventPhaseNone, YES, &stop); NSCAssert(webview.steps.count == 2, @"A new gesture must cancel the previous gesture's animation callback"); for (NSInteger test = 0; test < 7; test++) { TestEvent *ignored = event(); navigation.enabled = test != 0; webview.allowsSwipe = test != 1; webview.back = webview.forward = test != 2; if (test == 3) ignored.precise = NO; if (test == 4) ignored.dy = 30; if (test == 5) ignored.testMomentum = NSEventPhaseChanged; if (test == 6) ignored.testWindow = nil; NSCAssert([navigation handleEvent:ignored] == ignored && ignored.trackCount == 0, @"Non-navigation events must remain available for scrolling"); } navigation.enabled = YES; webview.allowsSwipe = YES; webview.back = YES; webview.deferPermission = YES; TestEvent *stale = event(); [navigation handleEvent:stale]; TestEvent *ended = event(); ended.testPhase = NSEventPhaseEnded; [navigation handleEvent:ended]; webview.permission(@YES, nil); NSCAssert(stale.trackCount == 0, @"Late hit tests must not resurrect ended gestures"); TestEvent *changedPage = event(); [navigation handleEvent:changedPage]; webview.testURL = [NSURL URLWithString:@"https://example.com/settings"]; webview.permission(@YES, nil); NSCAssert(changedPage.trackCount == 0, @"Page changes must invalidate a pending gesture"); puts("PASS: AppKit back/forward, cancelled/partial swipes, system preference, nested scroll ownership, empty history, mouse/vertical/momentum events, stale callbacks"); } return 0; }