/* * Copyright (C) 2020 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" #if USE(APPLE_INTERNAL_SDK) #import "InstanceMethodSwizzler.h" #import "PlatformUtilities.h" #import "Test.h" #import "TestURLSchemeHandler.h" #import "TestWKWebView.h" #import "VirtualGamepad.h" #import @interface GamepadMessageHandler : NSObject @property (readonly, nonatomic) Vector> messages; @end static bool didReceiveMessage = false; @implementation GamepadMessageHandler - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { _messages.append(message.body); didReceiveMessage = true; } @end namespace TestWebKitAPI { static const char* mainBytes = R"GAMEPADRESOURCE( )GAMEPADRESOURCE"; static NSWindow *keyWindowForTesting; static NSWindow *getKeyWindowForTesting() { return keyWindowForTesting; } TEST(Gamepad, Basic) { auto keyWindowSwizzler = makeUnique([NSApplication class], @selector(keyWindow), reinterpret_cast(getKeyWindowForTesting)); auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto messageHandler = adoptNS([[GamepadMessageHandler alloc] init]); [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"gamepad"]; auto schemeHandler = adoptNS([[TestURLSchemeHandler alloc] init]); [configuration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"gamepad"]; [schemeHandler setStartURLSchemeTaskHandler:^(WKWebView *, id task) { auto response = adoptNS([[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:0 textEncodingName:nil]); [task didReceiveResponse:response.get()]; [task didReceiveData:[NSData dataWithBytes:mainBytes length:strlen(mainBytes)]]; [task didFinish]; }]; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); keyWindowForTesting = [webView window]; [webView.get().configuration.processPool _setUsesOnlyHIDGamepadProviderForTesting:YES]; [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"gamepad://host/main.html"]]]; [[webView window] makeFirstResponder:webView.get()]; // Resigning/reinstating the key window state triggers the "key window did change" notification that WKWebView currently // needs to convince it to monitor gamepad devices [[webView window] resignKeyWindow]; [[webView window] makeKeyWindow]; // Create a gamepad with a fake vendorID auto mapping = VirtualGamepad::steelSeriesNimbusMapping(); mapping.vendorID = HIDVendorID::Fake; auto gamepad = makeUnique(mapping); while (![webView.get().configuration.processPool _numberOfConnectedGamepadsForTesting]) Util::sleep(0.1); // Press a button on it, waiting for the web page to see it gamepad->setButtonValue(0, 1.0); gamepad->publishReport(); Util::run(&didReceiveMessage); didReceiveMessage = false; EXPECT_EQ(messageHandler.get().messages.size(), 1u); EXPECT_TRUE([messageHandler.get().messages[0] isEqualToString:@"\"ffff-1420-Virtual Nimbus\""]); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedGamepadsForTesting], 1u); } TEST(Gamepad, GCFVersusHID) { auto keyWindowSwizzler = makeUnique([NSApplication class], @selector(keyWindow), reinterpret_cast(getKeyWindowForTesting)); auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto messageHandler = adoptNS([[GamepadMessageHandler alloc] init]); [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"gamepad"]; auto schemeHandler = adoptNS([[TestURLSchemeHandler alloc] init]); [configuration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"gamepad"]; [schemeHandler setStartURLSchemeTaskHandler:^(WKWebView *, id task) { auto response = adoptNS([[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:0 textEncodingName:nil]); [task didReceiveResponse:response.get()]; [task didReceiveData:[NSData dataWithBytes:mainBytes length:strlen(mainBytes)]]; [task didFinish]; }]; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); keyWindowForTesting = [webView window]; [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"gamepad://host/main.html"]]]; [[webView window] makeFirstResponder:webView.get()]; // Resigning/reinstating the key window state triggers the "key window did change" notification that WKWebView currently // needs to convince it to monitor gamepad devices [[webView window] resignKeyWindow]; [[webView window] makeKeyWindow]; static const size_t TestGamepadCount = 5; std::unique_ptr gamepads[TestGamepadCount]; // Create gamepads that should go through GameController.framework gamepads[0] = makeUnique(VirtualGamepad::steelSeriesNimbusMapping()); gamepads[1] = makeUnique(VirtualGamepad::microsoftXboxOneMapping()); gamepads[2] = makeUnique(VirtualGamepad::sonyDualshock4Mapping()); // Create one that should go through HID gamepads[3] = makeUnique(VirtualGamepad::shenzhenLongshengweiTechnologyGamepadMapping()); // Create one that GameController framework tries to handle, but shouldn't: // Make sure HID handles it instead. gamepads[4] = makeUnique(VirtualGamepad::sunLightApplicationGenericNESMapping()); while ([webView.get().configuration.processPool _numberOfConnectedGamepadsForTesting] != TestGamepadCount) Util::sleep(0.1); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedHIDGamepadsForTesting], 2u); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedGameControllerFrameworkGamepadsForTesting], 3u); // Press buttons on them, waiting for the web page to see them for (size_t i = 0; i < TestGamepadCount; ++i) { gamepads[i]->setButtonValue(0, 1.0); gamepads[i]->publishReport(); } // The page messages back once for each controller while (messageHandler.get().messages.size() < TestGamepadCount) { didReceiveMessage = false; Util::run(&didReceiveMessage); } NSSet *expectedGamepadNames = [NSSet setWithArray:@[ @"\"Virtual Nimbus Extended Gamepad\"", @"\"Virtual Dualshock4 Extended Gamepad\"", @"\"Virtual Xbox One Gamepad\"", @"\"Virtual Xbox One Extended Gamepad\"", @"\"79-11-Virtual Shenzhen Longshengwei Technology Gamepad\"", @"\"12bd-d015-Virtual Sun Light Application Generic NES Controller\""]]; for (size_t i = 0; i < TestGamepadCount; ++i) EXPECT_TRUE([expectedGamepadNames containsObject:messageHandler.get().messages[i].get()]); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedHIDGamepadsForTesting], 2u); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedGameControllerFrameworkGamepadsForTesting], 3u); } static const char* pollGamepadStateFunction = R"GAMEPADRESOURCE( var result = new Object(); var gamepads = navigator.getGamepads(); result.gamepadCount = gamepads.length; result.gamepadButtons = new Array; result.gamepadAxes = new Array; result.gamepadMapping = new Array; for (var i = 0; i < gamepads.length; ++i) { result.gamepadButtons[i] = new Array; for (var j = 0; j < gamepads[i].buttons.length; ++j) result.gamepadButtons[i][j] = gamepads[i].buttons[j].value; result.gamepadAxes[i] = new Array; for (var j = 0; j < gamepads[i].axes.length; ++j) result.gamepadAxes[i][j] = gamepads[i].axes[j]; result.gamepadMapping[i] = gamepads[i].mapping; } return result; )GAMEPADRESOURCE"; TEST(Gamepad, GamepadState) { auto keyWindowSwizzler = makeUnique([NSApplication class], @selector(keyWindow), reinterpret_cast(getKeyWindowForTesting)); auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto messageHandler = adoptNS([[GamepadMessageHandler alloc] init]); [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"gamepad"]; auto schemeHandler = adoptNS([[TestURLSchemeHandler alloc] init]); [configuration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"gamepad"]; [schemeHandler setStartURLSchemeTaskHandler:^(WKWebView *, id task) { auto response = adoptNS([[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:0 textEncodingName:nil]); [task didReceiveResponse:response.get()]; [task didReceiveData:[NSData dataWithBytes:mainBytes length:strlen(mainBytes)]]; [task didFinish]; }]; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); keyWindowForTesting = [webView window]; [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"gamepad://host/main.html"]]]; [[webView window] makeFirstResponder:webView.get()]; // Resigning/reinstating the key window state triggers the "key window did change" notification that WKWebView currently // needs to convince it to monitor gamepad devices [[webView window] resignKeyWindow]; [[webView window] makeKeyWindow]; // Connect a gamepad and make it visible to the page auto gamepad = makeUnique(VirtualGamepad::shenzhenLongshengweiTechnologyGamepadMapping()); while (![webView.get().configuration.processPool _numberOfConnectedGamepadsForTesting]) Util::sleep(0.01); Vector expectedButtons = { 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; Vector expectedAxes = { -0.9921568627450981, -0.003921568627450966, -0.003921568627450966, -0.003921568627450966, -0.003921568627450966 }; auto updateStateAndPublish = [&] { for (size_t i = 0; i < expectedButtons.size(); ++i) gamepad->setButtonValue(i, expectedButtons[i]); for (size_t i = 0; i < 5; ++i) gamepad->setAxisValue(i, expectedAxes[i]); gamepad->publishReport(); }; updateStateAndPublish(); // Wait for the page to tell us a gamepad connected Util::run(&didReceiveMessage); didReceiveMessage = false; EXPECT_EQ(messageHandler.get().messages.size(), 1u); EXPECT_TRUE([messageHandler.get().messages[0] isEqualToString:@"\"79-11-Virtual Shenzhen Longshengwei Technology Gamepad\""]); bool done = false; bool gotNewValues = false; auto resultBlock = [&] (id result, NSError *error) { EXPECT_NULL(error); EXPECT_TRUE([result[@"gamepadCount"] isEqualToNumber:@(1)]); bool areEqual = true; for (size_t i = 0; i < 10; ++i) { if (!WTF::areEssentiallyEqual([(NSNumber *)result[@"gamepadButtons"][0][i] doubleValue], expectedButtons[i])) areEqual = false; } for (size_t i = 0; i < 5; ++i) { if (!WTF::areEssentiallyEqual([(NSNumber *)result[@"gamepadAxes"][0][i] doubleValue], expectedAxes[i])) areEqual = false; } if (areEqual) gotNewValues = true; done = true; }; // Change some buttons, polling state to confirm NSDate *start = [NSDate date]; while (!gotNewValues) { [webView callAsyncJavaScript:@(pollGamepadStateFunction) arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:resultBlock]; Util::run(&done); done = false; if ([[NSDate date] timeIntervalSinceDate:start] > 1.0) break; } EXPECT_TRUE(gotNewValues); gotNewValues = false; expectedButtons = { 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0 }; updateStateAndPublish(); start = [NSDate date]; while (!gotNewValues) { [webView callAsyncJavaScript:@(pollGamepadStateFunction) arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:resultBlock]; Util::run(&done); done = false; if ([[NSDate date] timeIntervalSinceDate:start] > 1.0) break; } EXPECT_TRUE(gotNewValues); gotNewValues = false; expectedAxes = { -1.0, -1.0, -1.0, -1.0, 1.0 }; updateStateAndPublish(); // Even though we set -1.0 for each "X" axis, the first 3 axes on this controller are always constant expectedAxes = { -0.9921568627450981, -0.003921568627450966, -0.003921568627450966, -1.0, 1.0 }; start = [NSDate date]; while (!gotNewValues) { [webView callAsyncJavaScript:@(pollGamepadStateFunction) arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:resultBlock]; Util::run(&done); done = false; if ([[NSDate date] timeIntervalSinceDate:start] > 1.0) break; } EXPECT_TRUE(gotNewValues); gotNewValues = false; // Disconnect the gamepad gamepad = nullptr; Util::run(&didReceiveMessage); didReceiveMessage = false; EXPECT_EQ(messageHandler.get().messages.size(), 2u); EXPECT_TRUE([messageHandler.get().messages[1] isEqualToString:@"Disconnect: \"79-11-Virtual Shenzhen Longshengwei Technology Gamepad\""]); } TEST(Gamepad, Dualshock3Basic) { auto keyWindowSwizzler = makeUnique([NSApplication class], @selector(keyWindow), reinterpret_cast(getKeyWindowForTesting)); auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto messageHandler = adoptNS([[GamepadMessageHandler alloc] init]); [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"gamepad"]; auto schemeHandler = adoptNS([[TestURLSchemeHandler alloc] init]); [configuration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"gamepad"]; [schemeHandler setStartURLSchemeTaskHandler:^(WKWebView *, id task) { auto response = adoptNS([[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:0 textEncodingName:nil]); [task didReceiveResponse:response.get()]; [task didReceiveData:[NSData dataWithBytes:mainBytes length:strlen(mainBytes)]]; [task didFinish]; }]; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); keyWindowForTesting = [webView window]; [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"gamepad://host/main.html"]]]; [[webView window] makeFirstResponder:webView.get()]; // Resigning/reinstating the key window state triggers the "key window did change" notification that WKWebView currently // needs to convince it to monitor gamepad devices [[webView window] resignKeyWindow]; [[webView window] makeKeyWindow]; // Connect a gamepad and make it visible to the page auto gamepad = makeUnique(VirtualGamepad::sonyDualshock3Mapping()); while (![webView.get().configuration.processPool _numberOfConnectedGamepadsForTesting]) Util::sleep(0.01); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedHIDGamepadsForTesting], 1u); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedGameControllerFrameworkGamepadsForTesting], 0u); gamepad->setButtonValue(0, 0.75); gamepad->publishReport(); // Wait for the page to tell us a gamepad connected Util::run(&didReceiveMessage); didReceiveMessage = false; EXPECT_EQ(messageHandler.get().messages.size(), 1u); EXPECT_TRUE([messageHandler.get().messages[0] isEqualToString:@"\"54c-268-Virtual Dualshock3\""]); bool done = false; auto resultBlock = [&] (id result, NSError *error) { EXPECT_NULL(error); EXPECT_TRUE([result[@"gamepadCount"] isEqualToNumber:@(1)]); EXPECT_EQ(((NSArray *)result[@"gamepadButtons"][0]).count, 17u); EXPECT_EQ(((NSArray *)result[@"gamepadAxes"][0]).count, 4u); done = true; }; [webView callAsyncJavaScript:@(pollGamepadStateFunction) arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:resultBlock]; Util::run(&done); didReceiveMessage = true; } TEST(Gamepad, Stadia) { auto keyWindowSwizzler = makeUnique([NSApplication class], @selector(keyWindow), reinterpret_cast(getKeyWindowForTesting)); auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto messageHandler = adoptNS([[GamepadMessageHandler alloc] init]); [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"gamepad"]; auto schemeHandler = adoptNS([[TestURLSchemeHandler alloc] init]); [configuration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"gamepad"]; [schemeHandler setStartURLSchemeTaskHandler:^(WKWebView *, id task) { auto response = adoptNS([[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:0 textEncodingName:nil]); [task didReceiveResponse:response.get()]; [task didReceiveData:[NSData dataWithBytes:mainBytes length:strlen(mainBytes)]]; [task didFinish]; }]; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); keyWindowForTesting = [webView window]; [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"gamepad://host/main.html"]]]; [[webView window] makeFirstResponder:webView.get()]; // Resigning/reinstating the key window state triggers the "key window did change" notification that WKWebView currently // needs to convince it to monitor gamepad devices [[webView window] resignKeyWindow]; [[webView window] makeKeyWindow]; // Connect a gamepad and make it visible to the page auto gamepad = makeUnique(VirtualGamepad::googleStadiaMapping()); while (![webView.get().configuration.processPool _numberOfConnectedGamepadsForTesting]) Util::sleep(0.01); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedHIDGamepadsForTesting], 1u); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedGameControllerFrameworkGamepadsForTesting], 0u); gamepad->setButtonValue(0, 1.0); gamepad->setButtonValue(1, 1.0); gamepad->setButtonValue(2, 1.0); gamepad->setButtonValue(3, 1.0); gamepad->setButtonValue(4, 1.0); gamepad->setButtonValue(5, 1.0); gamepad->publishReport(); // Wait for the page to tell us a gamepad connected Util::run(&didReceiveMessage); didReceiveMessage = false; EXPECT_EQ(messageHandler.get().messages.size(), 1u); EXPECT_TRUE([messageHandler.get().messages[0] isEqualToString:@"\"18d1-9400-Virtual Stadia\""]); bool done = false; auto resultBlock = [&] (id result, NSError *error) { EXPECT_NULL(error); EXPECT_TRUE([result[@"gamepadCount"] isEqualToNumber:@(1)]); EXPECT_EQ(((NSArray *)result[@"gamepadButtons"][0]).count, 19u); EXPECT_EQ(((NSArray *)result[@"gamepadAxes"][0]).count, 4u); done = true; }; [webView callAsyncJavaScript:@(pollGamepadStateFunction) arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:resultBlock]; Util::run(&done); didReceiveMessage = true; } TEST(Gamepad, LogitechBasic) { auto keyWindowSwizzler = makeUnique([NSApplication class], @selector(keyWindow), reinterpret_cast(getKeyWindowForTesting)); auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto messageHandler = adoptNS([[GamepadMessageHandler alloc] init]); [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"gamepad"]; auto schemeHandler = adoptNS([[TestURLSchemeHandler alloc] init]); [configuration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"gamepad"]; [schemeHandler setStartURLSchemeTaskHandler:^(WKWebView *, id task) { auto response = adoptNS([[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:0 textEncodingName:nil]); [task didReceiveResponse:response.get()]; [task didReceiveData:[NSData dataWithBytes:mainBytes length:strlen(mainBytes)]]; [task didFinish]; }]; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); keyWindowForTesting = [webView window]; [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"gamepad://host/main.html"]]]; [[webView window] makeFirstResponder:webView.get()]; // Resigning/reinstating the key window state triggers the "key window did change" notification that WKWebView currently // needs to convince it to monitor gamepad devices [[webView window] resignKeyWindow]; [[webView window] makeKeyWindow]; [[webView window] makeFirstResponder:webView.get()]; // Connect a gamepad and make it visible to the page auto gamepad1 = makeUnique(VirtualGamepad::logitechF310Mapping()); auto gamepad2 = makeUnique(VirtualGamepad::logitechF710Mapping()); while ([webView.get().configuration.processPool _numberOfConnectedGamepadsForTesting] != 2u) Util::sleep(0.01); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedHIDGamepadsForTesting], 2u); EXPECT_EQ([webView.get().configuration.processPool _numberOfConnectedGameControllerFrameworkGamepadsForTesting], 0u); gamepad1->setButtonValue(0, 1.0); gamepad2->setButtonValue(0, 1.0); gamepad1->publishReport(); gamepad2->publishReport(); // The page messages back once for each controller while (messageHandler.get().messages.size() < 2) { didReceiveMessage = false; Util::run(&didReceiveMessage); } EXPECT_EQ(messageHandler.get().messages.size(), 2u); EXPECT_TRUE([messageHandler.get().messages[0] isEqualToString:@"\"46d-c216-Virtual Logitech F310\""]); NSSet *expectedGamepadNames = [NSSet setWithArray:@[ @"\"46d-c216-Virtual Logitech F310\"", @"\"46d-c219-Virtual Logitech F710\""]]; for (size_t i = 0; i < 2; ++i) EXPECT_TRUE([expectedGamepadNames containsObject:messageHandler.get().messages[i].get()]); bool done = false; auto resultBlock = [&] (id result, NSError *error) { EXPECT_NULL(error); EXPECT_TRUE([result[@"gamepadCount"] isEqualToNumber:@(2)]); EXPECT_EQ(((NSArray *)result[@"gamepadButtons"][0]).count, 16u); EXPECT_EQ(((NSArray *)result[@"gamepadButtons"][1]).count, 16u); EXPECT_EQ(((NSArray *)result[@"gamepadAxes"][0]).count, 4u); EXPECT_EQ(((NSArray *)result[@"gamepadAxes"][1]).count, 4u); done = true; }; [webView callAsyncJavaScript:@(pollGamepadStateFunction) arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:resultBlock]; Util::run(&done); didReceiveMessage = true; } TEST(Gamepad, FullInfoAfterConnection) { auto keyWindowSwizzler = makeUnique([NSApplication class], @selector(keyWindow), reinterpret_cast(getKeyWindowForTesting)); auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto messageHandler = adoptNS([[GamepadMessageHandler alloc] init]); [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"gamepad"]; auto schemeHandler = adoptNS([[TestURLSchemeHandler alloc] init]); [configuration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"gamepad"]; [schemeHandler setStartURLSchemeTaskHandler:^(WKWebView *, id task) { auto response = adoptNS([[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:0 textEncodingName:nil]); [task didReceiveResponse:response.get()]; [task didReceiveData:[NSData dataWithBytes:mainBytes length:strlen(mainBytes)]]; [task didFinish]; }]; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); keyWindowForTesting = [webView window]; [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"gamepad://host/main.html"]]]; [[webView window] makeFirstResponder:webView.get()]; // Resigning/reinstating the key window state triggers the "key window did change" notification that WKWebView currently // needs to convince it to monitor gamepad devices [[webView window] resignKeyWindow]; [[webView window] makeKeyWindow]; // Connect a gamepad and make it visible to the page auto gamepad1 = makeUnique(VirtualGamepad::logitechF310Mapping()); while ([webView.get().configuration.processPool _numberOfConnectedGamepadsForTesting] != 1u) Util::sleep(0.01); gamepad1->setButtonValue(0, 1.0); gamepad1->publishReport(); // Wait until the page sees the gamepad while (messageHandler.get().messages.size() < 1) { didReceiveMessage = false; Util::run(&didReceiveMessage); } bool done = false; auto resultBlock = [&] (id result, NSError *error) { EXPECT_NULL(error); EXPECT_TRUE([result[@"gamepadCount"] isEqualToNumber:@(1)]); EXPECT_TRUE([result[@"gamepadMapping"][0] isEqualToString:@"standard"]); done = true; }; // Verify the gamepad has the expected mapping. [webView callAsyncJavaScript:@(pollGamepadStateFunction) arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:resultBlock]; Util::run(&done); done = false; // Make a second web view to make the same gamepad visible to it. auto webView2 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); keyWindowForTesting = [webView2 window]; [webView2 synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"gamepad://host/main.html"]]]; [[webView2 window] makeFirstResponder:webView2.get()]; // The gamepad is already connected, but this new web view can't see it. // Press buttons to make it visible. gamepad1->setButtonValue(1, 1.0); gamepad1->publishReport(); // The gamepad is already connected, but this new web view can't see it. // Press buttons to make it visible. while (messageHandler.get().messages.size() < 2) { didReceiveMessage = false; Util::run(&didReceiveMessage); } // Verify the gamepad has the expected mapping in the second web view. [webView2 callAsyncJavaScript:@(pollGamepadStateFunction) arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:resultBlock]; Util::run(&done); done = false; } } // namespace TestWebKitAPI #endif // USE(APPLE_INTERNAL_SDK)