1/*
2 * Copyright (C) 2010 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "LayoutTestController.h"
28
29#include "InjectedBundle.h"
30#include "InjectedBundlePage.h"
31#include "JSLayoutTestController.h"
32#include "PlatformWebView.h"
33#include "StringFunctions.h"
34#include "TestController.h"
35#include <WebKit2/WKBundleBackForwardList.h>
36#include <WebKit2/WKBundleFrame.h>
37#include <WebKit2/WKBundleFramePrivate.h>
38#include <WebKit2/WKBundleInspector.h>
39#include <WebKit2/WKBundleNodeHandlePrivate.h>
40#include <WebKit2/WKBundlePagePrivate.h>
41#include <WebKit2/WKBundlePrivate.h>
42#include <WebKit2/WKBundleScriptWorld.h>
43#include <WebKit2/WKRetainPtr.h>
44#include <WebKit2/WebKit2.h>
45#include <wtf/HashMap.h>
46
47namespace WTR {
48
49// This is lower than DumpRenderTree's timeout, to make it easier to work through the failures
50// Eventually it should be changed to match.
51const double LayoutTestController::waitToDumpWatchdogTimerInterval = 6;
52
53static JSValueRef propertyValue(JSContextRef context, JSObjectRef object, const char* propertyName)
54{
55    if (!object)
56        return 0;
57    JSRetainPtr<JSStringRef> propertyNameString(Adopt, JSStringCreateWithUTF8CString(propertyName));
58    JSValueRef exception;
59    return JSObjectGetProperty(context, object, propertyNameString.get(), &exception);
60}
61
62static JSObjectRef propertyObject(JSContextRef context, JSObjectRef object, const char* propertyName)
63{
64    JSValueRef value = propertyValue(context, object, propertyName);
65    if (!value || !JSValueIsObject(context, value))
66        return 0;
67    return const_cast<JSObjectRef>(value);
68}
69
70static JSObjectRef getElementById(WKBundleFrameRef frame, JSStringRef elementId)
71{
72    JSContextRef context = WKBundleFrameGetJavaScriptContext(frame);
73    JSObjectRef document = propertyObject(context, JSContextGetGlobalObject(context), "document");
74    if (!document)
75        return 0;
76    JSValueRef getElementById = propertyObject(context, document, "getElementById");
77    if (!getElementById || !JSValueIsObject(context, getElementById))
78        return 0;
79    JSValueRef elementIdValue = JSValueMakeString(context, elementId);
80    JSValueRef exception;
81    JSValueRef element = JSObjectCallAsFunction(context, const_cast<JSObjectRef>(getElementById), document, 1, &elementIdValue, &exception);
82    if (!element || !JSValueIsObject(context, element))
83        return 0;
84    return const_cast<JSObjectRef>(element);
85}
86
87PassRefPtr<LayoutTestController> LayoutTestController::create()
88{
89    return adoptRef(new LayoutTestController);
90}
91
92LayoutTestController::LayoutTestController()
93    : m_whatToDump(RenderTree)
94    , m_shouldDumpAllFrameScrollPositions(false)
95    , m_shouldDumpBackForwardListsForAllWindows(false)
96    , m_shouldAllowEditing(true)
97    , m_shouldCloseExtraWindows(false)
98    , m_dumpEditingCallbacks(false)
99    , m_dumpStatusCallbacks(false)
100    , m_dumpTitleChanges(false)
101    , m_dumpPixels(true)
102    , m_dumpFullScreenCallbacks(false)
103    , m_waitToDump(false)
104    , m_testRepaint(false)
105    , m_testRepaintSweepHorizontally(false)
106    , m_willSendRequestReturnsNull(false)
107{
108    platformInitialize();
109}
110
111LayoutTestController::~LayoutTestController()
112{
113}
114
115JSClassRef LayoutTestController::wrapperClass()
116{
117    return JSLayoutTestController::layoutTestControllerClass();
118}
119
120void LayoutTestController::display()
121{
122    // FIXME: actually implement, once we want pixel tests
123}
124
125void LayoutTestController::dumpAsText()
126{
127    m_whatToDump = MainFrameText;
128    m_dumpPixels = false;
129}
130
131void LayoutTestController::waitUntilDone()
132{
133    m_waitToDump = true;
134    initializeWaitToDumpWatchdogTimerIfNeeded();
135}
136
137void LayoutTestController::waitToDumpWatchdogTimerFired()
138{
139    invalidateWaitToDumpWatchdogTimer();
140    const char* message = "FAIL: Timed out waiting for notifyDone to be called\n";
141    InjectedBundle::shared().os() << message << "\n";
142    InjectedBundle::shared().done();
143}
144
145void LayoutTestController::notifyDone()
146{
147    if (!InjectedBundle::shared().isTestRunning())
148        return;
149
150    if (m_waitToDump && !InjectedBundle::shared().topLoadingFrame())
151        InjectedBundle::shared().page()->dump();
152
153    m_waitToDump = false;
154}
155
156unsigned LayoutTestController::numberOfActiveAnimations() const
157{
158    // FIXME: Is it OK this works only for the main frame?
159    // FIXME: If this is needed only for the main frame, then why is the function on WKBundleFrame instead of WKBundlePage?
160    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
161    return WKBundleFrameGetNumberOfActiveAnimations(mainFrame);
162}
163
164bool LayoutTestController::pauseAnimationAtTimeOnElementWithId(JSStringRef animationName, double time, JSStringRef elementId)
165{
166    // FIXME: Is it OK this works only for the main frame?
167    // FIXME: If this is needed only for the main frame, then why is the function on WKBundleFrame instead of WKBundlePage?
168    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
169    return WKBundleFramePauseAnimationOnElementWithId(mainFrame, toWK(animationName).get(), toWK(elementId).get(), time);
170}
171
172void LayoutTestController::suspendAnimations()
173{
174    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
175    WKBundleFrameSuspendAnimations(mainFrame);
176}
177
178void LayoutTestController::resumeAnimations()
179{
180    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
181    WKBundleFrameResumeAnimations(mainFrame);
182}
183
184JSRetainPtr<JSStringRef> LayoutTestController::layerTreeAsText() const
185{
186    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
187    WKRetainPtr<WKStringRef> text(AdoptWK, WKBundleFrameCopyLayerTreeAsText(mainFrame));
188    return toJS(text);
189}
190
191void LayoutTestController::addUserScript(JSStringRef source, bool runAtStart, bool allFrames)
192{
193    WKRetainPtr<WKStringRef> sourceWK = toWK(source);
194    WKRetainPtr<WKBundleScriptWorldRef> scriptWorld(AdoptWK, WKBundleScriptWorldCreateWorld());
195
196    WKBundleAddUserScript(InjectedBundle::shared().bundle(), InjectedBundle::shared().pageGroup(), scriptWorld.get(), sourceWK.get(), 0, 0, 0,
197        (runAtStart ? kWKInjectAtDocumentStart : kWKInjectAtDocumentEnd),
198        (allFrames ? kWKInjectInAllFrames : kWKInjectInTopFrameOnly));
199}
200
201void LayoutTestController::addUserStyleSheet(JSStringRef source, bool allFrames)
202{
203    WKRetainPtr<WKStringRef> sourceWK = toWK(source);
204    WKRetainPtr<WKBundleScriptWorldRef> scriptWorld(AdoptWK, WKBundleScriptWorldCreateWorld());
205
206    WKBundleAddUserStyleSheet(InjectedBundle::shared().bundle(), InjectedBundle::shared().pageGroup(), scriptWorld.get(), sourceWK.get(), 0, 0, 0,
207        (allFrames ? kWKInjectInAllFrames : kWKInjectInTopFrameOnly));
208}
209
210void LayoutTestController::keepWebHistory()
211{
212    WKBundleSetShouldTrackVisitedLinks(InjectedBundle::shared().bundle(), true);
213}
214
215JSValueRef LayoutTestController::computedStyleIncludingVisitedInfo(JSValueRef element)
216{
217    // FIXME: Is it OK this works only for the main frame?
218    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
219    JSContextRef context = WKBundleFrameGetJavaScriptContext(mainFrame);
220    if (!JSValueIsObject(context, element))
221        return JSValueMakeUndefined(context);
222    JSValueRef value = WKBundleFrameGetComputedStyleIncludingVisitedInfo(mainFrame, const_cast<JSObjectRef>(element));
223    if (!value)
224        return JSValueMakeUndefined(context);
225    return value;
226}
227
228JSRetainPtr<JSStringRef> LayoutTestController::counterValueForElementById(JSStringRef elementId)
229{
230    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
231    JSObjectRef element = getElementById(mainFrame, elementId);
232    if (!element)
233        return 0;
234    WKRetainPtr<WKStringRef> value(AdoptWK, WKBundleFrameCopyCounterValue(mainFrame, const_cast<JSObjectRef>(element)));
235    return toJS(value);
236}
237
238JSRetainPtr<JSStringRef> LayoutTestController::markerTextForListItem(JSValueRef element)
239{
240    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
241    JSContextRef context = WKBundleFrameGetJavaScriptContext(mainFrame);
242    if (!element || !JSValueIsObject(context, element))
243        return 0;
244    WKRetainPtr<WKStringRef> text(AdoptWK, WKBundleFrameCopyMarkerText(mainFrame, const_cast<JSObjectRef>(element)));
245    if (WKStringIsEmpty(text.get()))
246        return 0;
247    return toJS(text);
248}
249
250void LayoutTestController::execCommand(JSStringRef name, JSStringRef argument)
251{
252    WKBundlePageExecuteEditingCommand(InjectedBundle::shared().page()->page(), toWK(name).get(), toWK(argument).get());
253}
254
255bool LayoutTestController::findString(JSStringRef target, JSValueRef optionsArrayAsValue)
256{
257    WKFindOptions options = 0;
258
259    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
260    JSContextRef context = WKBundleFrameGetJavaScriptContext(mainFrame);
261    JSRetainPtr<JSStringRef> lengthPropertyName(Adopt, JSStringCreateWithUTF8CString("length"));
262    JSObjectRef optionsArray = JSValueToObject(context, optionsArrayAsValue, 0);
263    JSValueRef lengthValue = JSObjectGetProperty(context, optionsArray, lengthPropertyName.get(), 0);
264    if (!JSValueIsNumber(context, lengthValue))
265        return false;
266
267    size_t length = static_cast<size_t>(JSValueToNumber(context, lengthValue, 0));
268    for (size_t i = 0; i < length; ++i) {
269        JSValueRef value = JSObjectGetPropertyAtIndex(context, optionsArray, i, 0);
270        if (!JSValueIsString(context, value))
271            continue;
272
273        JSRetainPtr<JSStringRef> optionName(Adopt, JSValueToStringCopy(context, value, 0));
274
275        if (JSStringIsEqualToUTF8CString(optionName.get(), "CaseInsensitive"))
276            options |= kWKFindOptionsCaseInsensitive;
277        else if (JSStringIsEqualToUTF8CString(optionName.get(), "AtWordStarts"))
278            options |= kWKFindOptionsAtWordStarts;
279        else if (JSStringIsEqualToUTF8CString(optionName.get(), "TreatMedialCapitalAsWordStart"))
280            options |= kWKFindOptionsTreatMedialCapitalAsWordStart;
281        else if (JSStringIsEqualToUTF8CString(optionName.get(), "Backwards"))
282            options |= kWKFindOptionsBackwards;
283        else if (JSStringIsEqualToUTF8CString(optionName.get(), "WrapAround"))
284            options |= kWKFindOptionsWrapAround;
285        else if (JSStringIsEqualToUTF8CString(optionName.get(), "StartInSelection")) {
286            // FIXME: No kWKFindOptionsStartInSelection.
287        }
288    }
289
290    return WKBundlePageFindString(InjectedBundle::shared().page()->page(), toWK(target).get(), options);
291}
292
293void LayoutTestController::clearAllDatabases()
294{
295    WKBundleClearAllDatabases(InjectedBundle::shared().bundle());
296}
297
298void LayoutTestController::setDatabaseQuota(uint64_t quota)
299{
300    return WKBundleSetDatabaseQuota(InjectedBundle::shared().bundle(), quota);
301}
302
303bool LayoutTestController::isCommandEnabled(JSStringRef name)
304{
305    return WKBundlePageIsEditingCommandEnabled(InjectedBundle::shared().page()->page(), toWK(name).get());
306}
307
308void LayoutTestController::setCanOpenWindows(bool)
309{
310    // It's not clear if or why any tests require opening windows be forbidden.
311    // For now, just ignore this setting, and if we find later it's needed we can add it.
312}
313
314void LayoutTestController::setXSSAuditorEnabled(bool enabled)
315{
316    WKBundleOverrideXSSAuditorEnabledForTestRunner(InjectedBundle::shared().bundle(), InjectedBundle::shared().pageGroup(), true);
317}
318
319void LayoutTestController::setAllowUniversalAccessFromFileURLs(bool enabled)
320{
321    WKBundleOverrideAllowUniversalAccessFromFileURLsForTestRunner(InjectedBundle::shared().bundle(), InjectedBundle::shared().pageGroup(), enabled);
322}
323
324void LayoutTestController::setAllowFileAccessFromFileURLs(bool enabled)
325{
326    WKBundleSetAllowFileAccessFromFileURLs(InjectedBundle::shared().bundle(), InjectedBundle::shared().pageGroup(), enabled);
327}
328
329int LayoutTestController::numberOfPages(double pageWidthInPixels, double pageHeightInPixels)
330{
331    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
332    return WKBundleNumberOfPages(InjectedBundle::shared().bundle(), mainFrame, pageWidthInPixels, pageHeightInPixels);
333}
334
335int LayoutTestController::pageNumberForElementById(JSStringRef id, double pageWidthInPixels, double pageHeightInPixels)
336{
337    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
338    return WKBundlePageNumberForElementById(InjectedBundle::shared().bundle(), mainFrame, toWK(id).get(), pageWidthInPixels, pageHeightInPixels);
339}
340
341JSRetainPtr<JSStringRef> LayoutTestController::pageSizeAndMarginsInPixels(int pageIndex, int width, int height, int marginTop, int marginRight, int marginBottom, int marginLeft)
342{
343    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
344    return toJS(WKBundlePageSizeAndMarginsInPixels(InjectedBundle::shared().bundle(), mainFrame, pageIndex, width, height, marginTop, marginRight, marginBottom, marginLeft));
345}
346
347bool LayoutTestController::isPageBoxVisible(int pageIndex)
348{
349    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
350    return WKBundleIsPageBoxVisible(InjectedBundle::shared().bundle(), mainFrame, pageIndex);
351}
352
353unsigned LayoutTestController::windowCount()
354{
355    return InjectedBundle::shared().pageCount();
356}
357
358JSValueRef LayoutTestController::shadowRoot(JSValueRef element)
359{
360    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
361    JSContextRef context = WKBundleFrameGetJavaScriptContext(mainFrame);
362
363    if (!element || !JSValueIsObject(context, element))
364        return JSValueMakeNull(context);
365
366    WKRetainPtr<WKBundleNodeHandleRef> domElement = adoptWK(WKBundleNodeHandleCreate(context, const_cast<JSObjectRef>(element)));
367    if (!domElement)
368        return JSValueMakeNull(context);
369
370    WKRetainPtr<WKBundleNodeHandleRef> shadowRootDOMElement = adoptWK(WKBundleNodeHandleCopyElementShadowRoot(domElement.get()));
371    if (!shadowRootDOMElement)
372        return JSValueMakeNull(context);
373
374    return WKBundleFrameGetJavaScriptWrapperForNodeForWorld(mainFrame, shadowRootDOMElement.get(), WKBundleScriptWorldNormalWorld());
375}
376
377void LayoutTestController::clearBackForwardList()
378{
379    WKBundleBackForwardListClear(WKBundlePageGetBackForwardList(InjectedBundle::shared().page()->page()));
380}
381
382// Object Creation
383
384void LayoutTestController::makeWindowObject(JSContextRef context, JSObjectRef windowObject, JSValueRef* exception)
385{
386    setProperty(context, windowObject, "layoutTestController", this, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete, exception);
387}
388
389void LayoutTestController::showWebInspector()
390{
391    WKBundleInspectorShow(WKBundlePageGetInspector(InjectedBundle::shared().page()->page()));
392}
393
394void LayoutTestController::closeWebInspector()
395{
396    WKBundleInspectorClose(WKBundlePageGetInspector(InjectedBundle::shared().page()->page()));
397}
398
399void LayoutTestController::evaluateInWebInspector(long callID, JSStringRef script)
400{
401    WKRetainPtr<WKStringRef> scriptWK = toWK(script);
402    WKBundleInspectorEvaluateScriptForTest(WKBundlePageGetInspector(InjectedBundle::shared().page()->page()), callID, scriptWK.get());
403}
404
405void LayoutTestController::setTimelineProfilingEnabled(bool enabled)
406{
407    WKBundleInspectorSetPageProfilingEnabled(WKBundlePageGetInspector(InjectedBundle::shared().page()->page()), enabled);
408}
409
410typedef WTF::HashMap<unsigned, WKRetainPtr<WKBundleScriptWorldRef> > WorldMap;
411static WorldMap& worldMap()
412{
413    static WorldMap& map = *new WorldMap;
414    return map;
415}
416
417unsigned LayoutTestController::worldIDForWorld(WKBundleScriptWorldRef world)
418{
419    WorldMap::const_iterator end = worldMap().end();
420    for (WorldMap::const_iterator it = worldMap().begin(); it != end; ++it) {
421        if (it->second == world)
422            return it->first;
423    }
424
425    return 0;
426}
427
428void LayoutTestController::evaluateScriptInIsolatedWorld(JSContextRef context, unsigned worldID, JSStringRef script)
429{
430    // A worldID of 0 always corresponds to a new world. Any other worldID corresponds to a world
431    // that is created once and cached forever.
432    WKRetainPtr<WKBundleScriptWorldRef> world;
433    if (!worldID)
434        world.adopt(WKBundleScriptWorldCreateWorld());
435    else {
436        WKRetainPtr<WKBundleScriptWorldRef>& worldSlot = worldMap().add(worldID, 0).first->second;
437        if (!worldSlot)
438            worldSlot.adopt(WKBundleScriptWorldCreateWorld());
439        world = worldSlot;
440    }
441
442    WKBundleFrameRef frame = WKBundleFrameForJavaScriptContext(context);
443    if (!frame)
444        frame = WKBundlePageGetMainFrame(InjectedBundle::shared().page()->page());
445
446    JSGlobalContextRef jsContext = WKBundleFrameGetJavaScriptContextForWorld(frame, world.get());
447    JSEvaluateScript(jsContext, script, 0, 0, 0, 0);
448}
449
450void LayoutTestController::setPOSIXLocale(JSStringRef locale)
451{
452    char localeBuf[32];
453    JSStringGetUTF8CString(locale, localeBuf, sizeof(localeBuf));
454    setlocale(LC_ALL, localeBuf);
455}
456
457} // namespace WTR
458