1/*
2 * Copyright (C) 2017 Igalia S.L.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2,1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB. If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20#include "config.h"
21
22#include "TestMain.h"
23#include <gio/gio.h>
24#include <wtf/UUID.h>
25#include <wtf/text/StringBuilder.h>
26
27class AutomationTest: public Test {
28public:
29 MAKE_GLIB_TEST_FIXTURE(AutomationTest);
30
31 AutomationTest()
32 : m_mainLoop(adoptGRef(g_main_loop_new(nullptr, TRUE)))
33 {
34 g_dbus_connection_new_for_address("tcp:host=127.0.0.1,port=2229",
35 G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, nullptr, nullptr, [](GObject*, GAsyncResult* result, gpointer userData) {
36 GRefPtr<GDBusConnection> connection = adoptGRef(g_dbus_connection_new_for_address_finish(result, nullptr));
37 static_cast<AutomationTest*>(userData)->setConnection(WTFMove(connection));
38 }, this);
39 g_main_loop_run(m_mainLoop.get());
40 }
41
42 ~AutomationTest()
43 {
44 }
45
46 struct Target {
47 Target() = default;
48 Target(guint64 id, CString name, bool isPaired)
49 : id(id)
50 , name(name)
51 , isPaired(isPaired)
52 {
53 }
54
55 guint64 id { 0 };
56 CString name;
57 bool isPaired { false };
58 };
59
60 const GDBusInterfaceVTable s_interfaceVTable = {
61 // method_call
62 [](GDBusConnection* connection, const gchar* sender, const gchar* objectPath, const gchar* interfaceName, const gchar* methodName, GVariant* parameters, GDBusMethodInvocation* invocation, gpointer userData) {
63 auto* test = static_cast<AutomationTest*>(userData);
64 if (!g_strcmp0(methodName, "SetTargetList")) {
65 guint64 connectionID;
66 GUniqueOutPtr<GVariantIter> iter;
67 g_variant_get(parameters, "(ta(tsssb))", &connectionID, &iter.outPtr());
68 guint64 targetID;
69 const char* type;
70 const char* name;
71 const char* dummy;
72 gboolean isPaired;
73 while (g_variant_iter_loop(iter.get(), "(t&s&s&sb)", &targetID, &type, &name, &dummy, &isPaired)) {
74 if (!g_strcmp0(type, "Automation")) {
75 test->setTarget(connectionID, Target(targetID, name, isPaired));
76 break;
77 }
78 }
79 g_dbus_method_invocation_return_value(invocation, nullptr);
80 } else if (!g_strcmp0(methodName, "SendMessageToFrontend")) {
81 guint64 connectionID, targetID;
82 const char* message;
83 g_variant_get(parameters, "(tt&s)", &connectionID, &targetID, &message);
84 test->receivedMessage(connectionID, targetID, message);
85 g_dbus_method_invocation_return_value(invocation, nullptr);
86 }
87 },
88 // get_property
89 nullptr,
90 // set_property
91 nullptr,
92 // padding
93 { 0 }
94 };
95
96 void registerDBusObject()
97 {
98 static const char introspectionXML[] =
99 "<node>"
100 " <interface name='org.webkit.RemoteInspectorClient'>"
101 " <method name='SetTargetList'>"
102 " <arg type='t' name='connectionID' direction='in'/>"
103 " <arg type='a(tsssb)' name='list' direction='in'/>"
104 " </method>"
105 " <method name='SendMessageToFrontend'>"
106 " <arg type='t' name='connectionID' direction='in'/>"
107 " <arg type='t' name='target' direction='in'/>"
108 " <arg type='s' name='message' direction='in'/>"
109 " </method>"
110 " </interface>"
111 "</node>";
112 static GDBusNodeInfo* introspectionData = nullptr;
113 if (!introspectionData)
114 introspectionData = g_dbus_node_info_new_for_xml(introspectionXML, nullptr);
115 g_dbus_connection_register_object(m_connection.get(), "/org/webkit/RemoteInspectorClient", introspectionData->interfaces[0], &s_interfaceVTable, this, nullptr, nullptr);
116 }
117
118 void setConnection(GRefPtr<GDBusConnection>&& connection)
119 {
120 g_assert_true(G_IS_DBUS_CONNECTION(connection.get()));
121 m_connection = WTFMove(connection);
122 registerDBusObject();
123 g_main_loop_quit(m_mainLoop.get());
124 }
125
126 void setTarget(guint64 connectionID, Target&& target)
127 {
128 bool newConnection = !m_connectionID;
129 bool wasPaired = m_target.isPaired;
130 m_connectionID = connectionID;
131 m_target = WTFMove(target);
132 if (newConnection || (!wasPaired && m_target.isPaired))
133 g_main_loop_quit(m_mainLoop.get());
134 }
135
136 void receivedMessage(guint64 connectionID, guint64 targetID, const char* message)
137 {
138 g_assert_cmpuint(connectionID, ==, m_connectionID);
139 g_assert_cmpuint(targetID, ==, m_target.id);
140 m_message = message;
141 g_main_loop_quit(m_mainLoop.get());
142 }
143
144 void sendCommandToBackend(const String& command, const String& parameters = String())
145 {
146 static long sequenceID = 0;
147 StringBuilder messageBuilder;
148 messageBuilder.appendLiteral("{\"id\":");
149 messageBuilder.appendNumber(++sequenceID);
150 messageBuilder.appendLiteral(",\"method\":\"Automation.");
151 messageBuilder.append(command);
152 messageBuilder.append('"');
153 if (!parameters.isNull()) {
154 messageBuilder.appendLiteral(",\"params\":");
155 messageBuilder.append(parameters);
156 }
157 messageBuilder.append('}');
158 g_dbus_connection_call(m_connection.get(), nullptr, "/org/webkit/Inspector", "org.webkit.Inspector",
159 "SendMessageToBackend", g_variant_new("(tts)", m_connectionID, m_target.id, messageBuilder.toString().utf8().data()),
160 nullptr, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, nullptr, nullptr, nullptr);
161 }
162
163 static WebKitWebView* createWebViewCallback(WebKitAutomationSession* session, AutomationTest* test)
164 {
165 test->m_createWebViewWasCalled = true;
166 return test->m_webViewForAutomation;
167 }
168
169 void automationStarted(WebKitAutomationSession* session)
170 {
171 m_session = session;
172 assertObjectIsDeletedWhenTestFinishes(G_OBJECT(m_session));
173 g_assert_null(webkit_automation_session_get_application_info(session));
174 WebKitApplicationInfo* info = webkit_application_info_new();
175 webkit_application_info_set_name(info, "AutomationTestBrowser");
176 webkit_application_info_set_version(info, WEBKIT_MAJOR_VERSION, WEBKIT_MINOR_VERSION, WEBKIT_MICRO_VERSION);
177 webkit_automation_session_set_application_info(session, info);
178 webkit_application_info_unref(info);
179 g_assert_true(webkit_automation_session_get_application_info(session) == info);
180 }
181
182 static void automationStartedCallback(WebKitWebContext* webContext, WebKitAutomationSession* session, AutomationTest* test)
183 {
184 g_assert_true(webContext == test->m_webContext.get());
185 g_assert_true(WEBKIT_IS_AUTOMATION_SESSION(session));
186 test->automationStarted(session);
187 }
188
189 static GUniquePtr<char> toVersionString(unsigned major, unsigned minor, unsigned micro)
190 {
191 if (!micro && !minor)
192 return GUniquePtr<char>(g_strdup_printf("%u", major));
193
194 if (!micro)
195 return GUniquePtr<char>(g_strdup_printf("%u.%u", major, minor));
196
197 return GUniquePtr<char>(g_strdup_printf("%u.%u.%u", major, minor, micro));
198 }
199
200 WebKitAutomationSession* requestSession(const char* sessionID)
201 {
202 auto signalID = g_signal_connect(m_webContext.get(), "automation-started", G_CALLBACK(automationStartedCallback), this);
203 g_dbus_connection_call(m_connection.get(), nullptr, "/org/webkit/Inspector", "org.webkit.Inspector",
204 "StartAutomationSession", g_variant_new("(sa{sv})", sessionID, nullptr), nullptr, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, nullptr,
205 [](GObject* source, GAsyncResult* result, gpointer userData) {
206 auto* test = static_cast<AutomationTest*>(userData);
207 if (!test->m_session)
208 return;
209
210 GRefPtr<GVariant> capabilities = adoptGRef(g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, nullptr));
211 g_assert_nonnull(capabilities.get());
212 const char* browserName;
213 const char* browserVersion;
214 g_variant_get(capabilities.get(), "(&s&s)", &browserName, &browserVersion);
215 g_assert_cmpstr(browserName, ==, "AutomationTestBrowser");
216 GUniquePtr<char> versionString = toVersionString(WEBKIT_MAJOR_VERSION, WEBKIT_MINOR_VERSION, WEBKIT_MICRO_VERSION);
217 g_assert_cmpstr(browserVersion, ==, versionString.get());
218 }, this
219 );
220 auto timeoutID = g_timeout_add(1000, [](gpointer userData) -> gboolean {
221 g_main_loop_quit(static_cast<GMainLoop*>(userData));
222 return G_SOURCE_REMOVE;
223 }, m_mainLoop.get());
224 g_main_loop_run(m_mainLoop.get());
225 if (!m_connectionID)
226 m_session = nullptr;
227 if (m_session && m_connectionID)
228 g_source_remove(timeoutID);
229 g_signal_handler_disconnect(m_webContext.get(), signalID);
230 return m_session;
231 }
232
233 void setupIfNeeded()
234 {
235 if (m_target.isPaired)
236 return;
237 g_assert_cmpuint(m_target.id, !=, 0);
238 g_dbus_connection_call(m_connection.get(), nullptr, "/org/webkit/Inspector", "org.webkit.Inspector",
239 "Setup", g_variant_new("(tt)", m_connectionID, m_target.id), nullptr, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, nullptr, nullptr, nullptr);
240 g_main_loop_run(m_mainLoop.get());
241 g_assert_true(m_target.isPaired);
242 }
243
244 bool createTopLevelBrowsingContext(WebKitWebView* webView)
245 {
246 setupIfNeeded();
247 m_webViewForAutomation = webView;
248 m_createWebViewWasCalled = false;
249 m_message = CString();
250 auto signalID = g_signal_connect(m_session, "create-web-view", G_CALLBACK(createWebViewCallback), this);
251 sendCommandToBackend("createBrowsingContext");
252 g_main_loop_run(m_mainLoop.get());
253 g_signal_handler_disconnect(m_session, signalID);
254 g_assert_true(m_createWebViewWasCalled);
255 g_assert_false(m_message.isNull());
256 m_webViewForAutomation = nullptr;
257
258 if (strstr(m_message.data(), "The remote session failed to create a new browsing context"))
259 return false;
260 if (strstr(m_message.data(), "handle"))
261 return true;
262 return false;
263 }
264
265 GRefPtr<GMainLoop> m_mainLoop;
266 GRefPtr<GDBusConnection> m_connection;
267 WebKitAutomationSession* m_session;
268 guint64 m_connectionID { 0 };
269 Target m_target;
270
271 WebKitWebView* m_webViewForAutomation { nullptr };
272 bool m_createWebViewWasCalled { false };
273 CString m_message;
274};
275
276static void testAutomationSessionRequestSession(AutomationTest* test, gconstpointer)
277{
278 String sessionID = createCanonicalUUIDString();
279 // WebKitAutomationSession::automation-started is never emitted if automation is not enabled.
280 g_assert_false(webkit_web_context_is_automation_allowed(test->m_webContext.get()));
281 auto* session = test->requestSession(sessionID.utf8().data());
282 g_assert_null(session);
283
284 webkit_web_context_set_automation_allowed(test->m_webContext.get(), TRUE);
285 g_assert_true(webkit_web_context_is_automation_allowed(test->m_webContext.get()));
286
287 // There can't be more than one context with automation enabled
288 GRefPtr<WebKitWebContext> otherContext = adoptGRef(webkit_web_context_new());
289 test->removeLogFatalFlag(G_LOG_LEVEL_WARNING);
290 webkit_web_context_set_automation_allowed(otherContext.get(), TRUE);
291 test->addLogFatalFlag(G_LOG_LEVEL_WARNING);
292 g_assert_false(webkit_web_context_is_automation_allowed(otherContext.get()));
293
294 session = test->requestSession(sessionID.utf8().data());
295 g_assert_cmpstr(webkit_automation_session_get_id(session), ==, sessionID.utf8().data());
296 g_assert_cmpuint(test->m_target.id, >, 0);
297 ASSERT_CMP_CSTRING(test->m_target.name, ==, sessionID.utf8());
298 g_assert_false(test->m_target.isPaired);
299
300 // Will fail to create a browsing context when not creating a web view (or not handling the signal).
301 g_assert_false(test->createTopLevelBrowsingContext(nullptr));
302
303 // Will also fail if the web view is not controlled by automation.
304 auto webView = Test::adoptView(Test::createWebView(test->m_webContext.get()));
305 g_assert_false(webkit_web_view_is_controlled_by_automation(webView.get()));
306 g_assert_false(test->createTopLevelBrowsingContext(webView.get()));
307
308 // And will work with a proper web view.
309 webView = Test::adoptView(g_object_new(WEBKIT_TYPE_WEB_VIEW,
310#if PLATFORM(WPE)
311 "backend", Test::createWebViewBackend(),
312#endif
313 "web-context", test->m_webContext.get(),
314 "is-controlled-by-automation", TRUE,
315 nullptr));
316 g_assert_true(webkit_web_view_is_controlled_by_automation(webView.get()));
317 g_assert_true(test->createTopLevelBrowsingContext(webView.get()));
318
319 webkit_web_context_set_automation_allowed(test->m_webContext.get(), FALSE);
320}
321
322static void testAutomationSessionApplicationInfo(Test* test, gconstpointer)
323{
324 WebKitApplicationInfo* info = webkit_application_info_new();
325 g_assert_cmpstr(webkit_application_info_get_name(info), ==, g_get_prgname());
326 webkit_application_info_set_name(info, "WebKitGTKBrowser");
327 g_assert_cmpstr(webkit_application_info_get_name(info), ==, "WebKitGTKBrowser");
328 webkit_application_info_set_name(info, nullptr);
329 g_assert_cmpstr(webkit_application_info_get_name(info), ==, g_get_prgname());
330
331 guint64 major, minor, micro;
332 webkit_application_info_get_version(info, &major, nullptr, nullptr);
333 g_assert_cmpuint(major, ==, 0);
334 webkit_application_info_set_version(info, 1, 2, 3);
335 webkit_application_info_get_version(info, &major, &minor, &micro);
336 g_assert_cmpuint(major, ==, 1);
337 g_assert_cmpuint(minor, ==, 2);
338 g_assert_cmpuint(micro, ==, 3);
339
340 webkit_application_info_unref(info);
341}
342
343
344void beforeAll()
345{
346 g_setenv("WEBKIT_INSPECTOR_SERVER", "127.0.0.1:2229", TRUE);
347
348 AutomationTest::add("WebKitAutomationSession", "request-session", testAutomationSessionRequestSession);
349 Test::add("WebKitAutomationSession", "application-info", testAutomationSessionApplicationInfo);
350}
351
352void afterAll()
353{
354 g_unsetenv("WEBKIT_INSPECTOR_SERVER");
355}
356