#import #import #import "../src-tauri/src/navigation.m" static void check(BOOL condition, NSString *message) { if (!condition) { fprintf(stderr, "FAIL: %s\n", message.UTF8String); exit(1); } } static void waitUntil(BOOL (^condition)(void), NSString *message) { NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:10]; while (!condition() && deadline.timeIntervalSinceNow > 0) { [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; } check(condition(), message); } static id evaluate(WKWebView *webview, NSString *script) { __block BOOL finished = NO; __block id result; [webview evaluateJavaScript:script completionHandler:^(id value, NSError *error) { check(error == nil, error.localizedDescription); result = value; finished = YES; }]; waitUntil(^BOOL { return finished; }, @"JavaScript must finish after the confirmation is dismissed"); return result; } static NSButton *findButton(NSView *view, NSString *title) { if ([view isKindOfClass:NSButton.class] && [((NSButton *)view).title isEqualToString:title]) { return (NSButton *)view; } for (NSView *child in view.subviews) { NSButton *button = findButton(child, title); if (button) { return button; } } return nil; } static BOOL containsText(NSView *view, NSString *text) { if ([view isKindOfClass:NSTextField.class] && [((NSTextField *)view).stringValue isEqualToString:text]) { return YES; } for (NSView *child in view.subviews) { if (containsText(child, text)) { return YES; } } return NO; } @interface WorkspaceUI : NSObject @property(nonatomic) BOOL closed; @property(nonatomic) BOOL uploaded; @end @implementation WorkspaceUI - (void)webViewDidClose:(WKWebView *)webView { self.closed = YES; } - (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray *))completionHandler { self.uploaded = YES; completionHandler(nil); } @end @interface WorkspacePages : NSObject @property(nonatomic) NSUInteger finished; @end @implementation WorkspacePages - (void)webView:(WKWebView *)webView startURLSchemeTask:(id)task { NSData *body = [[NSString stringWithFormat:@"%@", task.request.URL.path] dataUsingEncoding:NSUTF8StringEncoding]; NSURLResponse *response = [[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:body.length textEncodingName:@"utf-8"]; [task didReceiveResponse:response]; [task didReceiveData:body]; [task didFinish]; } - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id)task {} - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { self.finished++; } @end int main(void) { @autoreleasepool { [NSApplication sharedApplication]; WorkspacePages *pages = [WorkspacePages new]; WKWebViewConfiguration *configuration = [WKWebViewConfiguration new]; configuration.websiteDataStore = WKWebsiteDataStore.nonPersistentDataStore; [configuration setURLSchemeHandler:pages forURLScheme:@"kanban-test"]; WKWebView *webview = [[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration]; webview.navigationDelegate = pages; WorkspaceUI *originalUI = [WorkspaceUI new]; webview.UIDelegate = originalUI; check(!webview.allowsBackForwardNavigationGestures, @"WebKit defaults to disabled gestures"); install_workspace_navigation((__bridge void *)webview); id installedUI = webview.UIDelegate; install_workspace_navigation((__bridge void *)webview); check(webview.UIDelegate == installedUI, @"Installing workspace behavior twice must retain the same delegate"); check([installedUI respondsToSelector:@selector(webViewDidClose:)], @"Existing popup callbacks must remain available"); [installedUI webViewDidClose:webview]; check(originalUI.closed, @"Existing popup callbacks must reach the original delegate"); check([installedUI respondsToSelector:@selector(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:)], @"Existing file upload callbacks must remain available"); check(webview.allowsBackForwardNavigationGestures, @"The workspace must enable WebKit's interactive gestures"); check(webview.configuration.userContentController.userScripts.count == 0, @"Enabling gestures must not inject a history adapter"); check([webview goBack] == nil && [webview goForward] == nil, @"Empty history must not navigate"); [webview loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"kanban-test://workspace/first"]]]; waitUntil(^BOOL { return pages.finished == 1; }, @"First page must load"); check(!webview.canGoBack && !webview.canGoForward, @"The first page must have no navigation target"); [webview loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"kanban-test://workspace/second"]]]; waitUntil(^BOOL { return pages.finished == 2; }, @"Second page must load"); check(webview.canGoBack && !webview.canGoForward, @"Native history must retain the first page"); check([webview goBack] != nil, @"Native back navigation must start"); waitUntil(^BOOL { return pages.finished == 3; }, @"Native back navigation must complete"); check([webview.URL.path isEqualToString:@"/first"] && !webview.canGoBack && webview.canGoForward, @"Back must restore the first page and retain forward history"); check([webview goBack] == nil, @"Back at the first page must leave it in place"); check([webview goForward] != nil, @"Native forward navigation must start"); waitUntil(^BOOL { return pages.finished == 4; }, @"Native forward navigation must complete"); check([webview.URL.path isEqualToString:@"/second"] && webview.canGoBack && !webview.canGoForward, @"Forward must restore the second page"); check(webview.allowsBackForwardNavigationGestures, @"Native gestures must remain enabled across page navigation"); NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:NSWindowStyleMaskTitled backing:NSBackingStoreBuffered defer:NO]; window.releasedWhenClosed = NO; window.title = @"KanbanGit navigation test"; window.contentView = webview; [window orderFront:nil]; evaluate(webview, @"document.body.innerHTML = `Back`; window.visits = 0; document.querySelector('#back').onclick = event => { event.preventDefault(); if (!document.querySelector('#duplicate').checked || confirm('Discard unsaved changes?')) window.visits++; }; true;"); evaluate(webview, @"document.querySelector('#upload').click()"); waitUntil(^BOOL { return originalUI.uploaded; }, @"File selection must still reach the original UI delegate"); evaluate(webview, @"document.querySelector('#back').click()"); check([evaluate(webview, @"window.visits") intValue] == 1, @"Pristine forms must navigate without a confirmation"); evaluate(webview, @"document.querySelector('#duplicate').click()"); for (NSString *choice in @[@"Cancel", @"OK", @"Cancel", @"OK"]) { __block BOOL finished = NO; [webview evaluateJavaScript:@"document.querySelector('#back').click()" completionHandler:^(id result, NSError *error) { check(error == nil, error.localizedDescription); finished = YES; }]; waitUntil(^BOOL { return window.attachedSheet != nil; }, @"Dirty form navigation must show a native confirmation"); check(!finished, @"The original click must wait for the user's decision"); check(containsText(window.attachedSheet.contentView, @"workspace"), @"The confirmation must identify the requesting website"); check(containsText(window.attachedSheet.contentView, @"Discard unsaved changes?"), @"The confirmation must show the application's message"); NSButton *button = findButton(window.attachedSheet.contentView, choice); check(button != nil, @"The native confirmation must offer OK and Cancel"); [button performClick:nil]; waitUntil(^BOOL { return finished && window.attachedSheet == nil; }, @"Dismissal must release the original navigation"); check([evaluate(webview, @"document.querySelector('#draft').value") isEqualToString:@"Unsaved project"], @"Cancelling a confirmation must preserve the draft"); } check([evaluate(webview, @"window.visits") intValue] == 3, @"Only accepted visits must run, exactly once each"); window.contentView = nil; check(![evaluate(webview, @"confirm('Detached view')") boolValue], @"A detached view must cancel and release JavaScript immediately"); [window orderOut:nil]; [window close]; puts("PASS: WebKit interactive gesture configuration, full-page back/forward, native confirmations and delegate forwarding"); } }