1/*
2 * Copyright (C) 2010 Google 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 are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31#include "config.h"
32#include "TestShell.h"
33
34#include "DRTDevToolsAgent.h"
35#include "DRTDevToolsClient.h"
36#include "LayoutTestController.h"
37#include "WebDataSource.h"
38#include "WebDocument.h"
39#include "WebElement.h"
40#include "WebFrame.h"
41#include "WebHistoryItem.h"
42#include "WebKit.h"
43#include "WebRuntimeFeatures.h"
44#include "WebScriptController.h"
45#include "WebSettings.h"
46#include "WebSize.h"
47#include "WebSpeechInputControllerMock.h"
48#include "WebString.h"
49#include "WebURLRequest.h"
50#include "WebURLResponse.h"
51#include "WebView.h"
52#include "WebViewHost.h"
53#include "skia/ext/platform_canvas.h"
54#include "webkit/support/webkit_support.h"
55#include "webkit/support/webkit_support_gfx.h"
56#include <algorithm>
57#include <cctype>
58#include <vector>
59#include <wtf/MD5.h>
60
61using namespace WebKit;
62using namespace std;
63
64// Content area size for newly created windows.
65static const int testWindowWidth = 800;
66static const int testWindowHeight = 600;
67
68// The W3C SVG layout tests use a different size than the other layout tests.
69static const int SVGTestWindowWidth = 480;
70static const int SVGTestWindowHeight = 360;
71
72static const char layoutTestsPattern[] = "/LayoutTests/";
73static const string::size_type layoutTestsPatternSize = sizeof(layoutTestsPattern) - 1;
74static const char fileUrlPattern[] = "file:/";
75static const char fileTestPrefix[] = "(file test):";
76static const char dataUrlPattern[] = "data:";
77static const string::size_type dataUrlPatternSize = sizeof(dataUrlPattern) - 1;
78
79// FIXME: Move this to a common place so that it can be shared with
80// WebCore::TransparencyWin::makeLayerOpaque().
81static void makeCanvasOpaque(SkCanvas* canvas)
82{
83    const SkBitmap& bitmap = canvas->getTopDevice()->accessBitmap(true);
84    ASSERT(bitmap.config() == SkBitmap::kARGB_8888_Config);
85
86    SkAutoLockPixels lock(bitmap);
87    for (int y = 0; y < bitmap.height(); y++) {
88        uint32_t* row = bitmap.getAddr32(0, y);
89        for (int x = 0; x < bitmap.width(); x++)
90            row[x] |= 0xFF000000; // Set alpha bits to 1.
91    }
92}
93
94TestShell::TestShell(bool testShellMode)
95    : m_testIsPending(false)
96    , m_testIsPreparing(false)
97    , m_focusedWidget(0)
98    , m_testShellMode(testShellMode)
99    , m_devTools(0)
100    , m_allowExternalPages(false)
101    , m_acceleratedCompositingEnabled(false)
102    , m_forceCompositingMode(false)
103    , m_accelerated2dCanvasEnabled(false)
104    , m_stressOpt(false)
105    , m_stressDeopt(false)
106    , m_dumpWhenFinished(true)
107{
108    WebRuntimeFeatures::enableDataTransferItems(true);
109    WebRuntimeFeatures::enableGeolocation(true);
110    WebRuntimeFeatures::enableIndexedDatabase(true);
111    WebRuntimeFeatures::enableFileSystem(true);
112    WebRuntimeFeatures::enableJavaScriptI18NAPI(true);
113    m_accessibilityController.set(new AccessibilityController(this));
114    m_layoutTestController.set(new LayoutTestController(this));
115    m_eventSender.set(new EventSender(this));
116    m_plainTextController.set(new PlainTextController());
117    m_textInputController.set(new TextInputController(this));
118    m_notificationPresenter.set(new NotificationPresenter(this));
119    m_printer.set(m_testShellMode ? TestEventPrinter::createTestShellPrinter() : TestEventPrinter::createDRTPrinter());
120
121    // 30 second is the same as the value in Mac DRT.
122    // If we use a value smaller than the timeout value of
123    // (new-)run-webkit-tests, (new-)run-webkit-tests misunderstands that a
124    // timed-out DRT process was crashed.
125    m_timeout = 30 * 1000;
126
127    createMainWindow();
128}
129
130void TestShell::createMainWindow()
131{
132    m_drtDevToolsAgent.set(new DRTDevToolsAgent);
133    m_webViewHost = createNewWindow(WebURL(), m_drtDevToolsAgent.get());
134    m_webView = m_webViewHost->webView();
135    m_drtDevToolsAgent->setWebView(m_webView);
136}
137
138TestShell::~TestShell()
139{
140    // Note: DevTools are closed together with all the other windows in the
141    // windows list.
142
143    // Destroy the WebView before its WebViewHost.
144    m_drtDevToolsAgent->setWebView(0);
145}
146
147void TestShell::createDRTDevToolsClient(DRTDevToolsAgent* agent)
148{
149    m_drtDevToolsClient.set(new DRTDevToolsClient(agent, m_devTools->webView()));
150}
151
152void TestShell::showDevTools()
153{
154    if (!m_devTools) {
155        WebURL url = webkit_support::GetDevToolsPathAsURL();
156        if (!url.isValid()) {
157            ASSERT(false);
158            return;
159        }
160        m_devTools = createNewWindow(url);
161        ASSERT(m_devTools);
162        createDRTDevToolsClient(m_drtDevToolsAgent.get());
163    }
164    m_devTools->show(WebKit::WebNavigationPolicyNewWindow);
165}
166
167void TestShell::closeDevTools()
168{
169    if (m_devTools) {
170        m_drtDevToolsAgent->reset();
171        m_drtDevToolsClient.clear();
172        closeWindow(m_devTools);
173        m_devTools = 0;
174    }
175}
176
177void TestShell::resetWebSettings(WebView& webView)
178{
179    m_prefs.reset();
180    m_prefs.acceleratedCompositingEnabled = m_acceleratedCompositingEnabled;
181    m_prefs.forceCompositingMode = m_forceCompositingMode;
182    m_prefs.accelerated2dCanvasEnabled = m_accelerated2dCanvasEnabled;
183    m_prefs.applyTo(&webView);
184}
185
186void TestShell::runFileTest(const TestParams& params)
187{
188    ASSERT(params.testUrl.isValid());
189    m_testIsPreparing = true;
190    m_params = params;
191    string testUrl = m_params.testUrl.spec();
192
193    if (testUrl.find("loading/") != string::npos
194        || testUrl.find("loading\\") != string::npos)
195        m_layoutTestController->setShouldDumpFrameLoadCallbacks(true);
196
197    if (testUrl.find("/dumpAsText/") != string::npos
198        || testUrl.find("\\dumpAsText\\") != string::npos) {
199        m_layoutTestController->setShouldDumpAsText(true);
200        m_layoutTestController->setShouldGeneratePixelResults(false);
201    }
202
203    if (testUrl.find("/inspector/") != string::npos
204        || testUrl.find("\\inspector\\") != string::npos)
205        showDevTools();
206
207    if (m_params.debugLayerTree)
208        m_layoutTestController->setShowDebugLayerTree(true);
209
210    if (m_dumpWhenFinished)
211        m_printer->handleTestHeader(testUrl.c_str());
212    loadURL(m_params.testUrl);
213
214    m_testIsPreparing = false;
215    waitTestFinished();
216}
217
218static inline bool isSVGTestURL(const WebURL& url)
219{
220    return url.isValid() && string(url.spec()).find("W3C-SVG-1.1") != string::npos;
221}
222
223void TestShell::resizeWindowForTest(WebViewHost* window, const WebURL& url)
224{
225    int width, height;
226    if (isSVGTestURL(url)) {
227        width = SVGTestWindowWidth;
228        height = SVGTestWindowHeight;
229    } else {
230        width = testWindowWidth;
231        height = testWindowHeight;
232    }
233    window->setWindowRect(WebRect(0, 0, width + virtualWindowBorder * 2, height + virtualWindowBorder * 2));
234}
235
236void TestShell::resetTestController()
237{
238    resetWebSettings(*webView());
239    m_accessibilityController->reset();
240    m_layoutTestController->reset();
241    m_eventSender->reset();
242    m_webViewHost->reset();
243    m_notificationPresenter->reset();
244    m_drtDevToolsAgent->reset();
245    if (m_drtDevToolsClient)
246        m_drtDevToolsClient->reset();
247    webView()->mainFrame()->clearOpener();
248}
249
250void TestShell::loadURL(const WebURL& url)
251{
252    m_webViewHost->loadURLForFrame(url, WebString());
253}
254
255void TestShell::reload()
256{
257    m_webViewHost->navigationController()->reload();
258}
259
260void TestShell::goToOffset(int offset)
261{
262     m_webViewHost->navigationController()->goToOffset(offset);
263}
264
265int TestShell::navigationEntryCount() const
266{
267    return m_webViewHost->navigationController()->entryCount();
268}
269
270void TestShell::callJSGC()
271{
272    m_webView->mainFrame()->collectGarbage();
273}
274
275void TestShell::setFocus(WebWidget* widget, bool enable)
276{
277    // Simulate the effects of InteractiveSetFocus(), which includes calling
278    // both setFocus() and setIsActive().
279    if (enable) {
280        if (m_focusedWidget != widget) {
281            if (m_focusedWidget)
282                m_focusedWidget->setFocus(false);
283            webView()->setIsActive(enable);
284            widget->setFocus(enable);
285            m_focusedWidget = widget;
286        }
287    } else {
288        if (m_focusedWidget == widget) {
289            widget->setFocus(enable);
290            webView()->setIsActive(enable);
291            m_focusedWidget = 0;
292        }
293    }
294}
295
296void TestShell::testFinished()
297{
298    if (!m_testIsPending)
299        return;
300    m_testIsPending = false;
301    if (m_dumpWhenFinished)
302        dump();
303    webkit_support::QuitMessageLoop();
304}
305
306void TestShell::testTimedOut()
307{
308    m_printer->handleTimedOut();
309    testFinished();
310}
311
312static string dumpDocumentText(WebFrame* frame)
313{
314    // We use the document element's text instead of the body text here because
315    // not all documents have a body, such as XML documents.
316    WebElement documentElement = frame->document().documentElement();
317    if (documentElement.isNull())
318        return string();
319    return documentElement.innerText().utf8();
320}
321
322static string dumpFramesAsText(WebFrame* frame, bool recursive)
323{
324    string result;
325
326    // Add header for all but the main frame. Skip empty frames.
327    if (frame->parent() && !frame->document().documentElement().isNull()) {
328        result.append("\n--------\nFrame: '");
329        result.append(frame->name().utf8().data());
330        result.append("'\n--------\n");
331    }
332
333    result.append(dumpDocumentText(frame));
334    result.append("\n");
335
336    if (recursive) {
337        for (WebFrame* child = frame->firstChild(); child; child = child->nextSibling())
338            result.append(dumpFramesAsText(child, recursive));
339    }
340
341    return result;
342}
343
344static void dumpFrameScrollPosition(WebFrame* frame, bool recursive)
345{
346    WebSize offset = frame->scrollOffset();
347    if (offset.width > 0 || offset.height > 0) {
348        if (frame->parent())
349            printf("frame '%s' ", frame->name().utf8().data());
350        printf("scrolled to %d,%d\n", offset.width, offset.height);
351    }
352
353    if (!recursive)
354        return;
355    for (WebFrame* child = frame->firstChild(); child; child = child->nextSibling())
356        dumpFrameScrollPosition(child, recursive);
357}
358
359struct ToLower {
360    char16 operator()(char16 c) { return tolower(c); }
361};
362
363// FIXME: Eliminate std::transform(), std::vector, and std::sort().
364
365// Returns True if item1 < item2.
366static bool HistoryItemCompareLess(const WebHistoryItem& item1, const WebHistoryItem& item2)
367{
368    string16 target1 = item1.target();
369    string16 target2 = item2.target();
370    std::transform(target1.begin(), target1.end(), target1.begin(), ToLower());
371    std::transform(target2.begin(), target2.end(), target2.begin(), ToLower());
372    return target1 < target2;
373}
374
375static string dumpHistoryItem(const WebHistoryItem& item, int indent, bool isCurrent)
376{
377    string result;
378
379    if (isCurrent) {
380        result.append("curr->");
381        result.append(indent - 6, ' '); // 6 == "curr->".length()
382    } else {
383        result.append(indent, ' ');
384    }
385
386    string url = item.urlString().utf8();
387    size_t pos;
388    if (!url.find(fileUrlPattern) && ((pos = url.find(layoutTestsPattern)) != string::npos)) {
389        // adjust file URLs to match upstream results.
390        url.replace(0, pos + layoutTestsPatternSize, fileTestPrefix);
391    } else if (!url.find(dataUrlPattern)) {
392        // URL-escape data URLs to match results upstream.
393        string path = webkit_support::EscapePath(url.substr(dataUrlPatternSize));
394        url.replace(dataUrlPatternSize, url.length(), path);
395    }
396
397    result.append(url);
398    if (!item.target().isEmpty()) {
399        result.append(" (in frame \"");
400        result.append(item.target().utf8());
401        result.append("\")");
402    }
403    if (item.isTargetItem())
404        result.append("  **nav target**");
405    result.append("\n");
406
407    const WebVector<WebHistoryItem>& children = item.children();
408    if (!children.isEmpty()) {
409        // Must sort to eliminate arbitrary result ordering which defeats
410        // reproducible testing.
411        // FIXME: WebVector should probably just be a std::vector!!
412        std::vector<WebHistoryItem> sortedChildren;
413        for (size_t i = 0; i < children.size(); ++i)
414            sortedChildren.push_back(children[i]);
415        std::sort(sortedChildren.begin(), sortedChildren.end(), HistoryItemCompareLess);
416        for (size_t i = 0; i < sortedChildren.size(); ++i)
417            result += dumpHistoryItem(sortedChildren[i], indent + 4, false);
418    }
419
420    return result;
421}
422
423static void dumpBackForwardList(const TestNavigationController& navigationController, string& result)
424{
425    result.append("\n============== Back Forward List ==============\n");
426    for (int index = 0; index < navigationController.entryCount(); ++index) {
427        int currentIndex = navigationController.lastCommittedEntryIndex();
428        WebHistoryItem historyItem = navigationController.entryAtIndex(index)->contentState();
429        if (historyItem.isNull()) {
430            historyItem.initialize();
431            historyItem.setURLString(navigationController.entryAtIndex(index)->URL().spec().utf16());
432        }
433        result.append(dumpHistoryItem(historyItem, 8, index == currentIndex));
434    }
435    result.append("===============================================\n");
436}
437
438string TestShell::dumpAllBackForwardLists()
439{
440    string result;
441    for (unsigned i = 0; i < m_windowList.size(); ++i)
442        dumpBackForwardList(*m_windowList[i]->navigationController(), result);
443    return result;
444}
445
446void TestShell::dump()
447{
448    WebScriptController::flushConsoleMessages();
449
450    // Dump the requested representation.
451    WebFrame* frame = m_webView->mainFrame();
452    if (!frame)
453        return;
454    bool shouldDumpAsText = m_layoutTestController->shouldDumpAsText();
455    bool shouldGeneratePixelResults = m_layoutTestController->shouldGeneratePixelResults();
456    bool dumpedAnything = false;
457    if (m_params.dumpTree) {
458        dumpedAnything = true;
459        m_printer->handleTextHeader();
460        // Text output: the test page can request different types of output
461        // which we handle here.
462        if (!shouldDumpAsText) {
463            // Plain text pages should be dumped as text
464            string mimeType = frame->dataSource()->response().mimeType().utf8();
465            if (mimeType == "text/plain") {
466                shouldDumpAsText = true;
467                shouldGeneratePixelResults = false;
468            }
469        }
470        if (shouldDumpAsText) {
471            bool recursive = m_layoutTestController->shouldDumpChildFramesAsText();
472            string dataUtf8 = dumpFramesAsText(frame, recursive);
473            if (fwrite(dataUtf8.c_str(), 1, dataUtf8.size(), stdout) != dataUtf8.size())
474                FATAL("Short write to stdout, disk full?\n");
475        } else {
476            printf("%s", frame->renderTreeAsText(m_params.debugRenderTree).utf8().data());
477            bool recursive = m_layoutTestController->shouldDumpChildFrameScrollPositions();
478            dumpFrameScrollPosition(frame, recursive);
479        }
480        if (m_layoutTestController->shouldDumpBackForwardList())
481            printf("%s", dumpAllBackForwardLists().c_str());
482    }
483    if (dumpedAnything && m_params.printSeparators)
484        m_printer->handleTextFooter();
485
486    if (m_params.dumpPixels && shouldGeneratePixelResults) {
487        // Image output: we write the image data to the file given on the
488        // command line (for the dump pixels argument), and the MD5 sum to
489        // stdout.
490        dumpedAnything = true;
491        m_webView->layout();
492        if (m_layoutTestController->testRepaint()) {
493            WebSize viewSize = m_webView->size();
494            int width = viewSize.width;
495            int height = viewSize.height;
496            if (m_layoutTestController->sweepHorizontally()) {
497                for (WebRect column(0, 0, 1, height); column.x < width; column.x++)
498                    m_webViewHost->paintRect(column);
499            } else {
500                for (WebRect line(0, 0, width, 1); line.y < height; line.y++)
501                    m_webViewHost->paintRect(line);
502            }
503        } else
504            m_webViewHost->paintInvalidatedRegion();
505
506        // See if we need to draw the selection bounds rect. Selection bounds
507        // rect is the rect enclosing the (possibly transformed) selection.
508        // The rect should be drawn after everything is laid out and painted.
509        if (m_layoutTestController->shouldDumpSelectionRect()) {
510            // If there is a selection rect - draw a red 1px border enclosing rect
511            WebRect wr = frame->selectionBoundsRect();
512            if (!wr.isEmpty()) {
513                // Render a red rectangle bounding selection rect
514                SkPaint paint;
515                paint.setColor(0xFFFF0000); // Fully opaque red
516                paint.setStyle(SkPaint::kStroke_Style);
517                paint.setFlags(SkPaint::kAntiAlias_Flag);
518                paint.setStrokeWidth(1.0f);
519                SkIRect rect; // Bounding rect
520                rect.set(wr.x, wr.y, wr.x + wr.width, wr.y + wr.height);
521                m_webViewHost->canvas()->drawIRect(rect, paint);
522            }
523        }
524
525        dumpImage(m_webViewHost->canvas());
526    }
527    m_printer->handleImageFooter();
528    m_printer->handleTestFooter(dumpedAnything);
529    fflush(stdout);
530    fflush(stderr);
531}
532
533void TestShell::dumpImage(SkCanvas* canvas) const
534{
535    // Fix the alpha. The expected PNGs on Mac have an alpha channel, so we want
536    // to keep it. On Windows, the alpha channel is wrong since text/form control
537    // drawing may have erased it in a few places. So on Windows we force it to
538    // opaque and also don't write the alpha channel for the reference. Linux
539    // doesn't have the wrong alpha like Windows, but we match Windows.
540#if OS(MAC_OS_X)
541    bool discardTransparency = false;
542#else
543    bool discardTransparency = true;
544    makeCanvasOpaque(canvas);
545#endif
546
547    const SkBitmap& sourceBitmap = canvas->getTopDevice()->accessBitmap(false);
548    SkAutoLockPixels sourceBitmapLock(sourceBitmap);
549
550    // Compute MD5 sum.
551    MD5 digester;
552    Vector<uint8_t, 16> digestValue;
553    digester.addBytes(reinterpret_cast<const uint8_t*>(sourceBitmap.getPixels()), sourceBitmap.getSize());
554    digester.checksum(digestValue);
555    string md5hash;
556    md5hash.reserve(16 * 2);
557    for (unsigned i = 0; i < 16; ++i) {
558        char hex[3];
559        // Use "x", not "X". The string must be lowercased.
560        sprintf(hex, "%02x", digestValue[i]);
561        md5hash.append(hex);
562    }
563
564    // Only encode and dump the png if the hashes don't match. Encoding the
565    // image is really expensive.
566    if (md5hash.compare(m_params.pixelHash)) {
567        std::vector<unsigned char> png;
568        webkit_support::EncodeBGRAPNGWithChecksum(reinterpret_cast<const unsigned char*>(sourceBitmap.getPixels()), sourceBitmap.width(),
569            sourceBitmap.height(), static_cast<int>(sourceBitmap.rowBytes()), discardTransparency, md5hash, &png);
570
571        m_printer->handleImage(md5hash.c_str(), m_params.pixelHash.c_str(), &png[0], png.size(), m_params.pixelFileName.c_str());
572    } else
573        m_printer->handleImage(md5hash.c_str(), m_params.pixelHash.c_str(), 0, 0, m_params.pixelFileName.c_str());
574}
575
576void TestShell::bindJSObjectsToWindow(WebFrame* frame)
577{
578    m_accessibilityController->bindToJavascript(frame, WebString::fromUTF8("accessibilityController"));
579    m_layoutTestController->bindToJavascript(frame, WebString::fromUTF8("layoutTestController"));
580    m_eventSender->bindToJavascript(frame, WebString::fromUTF8("eventSender"));
581    m_plainTextController->bindToJavascript(frame, WebString::fromUTF8("plainText"));
582    m_textInputController->bindToJavascript(frame, WebString::fromUTF8("textInputController"));
583}
584
585WebViewHost* TestShell::createNewWindow(const WebKit::WebURL& url)
586{
587    return createNewWindow(url, 0);
588}
589
590WebViewHost* TestShell::createNewWindow(const WebKit::WebURL& url, DRTDevToolsAgent* devToolsAgent)
591{
592    WebViewHost* host = new WebViewHost(this);
593    WebView* view = WebView::create(host);
594    view->setDevToolsAgentClient(devToolsAgent);
595    host->setWebWidget(view);
596    m_prefs.applyTo(view);
597    view->initializeMainFrame(host);
598    m_windowList.append(host);
599    host->loadURLForFrame(url, WebString());
600    return host;
601}
602
603void TestShell::closeWindow(WebViewHost* window)
604{
605    size_t i = m_windowList.find(window);
606    if (i == notFound) {
607        ASSERT_NOT_REACHED();
608        return;
609    }
610    m_windowList.remove(i);
611    WebWidget* focusedWidget = m_focusedWidget;
612    if (window->webWidget() == m_focusedWidget)
613        focusedWidget = 0;
614
615    delete window;
616    // We set the focused widget after deleting the web view host because it
617    // can change the focus.
618    m_focusedWidget = focusedWidget;
619    if (m_focusedWidget) {
620        webView()->setIsActive(true);
621        m_focusedWidget->setFocus(true);
622    }
623}
624
625void TestShell::closeRemainingWindows()
626{
627    // Just close devTools window manually because we have custom deinitialization code for it.
628    closeDevTools();
629
630    // Iterate through the window list and close everything except the main
631    // window. We don't want to delete elements as we're iterating, so we copy
632    // to a temp vector first.
633    Vector<WebViewHost*> windowsToDelete;
634    for (unsigned i = 0; i < m_windowList.size(); ++i) {
635        if (m_windowList[i] != webViewHost())
636            windowsToDelete.append(m_windowList[i]);
637    }
638    ASSERT(windowsToDelete.size() + 1 == m_windowList.size());
639    for (unsigned i = 0; i < windowsToDelete.size(); ++i)
640        closeWindow(windowsToDelete[i]);
641    ASSERT(m_windowList.size() == 1);
642}
643
644int TestShell::windowCount()
645{
646    return m_windowList.size();
647}
648