blob: 9ed1137b38111cfba0bbedfb9aa95db5a82a96bb [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "ios/web/public/test/javascript_test.h"
#import "ios/web/public/test/js_test_util.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "url/gurl.h"
NSString* kUtilsSampleMessageHandlerName = @"UtilsSampleMessageHandlerName";
// A WKScriptMessageHandler which stores the last received WKScriptMessage;
@interface FakeScriptMessageHandler : NSObject <WKScriptMessageHandler>
@property(nonatomic, strong) WKScriptMessage* lastReceivedMessage;
@end
@implementation FakeScriptMessageHandler
- (void)userContentController:(WKUserContentController*)userContentController
didReceiveScriptMessage:(WKScriptMessage*)message {
_lastReceivedMessage = message;
}
@end
// Test fixture for utils.ts.
class UtilsJavaScriptTest : public web::JavascriptTest {
protected:
UtilsJavaScriptTest() : handler_([[FakeScriptMessageHandler alloc] init]) {
[web_view().configuration.userContentController
addScriptMessageHandler:handler_
name:kUtilsSampleMessageHandlerName];
}
~UtilsJavaScriptTest() override {}
void SetUp() override {
JavascriptTest::SetUp();
AddGCrWebScript();
AddUserScript(@"utils_test_api");
ASSERT_TRUE(LoadHtml(@"<p>"));
}
protected:
FakeScriptMessageHandler* handler_;
};
// Tests that removeQueryAndReferenceFromURL works as expected
TEST_F(UtilsJavaScriptTest, RemoveQueryAndReferenceFromURL) {
struct TestData {
NSString* input_url;
NSString* expected_output;
} test_data[] = {
{@"http://foo1.com/bar", @"http://foo1.com/bar"},
{@"http://foo2.com/bar#baz", @"http://foo2.com/bar"},
{@"http://foo3.com/bar?baz", @"http://foo3.com/bar"},
// Order of fragment and query string does not matter.
{@"http://foo4.com/bar#baz?blech", @"http://foo4.com/bar"},
{@"http://foo5.com/bar?baz#blech", @"http://foo5.com/bar"},
// Truncates on the first fragment mark.
{@"http://foo6.com/bar/#baz#blech", @"http://foo6.com/bar/"},
// Poorly formed URLs are normalized.
{@"http:///foo7.com//bar?baz", @"http://foo7.com//bar"},
// Non-http protocols.
{@"data:abc", @"data:abc"},
{@"javascript:login()", @"javascript:login()"},
};
for (size_t i = 0; i < std::size(test_data); i++) {
LoadHtml(@"<p>");
TestData& data = test_data[i];
id result = web::test::ExecuteJavaScript(
web_view(),
[NSString stringWithFormat:
@"__gCrWeb.getRegisteredApi('utils_tests').getFunction('"
@"removeQueryAndReferenceFromURL')('%@')",
data.input_url]);
EXPECT_NSEQ(data.expected_output, result)
<< " in test " << i << ": " << base::SysNSStringToUTF8(data.input_url);
}
}
// Tests that removeQueryAndReferenceFromURL() returns an empty string when
// the window.URL prototype was corrupted (i.e. the hosted page replaces the
// prototype by something else).
TEST_F(
UtilsJavaScriptTest,
RemoveQueryAndReferenceFromURL_WithCorruptedURLPrototype_MissingProperty) {
// Replace the window.URL prototype.
web::test::ExecuteJavaScriptInWebView(
web_view(), @"window.URL = function() { return { weird_field: 1 }; };");
NSString* apiCall = @"__gCrWeb.getRegisteredApi('utils_tests').getFunction('"
@"removeQueryAndReferenceFromURL')('%@')";
NSString* url = @"http://foo1.com/bar";
NSString* js = [NSString stringWithFormat:apiCall, url];
id result = web::test::ExecuteJavaScript(web_view(), js);
EXPECT_NSEQ(@"", result);
}
// Tests that removeQueryAndReferenceFromURL() returns an empty string when
// the window.URL prototype was corrupted (i.e. the hosted page replaces the
// prototype by something else).
TEST_F(UtilsJavaScriptTest,
RemoveQueryAndReferenceFromURL_WithCorruptedURLPrototype_WrongType) {
// Replace the window.URL prototype.
web::test::ExecuteJavaScriptInWebView(
web_view(), @"window.URL = function() { return {"
"origin: 'o', path: 'pa', protocol: 3 }; };");
NSString* apiCall = @"__gCrWeb.getRegisteredApi('utils_tests').getFunction('"
@"removeQueryAndReferenceFromURL')('%@')";
NSString* url = @"http://foo1.com/bar";
NSString* js = [NSString stringWithFormat:apiCall, url];
id result = web::test::ExecuteJavaScript(web_view(), js);
EXPECT_NSEQ(@"", result);
}
// Tests that removeQueryAndReferenceFromURL doesn't throw an exception on
// invalid input.
TEST_F(UtilsJavaScriptTest, RemoveQueryAndReferenceFromURL_InvalidInput) {
struct TestData {
NSString* input;
NSString* expected_output;
} test_data[] = {
{@"undefined", @""},
{@"null", @""},
{@"function() {}", @""},
{@"'stringButNotURL'", @""},
};
for (size_t i = 0; i < std::size(test_data); i++) {
TestData& data = test_data[i];
NSString* js = [NSString
stringWithFormat:@"__gCrWeb.getRegisteredApi('utils_tests')."
@"getFunction('removeQueryAndReferenceFromURL')(%@)",
data.input];
id result = web::test::ExecuteJavaScript(web_view(), js);
EXPECT_NSEQ(data.expected_output, result)
<< " in test " << i << ": " << base::SysNSStringToUTF8(data.input);
}
}
// Tests that sendWebKitMessage works as expected
TEST_F(UtilsJavaScriptTest, SendWebKitMessage) {
struct TestData {
NSString* input;
id expected_output;
} test_data[] = {
{@"1", @1},
{@"1.5", @1.5},
{@"'String data'", @"String data"},
{@"['a', 'b', 'c']", @[ @"a", @"b", @"c" ]},
{@"{'a' : 'x', 'b' : 'y', 'c' : 'z'}",
@{@"a" : @"x", @"b" : @"y", @"c" : @"z"}},
};
for (size_t i = 0; i < std::size(test_data); i++) {
TestData& data = test_data[i];
// Reset value to ensure wait below stops at correct time.
handler_.lastReceivedMessage = nil;
NSString* js = [NSString
stringWithFormat:@"__gCrWeb.getRegisteredApi('utils_tests')."
@"getFunction('sendWebKitMessage')('%@', %@)",
kUtilsSampleMessageHandlerName, data.input];
web::test::ExecuteJavaScriptInWebView(web_view(), js);
ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForJSCompletionTimeout, ^bool() {
return handler_.lastReceivedMessage;
}));
EXPECT_NSEQ(data.expected_output, handler_.lastReceivedMessage.body)
<< " in test " << i << ": " << base::SysNSStringToUTF8(data.input);
}
}
// Tests that trim works as expected
TEST_F(UtilsJavaScriptTest, Trim) {
struct TestData {
NSString* input_string;
NSString* expected_output;
} test_data[] = {
{@"' content'", @"content"},
{@"'content '", @"content"},
{@"' content '", @"content"},
{@"' cont ent '", @"cont ent"},
{@"null", @""},
{@"undefined", @""},
};
for (size_t i = 0; i < std::size(test_data); i++) {
TestData& data = test_data[i];
NSString* js = [NSString
stringWithFormat:
@"__gCrWeb.getRegisteredApi('utils_tests').getFunction('trim')(%@)",
data.input_string];
id result = web::test::ExecuteJavaScript(web_view(), js);
EXPECT_NSEQ(data.expected_output, result)
<< " in test " << i << ": "
<< base::SysNSStringToUTF8(data.input_string);
}
}
// Tests that isTextField from utils.ts works as expected.
TEST_F(UtilsJavaScriptTest, IsTextField) {
// Struct for isTextField() test data.
struct TextFieldTestElement {
// The element name.
const char* element_name;
// The index of this element in those that have the same name.
const int element_index;
// True if this is expected to be a text field.
const bool expected_is_text_field;
};
LoadHtml(@"<html><body>"
"<input type='text' name='firstname'>"
"<input type='text' name='lastname'>"
"<input type='email' name='email'>"
"<input type='tel' name='phone'>"
"<input type='url' name='blog'>"
"<input type='number' name='expected number of clicks'>"
"<input type='password' name='pwd'>"
"<input type='checkbox' name='vehicle' value='Bike'>"
"<input type='checkbox' name='vehicle' value='Car'>"
"<input type='checkbox' name='vehicle' value='Rocket'>"
"<input type='radio' name='boolean' value='true'>"
"<input type='radio' name='boolean' value='false'>"
"<input type='radio' name='boolean' value='other'>"
"<select name='state'>"
" <option value='CA'>CA</option>"
" <option value='MA'>MA</option>"
"</select>"
"<select name='cars' multiple>"
" <option value='volvo'>Volvo</option>"
" <option value='saab'>Saab</option>"
" <option value='opel'>Opel</option>"
" <option value='audi'>Audi</option>"
"</select>"
"<input type='submit' name='submit' value='Submit'>"
"</body></html>");
static const struct TextFieldTestElement testElements[] = {
{"firstname", 0, true}, {"lastname", 0, true},
{"email", 0, true}, {"phone", 0, true},
{"blog", 0, true}, {"expected number of clicks", 0, true},
{"pwd", 0, true}, {"vehicle", 0, false},
{"vehicle", 1, false}, {"vehicle", 2, false},
{"boolean", 0, false}, {"boolean", 1, false},
{"boolean", 2, false}, {"state", 0, false},
{"cars", 0, false}, {"submit", 0, false}};
for (size_t i = 0; i < std::size(testElements); ++i) {
TextFieldTestElement element = testElements[i];
id result = web::test::ExecuteJavaScript(
web_view(),
[NSString
stringWithFormat:@"__gCrWeb.getRegisteredApi('utils_tests')."
@"getFunction('isTextField')("
"window.document.getElementsByName('%s')[%d])",
element.element_name, element.element_index]);
EXPECT_NSEQ(element.expected_is_text_field ? @YES : @NO, result)
<< element.element_name << " with index " << element.element_index
<< " isTextField(): " << element.expected_is_text_field;
}
}