166 lines
9.4 KiB
Objective-C
166 lines
9.4 KiB
Objective-C
#import <AppKit/AppKit.h>
|
|
#import <WebKit/WebKit.h>
|
|
#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 <WKUIDelegate>
|
|
@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<NSURL *> *))completionHandler {
|
|
self.uploaded = YES;
|
|
completionHandler(nil);
|
|
}
|
|
@end
|
|
|
|
@interface WorkspacePages : NSObject <WKURLSchemeHandler, WKNavigationDelegate>
|
|
@property(nonatomic) NSUInteger finished;
|
|
@end
|
|
|
|
@implementation WorkspacePages
|
|
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)task {
|
|
NSData *body = [[NSString stringWithFormat:@"<html><body>%@<input value='draft'></body></html>", 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<WKURLSchemeTask>)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<WKUIDelegate> 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 = `<input id='duplicate' type='radio'><input id='draft' value='Unsaved project'><input id='upload' type='file'><a id='back' href='#back'>Back</a>`; 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");
|
|
}
|
|
}
|