blob: 72b94fac7b443e796d3febf10e1c36d42a679cbc [file] [log] [blame]
/*
* Copyright (C) 2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "PlatformUtilities.h"
#import "Test.h"
#import "TestWKWebView.h"
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/_WKContentWorld.h>
namespace TestWebKitAPI {
TEST(AsyncFunction, Basic)
{
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
NSError *error;
// Function with no return value.
// Returns JavaScript undefined which translates to nil.
id result = [webView objectByCallingAsyncFunction:@"1" withArguments:nil error:&error];
EXPECT_NULL(error);
EXPECT_EQ(result, nil);
// Function returns explicit null.
// Returns JavaScript null which translates to NSNull.
result = [webView objectByCallingAsyncFunction:@"return null" withArguments:nil error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([result isKindOfClass:[NSNull class]]);
// Function returns a number.
result = [webView objectByCallingAsyncFunction:@"return 1" withArguments:nil error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([result isKindOfClass:[NSNumber class]]);
EXPECT_TRUE([result isEqualToNumber:@1]);
// Function returns a string.
result = [webView objectByCallingAsyncFunction:@"return '1'" withArguments:nil error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([result isKindOfClass:[NSString class]]);
EXPECT_TRUE([result isEqualToString:@"1"]);
// Takes multiple arguments.
result = [webView objectByCallingAsyncFunction:@"return a + b" withArguments:@{ @"a" : @40, @"b" : @2 } error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([result isKindOfClass:[NSNumber class]]);
EXPECT_TRUE([result isEqualToNumber:@42]);
// Takes multiple arguments of different types, follows javascripts "type appending" rules.
result = [webView objectByCallingAsyncFunction:@"return foo + bar" withArguments:@{ @"foo" : @"foo", @"bar" : @42 } error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([result isKindOfClass:[NSString class]]);
EXPECT_TRUE([result isEqualToString:@"foo42"]);
// Invalid JavaScript, should return an error.
result = [webView objectByCallingAsyncFunction:@"retunr null" withArguments:nil error:&error];
EXPECT_FALSE(error == nil);
EXPECT_NULL(result);
}
TEST(AsyncFunction, InvalidArguments)
{
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
NSError *error;
// Values can only be NSString, NSNumber, NSDate, NSNull, NS(Mutable)Array, NS(Mutable)Dictionary, NSNull, and may only contain those 6 types.
id result = [webView objectByCallingAsyncFunction:@"return 1" withArguments:@{ @"a" : [NSData data] } error:&error];
EXPECT_NULL(result);
EXPECT_FALSE(error == nil);
result = [webView objectByCallingAsyncFunction:@"return 1" withArguments:@{ @"a" : @[ @1, [NSData data] ] } error:&error];
EXPECT_NULL(result);
EXPECT_FALSE(error == nil);
// References an argument that was not provided.
result = [webView objectByCallingAsyncFunction:@"return a + b" withArguments:@{ @"a" : @40 } error:&error];
EXPECT_NULL(result);
EXPECT_FALSE(error == nil);
}
TEST(AsyncFunction, RoundTrip)
{
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
NSError *error;
// Tests round tripping a whole bunch of argument inputs and verifying the result.
id value = @42;
id arguments = @{ @"a" : value };
id result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([value isEqual:result]);
value = [NSNull null];
arguments = @{ @"a" : value };
result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([value isEqual:result]);
value = @"Foo";
arguments = @{ @"a" : value };
result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([value isEqual:result]);
value = [NSDate dateWithTimeIntervalSinceReferenceDate:0];
arguments = @{ @"a" : value };
result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([value isEqual:result]);
value = @[ @1, @2 ];
arguments = @{ @"a" : value };
result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([value isEqual:result]);
NSMutableArray *mutableArray = [[NSMutableArray alloc] init];
[mutableArray addObject:value];
arguments = @{ @"a" : mutableArray };
result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([mutableArray isEqual:result]);
[mutableArray release];
value = @[ @"foo", [NSDate dateWithTimeIntervalSinceReferenceDate:0] ];
arguments = @{ @"a" : value };
result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([value isEqual:result]);
value = @{ @"a" : @1, @"b" : @2 };
arguments = @{ @"a" : value };
result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([value isEqual:result]);
NSMutableDictionary<NSString *, id> *mutableDictionary = [[NSMutableDictionary alloc] init];
mutableDictionary[@"foo"] = value;
arguments = @{ @"a" : mutableDictionary };
result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([mutableDictionary isEqual:result]);
[mutableDictionary release];
value = @{ @"a" : @"foo", @"b" : [NSDate dateWithTimeIntervalSinceReferenceDate:0] };
arguments = @{ @"a" : value };
result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([value isEqual:result]);
value = @{ @"a" : @{ @"a" : @1 } };
arguments = @{ @"a" : value };
result = [webView objectByCallingAsyncFunction:@"return a" withArguments:arguments error:&error];
EXPECT_NULL(error);
EXPECT_TRUE([value isEqual:result]);
}
static void tryGCPromise(WKWebView *webView, bool& done)
{
if (done)
return;
NSString *functionBody = @"return new Promise(function(resolve, reject) { })";
[webView _callAsyncJavaScriptFunction:functionBody withArguments:nil inWorld:_WKContentWorld.pageContentWorld completionHandler:[&] (id result, NSError *error) {
EXPECT_NULL(result);
EXPECT_TRUE(error != nil);
EXPECT_TRUE([[error description] containsString:@"no longer reachable"]);
done = true;
}];
dispatch_async(dispatch_get_main_queue(), ^{ tryGCPromise(webView, done); });
}
TEST(AsyncFunction, Promise)
{
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
NSString *functionBody = @"return new Promise(function(resolve, reject) { setTimeout(function(){ resolve(42) }, 0); })";
bool done = false;
[webView _callAsyncJavaScriptFunction:functionBody withArguments:nil inWorld:_WKContentWorld.pageContentWorld completionHandler:[&] (id result, NSError *error) {
EXPECT_NULL(error);
EXPECT_TRUE([result isKindOfClass:[NSNumber class]]);
EXPECT_TRUE([result isEqualToNumber:@42]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
functionBody = @"return new Promise(function(resolve, reject) { setTimeout(function(){ reject('Rejected!') }, 0); })";
done = false;
[webView _callAsyncJavaScriptFunction:functionBody withArguments:nil inWorld:_WKContentWorld.pageContentWorld completionHandler:[&] (id result, NSError *error) {
EXPECT_NULL(result);
EXPECT_TRUE(error != nil);
EXPECT_TRUE([[error description] containsString:@"Rejected!"]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
functionBody = @"let p = new Proxy(function(resolve, reject) { setTimeout(function() { resolve(42); }, 0); }, { }); return { then: p };";
done = false;
[webView _callAsyncJavaScriptFunction:functionBody withArguments:nil inWorld:_WKContentWorld.pageContentWorld completionHandler:[&] (id result, NSError *error) {
EXPECT_NULL(error);
EXPECT_TRUE([result isKindOfClass:[NSNumber class]]);
EXPECT_TRUE([result isEqualToNumber:@42]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
functionBody = @"let p = new Proxy(function(resolve, reject) { setTimeout(function() { reject('Rejected!'); }, 0); }, { }); return { then: p };";
done = false;
[webView _callAsyncJavaScriptFunction:functionBody withArguments:nil inWorld:_WKContentWorld.pageContentWorld completionHandler:[&] (id result, NSError *error) {
EXPECT_NULL(result);
EXPECT_TRUE(error != nil);
EXPECT_TRUE([[error description] containsString:@"Rejected!"]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
// Verify we can await for a promise to be resolved before returning.
functionBody = @"var r = 0; var p = new Promise(function(fulfill, reject) { setTimeout(function(){ r = 42; fulfill(); }, 5);}); await p; return r;";
done = false;
[webView _callAsyncJavaScriptFunction:functionBody withArguments:nil inWorld:_WKContentWorld.pageContentWorld completionHandler:[&] (id result, NSError *error) {
EXPECT_NULL(error);
EXPECT_TRUE([result isKindOfClass:[NSNumber class]]);
EXPECT_TRUE([result isEqualToNumber:@42]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
// Returning an already resolved promise gives the value it was resolved with.
functionBody = @"var p = new Promise(function(fulfill, reject) { setTimeout(function(){ fulfill('Fulfilled!') }, 5);}); await p; return p;";
done = false;
[webView _callAsyncJavaScriptFunction:functionBody withArguments:nil inWorld:_WKContentWorld.pageContentWorld completionHandler:[&] (id result, NSError *error) {
EXPECT_NULL(error);
EXPECT_TRUE([result isKindOfClass:[NSString class]]);
EXPECT_TRUE([result isEqualToString:@"Fulfilled!"]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
// Chaining thenables should work.
functionBody = @"var p = new Promise(function (r) { r(new Promise(function (r) { r(42); })); }); await p; return 'Done';";
done = false;
[webView _callAsyncJavaScriptFunction:functionBody withArguments:nil inWorld:_WKContentWorld.pageContentWorld completionHandler:[&] (id result, NSError *error) {
EXPECT_NULL(error);
EXPECT_TRUE([result isKindOfClass:[NSString class]]);
EXPECT_TRUE([result isEqualToString:@"Done"]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
tryGCPromise(webView.get(), done);
TestWebKitAPI::Util::run(&done);
}
}