Web Inspector: print the target of `console.screenshot` last so the target is the...
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 24 Jul 2019 21:39:32 +0000 (21:39 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 24 Jul 2019 21:39:32 +0000 (21:39 +0000)
https://bugs.webkit.org/show_bug.cgi?id=199308

Reviewed by Joseph Pecoraro.

Source/JavaScriptCore:

* inspector/ConsoleMessage.h:
(Inspector::ConsoleMessage::arguments const):

* inspector/ScriptArguments.h:
* inspector/ScriptArguments.cpp:
(Inspector::ScriptArguments::getFirstArgumentAsString const): Added.
(Inspector::ScriptArguments::getFirstArgumentAsString): Deleted.

Source/WebCore:

Right now, evaluating `console.screenshot(document, "test", 1);` will log a message to the
console with `#document`, `"test"`, and `1`, all on different lines (togglable by a
disclosure triangle) since `#document` isn't stringifiable.

The ideal situation would be to have `"test"` and `1` logged on the same line, and then have
`#document` be in a disclosure triangle. This way, you can "label" your images using
additional arguments (e.g. `console.screenshot(document.images[1], "second image");`), as
well as provide other data.

If the only argument was the target, it should print as if it was `console.log(target);`.

If there are no arguments, it should print the text "Viewport"` before the image.

Test: inspector/console/console-screenshot.html

* page/PageConsoleClient.cpp:
(WebCore::PageConsoleClient::addMessage):
(WebCore::PageConsoleClient::screenshot):

Source/WebInspectorUI:

* UserInterface/Views/ConsoleMessageView.js:
(WI.ConsoleMessageView.prototype.render):
(WI.ConsoleMessageView.prototype._appendMessageTextAndArguments):
(WI.ConsoleMessageView.prototype._handleContextMenu):
* UserInterface/Views/ConsoleMessageView.css:
(.console-image > .console-message-body > :matches(hr, img)): Added.
(.console-image > .console-message-body > hr): Added.
(.console-image > .console-message-body > img): Added.
(.console-log-level.console-image::before): Added.
(.console-message-body > .console-image): Deleted.
(.console-log-level.console-image-container::before): Deleted.
Allow `ConsoleMessage.MessageType.Image` to be an `ConsoleMessage.MessageLevel.Error`, and
print the message (and extra parameters) in that case.
Drive-by: reorganize the switch-case so all paths have the same `default` case.
* Localizations/en.lproj/localizedStrings.js:

LayoutTests:

* inspector/console/console-expected.txt:
* inspector/console/console-screenshot.html:
* inspector/console/console-screenshot-expected.txt:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@247790 268f45cc-cd09-0410-ab3c-d52691b4dbfc

14 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/console/console-screenshot-expected.txt
LayoutTests/inspector/console/console-screenshot.html
LayoutTests/js/console-expected.txt
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/ConsoleMessage.h
Source/JavaScriptCore/inspector/ScriptArguments.cpp
Source/JavaScriptCore/inspector/ScriptArguments.h
Source/WebCore/ChangeLog
Source/WebCore/page/PageConsoleClient.cpp
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Views/ConsoleMessageView.css
Source/WebInspectorUI/UserInterface/Views/ConsoleMessageView.js

index 509590b..3f5fe96 100644 (file)
@@ -1,3 +1,14 @@
+2019-07-24  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: print the target of `console.screenshot` last so the target is the closest item to the image
+        https://bugs.webkit.org/show_bug.cgi?id=199308
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/console/console-expected.txt:
+        * inspector/console/console-screenshot.html:
+        * inspector/console/console-screenshot-expected.txt:
+
 2019-07-24  Ryan Haddad  <ryanhaddad@apple.com>
 
         Unreviewed test gardening, land expectations for rdar://53324867.
index 0e9e380..653f903 100644 (file)
@@ -1,21 +1,33 @@
 CONSOLE MESSAGE: [object HTMLDivElement]
 CONSOLE MESSAGE: [object HTMLDivElement]
-CONSOLE MESSAGE: Could not capture screenshot
+CONSOLE MESSAGE: [object HTMLDivElement]
+CONSOLE MESSAGE: Viewport
+CONSOLE MESSAGE: Viewport
 Tests for the console.screenshot API.
 
 
 == Running test suite: console.screenshot
--- Running test case: console.screenshot.SingleNode
+-- Running test case: console.screenshot.Node.SingleArgument
 PASS: The added message should be an image.
 PASS: The image should not be empty.
+PASS: The image width should be 2px.
+PASS: The image height should be 2px.
 
--- Running test case: console.screenshot.MultipleNodes
+-- Running test case: console.screenshot.Node.MultipleArguments
 PASS: The added message should be an image.
 PASS: The image should not be empty.
+PASS: The image width should be 2px.
+PASS: The image height should be 2px.
 
--- Running test case: console.screenshot.DetachedNode
+-- Running test case: console.screenshot.Node.DetachedNonScreenshotable
 PASS: Could not capture screenshot
 
+-- Running test case: console.screenshot.NonScreenshotableTarget
+PASS: The added message should be an image.
+PASS: The image should not be empty.
+PASS: The image width should be greater than 2px.
+PASS: The image height should be greater than 2px.
+
 -- Running test case: console.screenshot.NoArguments
 PASS: The added message should be an image.
 PASS: The image should not be empty.
index c3e9ef3..04f6c66 100644 (file)
@@ -4,17 +4,15 @@
 <script src="../../http/tests/inspector/resources/inspector-test.js"></script>
 <script>
 
-function createDetachedTest3()
+function createDetachedTest()
 {
     let div = document.createElement("div");
-    div.id = "test3";
+    div.id = "detached";
     return div;
 }
 
 function test()
 {
-    InspectorTest.debug();
-
     let suite = InspectorTest.createAsyncSuite("console.screenshot");
 
     function addTest({name, expression, imageMessageAddedCallback, shouldError}) {
@@ -48,37 +46,49 @@ function test()
     }
 
     addTest({
-        name: "console.screenshot.SingleNode",
-        expression: `console.screenshot(document.querySelector("#test1"))`,
+        name: "console.screenshot.Node.SingleArgument",
+        expression: `console.screenshot(document.querySelector("#testNode"))`,
         async imageMessageAddedCallback(message) {
             InspectorTest.expectNotEqual(message.messageText, "data:", "The image should not be empty.");
 
             let img = await WI.ImageUtilities.promisifyLoad(message.messageText);
-            InspectorTest.assert(img.width === 2, "The image width should be 2px.");
-            InspectorTest.assert(img.height === 2, "The image height should be 2px.");
+            InspectorTest.expectEqual(img.width, 2, "The image width should be 2px.");
+            InspectorTest.expectEqual(img.height, 2, "The image height should be 2px.");
         },
     });
 
     addTest({
-        name: "console.screenshot.MultipleNodes", 
-        expression: `console.screenshot(document.querySelector("#test2"), document.querySelector("#test1"))`,
+        name: "console.screenshot.Node.MultipleArguments",
+        expression: `console.screenshot(document.querySelector("#testNode"), "test")`,
         async imageMessageAddedCallback(message) {
             InspectorTest.expectNotEqual(message.messageText, "data:", "The image should not be empty.");
 
             let img = await WI.ImageUtilities.promisifyLoad(message.messageText);
-            InspectorTest.assert(img.width === 2, "The image width should be 2px.");
-            InspectorTest.assert(img.height === 2, "The image height should be 2px.");
+            InspectorTest.expectEqual(img.width, 2, "The image width should be 2px.");
+            InspectorTest.expectEqual(img.height, 2, "The image height should be 2px.");
         },
     });
 
     addTest({
-        name: "console.screenshot.DetachedNode",  
-        expression: `console.screenshot(createDetachedTest3())`,
+        name: "console.screenshot.Node.DetachedNonScreenshotable",
+        expression: `console.screenshot(createDetachedTest())`,
         shouldError: true,
     });
 
     addTest({
-        name: "console.screenshot.NoArguments", 
+        name: "console.screenshot.NonScreenshotableTarget",
+        expression: `console.screenshot(42)`,
+        async imageMessageAddedCallback(message) {
+            InspectorTest.expectNotEqual(message.messageText, "data:", "The image should not be empty.");
+
+            let img = await WI.ImageUtilities.promisifyLoad(message.messageText);
+            InspectorTest.expectGreaterThan(img.width, 2, "The image width should be greater than 2px.");
+            InspectorTest.expectGreaterThan(img.height, 2, "The image height should be greater than 2px.");
+        },
+    });
+
+    addTest({
+        name: "console.screenshot.NoArguments",
         expression: `console.screenshot()`,
         async imageMessageAddedCallback(message) {
             InspectorTest.expectNotEqual(message.messageText, "data:", "The image should not be empty.");
@@ -95,25 +105,18 @@ function test()
 </head>
 <body onload="runTest()">
     <p>Tests for the console.screenshot API.</p>
-    <div id="test1"></div>
-    <div id="test2"></div>
-    <div id="test3"></div>
+    <div id="testNode"></div>
     <style>
-    #test1 {
+    #testNode {
         width: 2px;
         height: 2px;
         background-color: red;
     }
-    #test2 {
+    #detached {
         width: 2px;
         height: 2px;
         background-color: blue;
     }
-    #test3 {
-        width: 2px;
-        height: 2px;
-        background-color: green;
-    }
     </style>
 </body>
 </html>
index aa3b063..ada27c0 100644 (file)
@@ -8,6 +8,15 @@ CONSOLE MESSAGE: line 28: log message
 CONSOLE MESSAGE: line 1: 1
 CONSOLE MESSAGE: line 1: 2
 CONSOLE MESSAGE: line 1: 3
+CONSOLE MESSAGE: Viewport
+CONSOLE MESSAGE: Viewport
+CONSOLE MESSAGE: Viewport
+CONSOLE MESSAGE: Viewport
+CONSOLE MESSAGE: Viewport
+CONSOLE MESSAGE: Viewport
+CONSOLE MESSAGE: Viewport
+CONSOLE MESSAGE: Viewport
+CONSOLE MESSAGE: Viewport
 PASS typeof console is "object"
 PASS console.toString() is "[object Console]"
 PASS console is console
index ebb6847..03b4be7 100644 (file)
@@ -1,3 +1,18 @@
+2019-07-24  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: print the target of `console.screenshot` last so the target is the closest item to the image
+        https://bugs.webkit.org/show_bug.cgi?id=199308
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/ConsoleMessage.h:
+        (Inspector::ConsoleMessage::arguments const):
+
+        * inspector/ScriptArguments.h:
+        * inspector/ScriptArguments.cpp:
+        (Inspector::ScriptArguments::getFirstArgumentAsString const): Added.
+        (Inspector::ScriptArguments::getFirstArgumentAsString): Deleted.
+
 2019-07-23  Justin Michaud  <justin_michaud@apple.com>
 
         Sometimes we miss removable CheckInBounds
index 7b9d0bc..62378ef 100644 (file)
@@ -75,6 +75,7 @@ public:
 
     void incrementCount() { ++m_repeatCount; }
 
+    const RefPtr<ScriptArguments>& arguments() const { return m_arguments; }
     unsigned argumentCount() const;
 
     bool isEqual(ConsoleMessage* msg) const;
index 19194e1..61fadf3 100644 (file)
@@ -66,7 +66,7 @@ JSC::ExecState* ScriptArguments::globalState() const
     return nullptr;
 }
 
-bool ScriptArguments::getFirstArgumentAsString(String& result)
+bool ScriptArguments::getFirstArgumentAsString(String& result) const
 {
     if (!argumentCount())
         return false;
index 4e443ee..607feaf 100644 (file)
@@ -53,7 +53,7 @@ public:
 
     JSC::ExecState* globalState() const;
 
-    bool getFirstArgumentAsString(String& result);
+    bool getFirstArgumentAsString(String& result) const;
     bool isEqual(const ScriptArguments&) const;
 
 private:
index 38a26f6..7364e06 100644 (file)
@@ -1,3 +1,29 @@
+2019-07-24  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: print the target of `console.screenshot` last so the target is the closest item to the image
+        https://bugs.webkit.org/show_bug.cgi?id=199308
+
+        Reviewed by Joseph Pecoraro.
+
+        Right now, evaluating `console.screenshot(document, "test", 1);` will log a message to the
+        console with `#document`, `"test"`, and `1`, all on different lines (togglable by a
+        disclosure triangle) since `#document` isn't stringifiable.
+
+        The ideal situation would be to have `"test"` and `1` logged on the same line, and then have
+        `#document` be in a disclosure triangle. This way, you can "label" your images using
+        additional arguments (e.g. `console.screenshot(document.images[1], "second image");`), as
+        well as provide other data.
+
+        If the only argument was the target, it should print as if it was `console.log(target);`.
+
+        If there are no arguments, it should print the text "Viewport"` before the image.
+
+        Test: inspector/console/console-screenshot.html
+
+        * page/PageConsoleClient.cpp:
+        (WebCore::PageConsoleClient::addMessage):
+        (WebCore::PageConsoleClient::screenshot):
+
 2019-07-24  Alicia Boya GarcĂ­a  <aboya@igalia.com>
 
         [MSE] Reenqueue after removeCodedFrames()
index c3b1d36..e1972d9 100644 (file)
@@ -105,11 +105,22 @@ void PageConsoleClient::unmute()
 
 void PageConsoleClient::addMessage(std::unique_ptr<Inspector::ConsoleMessage>&& consoleMessage)
 {
-    if (consoleMessage->source() != MessageSource::CSS && consoleMessage->type() != MessageType::Image && !m_page.usesEphemeralSession()) {
-        m_page.chrome().client().addMessageToConsole(consoleMessage->source(), consoleMessage->level(), consoleMessage->message(), consoleMessage->line(), consoleMessage->column(), consoleMessage->url());
-
-        if (m_page.settings().logsPageMessagesToSystemConsoleEnabled() || shouldPrintExceptions())
-            ConsoleClient::printConsoleMessage(MessageSource::ConsoleAPI, MessageType::Log, consoleMessage->level(), consoleMessage->message(), consoleMessage->url(), consoleMessage->line(), consoleMessage->column());
+    if (!m_page.usesEphemeralSession()) {
+        String message;
+        if (consoleMessage->type() == MessageType::Image) {
+            ASSERT(consoleMessage->arguments());
+            consoleMessage->arguments()->getFirstArgumentAsString(message);
+        } else
+            message = consoleMessage->message();
+        m_page.chrome().client().addMessageToConsole(consoleMessage->source(), consoleMessage->level(), message, consoleMessage->line(), consoleMessage->column(), consoleMessage->url());
+
+        if (UNLIKELY(m_page.settings().logsPageMessagesToSystemConsoleEnabled() || shouldPrintExceptions())) {
+            if (consoleMessage->type() == MessageType::Image) {
+                ASSERT(consoleMessage->arguments());
+                ConsoleClient::printConsoleMessageWithArguments(MessageSource::ConsoleAPI, MessageType::Log, consoleMessage->level(), consoleMessage->arguments()->globalState(), *consoleMessage->arguments());
+            } else
+                ConsoleClient::printConsoleMessage(MessageSource::ConsoleAPI, MessageType::Log, consoleMessage->level(), consoleMessage->message(), consoleMessage->url(), consoleMessage->line(), consoleMessage->column());
+        }
     }
 
     InspectorInstrumentation::addMessageToConsole(m_page, WTFMove(consoleMessage));
@@ -261,44 +272,41 @@ void PageConsoleClient::recordEnd(JSC::ExecState* state, Ref<ScriptArguments>&&
 
 void PageConsoleClient::screenshot(JSC::ExecState* state, Ref<ScriptArguments>&& arguments)
 {
-    FAST_RETURN_IF_NO_FRONTENDS(void());
-
-    Frame& frame = m_page.mainFrame();
-
-    std::unique_ptr<ImageBuffer> snapshot;
-
-    auto* target = objectArgumentAt(arguments, 0);
-    if (target) {
-        auto* node = JSNode::toWrapped(state->vm(), target);
-        if (!node)
-            return;
-
-        snapshot = WebCore::snapshotNode(frame, *node);
-    } else {
-        // If no target is provided, capture an image of the viewport.
-        IntRect imageRect(IntPoint::zero(), frame.view()->sizeForVisibleContent());
-        snapshot = WebCore::snapshotFrameRect(frame, imageRect, SnapshotOptionsInViewCoordinates);
+    String dataURL;
+    JSC::JSValue target;
+
+    if (arguments->argumentCount()) {
+        auto possibleTarget = arguments->argumentAt(0);
+
+        if (auto* node = JSNode::toWrapped(state->vm(), possibleTarget)) {
+            target = possibleTarget;
+            if (UNLIKELY(InspectorInstrumentation::hasFrontends())) {
+                if (auto snapshot = WebCore::snapshotNode(m_page.mainFrame(), *node))
+                    dataURL = snapshot->toDataURL("image/png"_s, WTF::nullopt, PreserveResolution::Yes);
+            }
+        }
     }
 
-    if (!snapshot) {
-        addMessage(std::make_unique<Inspector::ConsoleMessage>(MessageSource::ConsoleAPI, MessageType::Log, MessageLevel::Error, "Could not capture screenshot"_s, arguments.copyRef()));
-        return;
-    }
-
-    String dataURL = snapshot->toDataURL("image/png"_s, WTF::nullopt, PreserveResolution::Yes);
-    if (dataURL.isEmpty()) {
-        addMessage(std::make_unique<Inspector::ConsoleMessage>(MessageSource::ConsoleAPI, MessageType::Log, MessageLevel::Error, "Could not capture screenshot"_s, arguments.copyRef()));
-        return;
-    }
+    if (UNLIKELY(InspectorInstrumentation::hasFrontends())) {
+        if (!target) {
+            // If no target is provided, capture an image of the viewport.
+            IntRect imageRect(IntPoint::zero(), m_page.mainFrame().view()->sizeForVisibleContent());
+            if (auto snapshot = WebCore::snapshotFrameRect(m_page.mainFrame(), imageRect, SnapshotOptionsInViewCoordinates))
+                dataURL = snapshot->toDataURL("image/png"_s, WTF::nullopt, PreserveResolution::Yes);
+        }
 
-    if (target) {
-        // Log the argument before sending the image for it.
-        String messageText;
-        arguments->getFirstArgumentAsString(messageText);
-        addMessage(std::make_unique<Inspector::ConsoleMessage>(MessageSource::ConsoleAPI, MessageType::Log, MessageLevel::Log, messageText, arguments.copyRef()));
+        if (dataURL.isEmpty()) {
+            addMessage(std::make_unique<Inspector::ConsoleMessage>(MessageSource::ConsoleAPI, MessageType::Image, MessageLevel::Error, "Could not capture screenshot"_s, WTFMove(arguments)));
+            return;
+        }
     }
 
-    addMessage(std::make_unique<Inspector::ConsoleMessage>(MessageSource::ConsoleAPI, MessageType::Image, MessageLevel::Log, dataURL));
+    Vector<JSC::Strong<JSC::Unknown>> adjustedArguments;
+    adjustedArguments.append({ state->vm(), target ? target : JSC::jsNontrivialString(state, "Viewport"_s) });
+    for (size_t i = (!target ? 0 : 1); i < arguments->argumentCount(); ++i)
+        adjustedArguments.append({ state->vm(), arguments->argumentAt(i) });
+    arguments = ScriptArguments::create(*state, WTFMove(adjustedArguments));
+    addMessage(std::make_unique<Inspector::ConsoleMessage>(MessageSource::ConsoleAPI, MessageType::Image, MessageLevel::Log, dataURL, WTFMove(arguments)));
 }
 
 } // namespace WebCore
index 34f9b90..6a8c65d 100644 (file)
@@ -1,3 +1,27 @@
+2019-07-24  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: print the target of `console.screenshot` last so the target is the closest item to the image
+        https://bugs.webkit.org/show_bug.cgi?id=199308
+
+        Reviewed by Joseph Pecoraro.
+
+        * UserInterface/Views/ConsoleMessageView.js:
+        (WI.ConsoleMessageView.prototype.render):
+        (WI.ConsoleMessageView.prototype._appendMessageTextAndArguments):
+        (WI.ConsoleMessageView.prototype._handleContextMenu):
+        * UserInterface/Views/ConsoleMessageView.css:
+        (.console-image > .console-message-body > :matches(hr, img)): Added.
+        (.console-image > .console-message-body > hr): Added.
+        (.console-image > .console-message-body > img): Added.
+        (.console-log-level.console-image::before): Added.
+        (.console-message-body > .console-image): Deleted.
+        (.console-log-level.console-image-container::before): Deleted.
+        Allow `ConsoleMessage.MessageType.Image` to be an `ConsoleMessage.MessageLevel.Error`, and
+        print the message (and extra parameters) in that case.
+        Drive-by: reorganize the switch-case so all paths have the same `default` case.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
 2019-07-23  Nikita Vasilyev  <nvasilyev@apple.com>
 
         Web Inspector: Styles: Command-X should cut selected properties
index 0dc878c..0401c9b 100644 (file)
@@ -1194,6 +1194,7 @@ localizedStrings["Very High"] = "Very High";
 localizedStrings["View Image"] = "View Image";
 localizedStrings["View Recording"] = "View Recording";
 localizedStrings["View Shader"] = "View Shader";
+localizedStrings["Viewport"] = "Viewport";
 localizedStrings["Visible"] = "Visible";
 localizedStrings["Waiting"] = "Waiting";
 localizedStrings["Waiting for canvas contexts created by script or CSS."] = "Waiting for canvas contexts created by script or CSS.";
index 60ef3f1..683b219 100644 (file)
     -webkit-user-select: none;
 }
 
-.console-message-body > .console-image {
-    max-width: 500px;
-    max-height: 500px;
-    box-shadow: 1px 2px 6px hsla(0, 0%, 0%, 0.58);
-}
-
 .console-message-body > .show-grid {
     /* Prevents the light blue highlight from being visible in the checkerboard. */
     --checkerboard-light-square: white;
 }
 
+.console-image > .console-message-body > :matches(hr, img) {
+    display: block;
+    margin: 4px 0;
+}
+
+.console-image > .console-message-body > hr {
+    height: 1px;
+    background-image: linear-gradient(to right, var(--border-color) 50%, transparent 50%);
+    background-size: 10px 1px;
+    background-repeat: repeat-x;
+    border: none;
+}
+
+.console-image > .console-message-body > img {
+    max-width: 500px;
+    max-height: 500px;
+    box-shadow: 1px 2px 6px hsla(0, 0%, 0%, 0.5);
+}
+
 .console-message.expandable .console-top-level-message::before {
     display: inline-block;
 
@@ -193,7 +206,7 @@ body[dir=rtl] .console-message.expandable .console-top-level-message::before {
     content: url(../Images/Log.svg);
 }
 
-.console-log-level.console-image-container::before {
+.console-log-level.console-image::before {
     content: url(../Images/ConsoleImage.svg);
 }
 
index 6f0a7dd..06c5d1a 100644 (file)
@@ -99,7 +99,7 @@ WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
         this._renderRepeatCount();
 
         if (this._message.type === WI.ConsoleMessage.MessageType.Image) {
-            this._element.classList.add("console-image-container");
+            this._element.classList.add("console-image");
             this._element.addEventListener("contextmenu", this._handleContextMenu.bind(this));
         }
     }
@@ -256,7 +256,7 @@ WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
                         args = args.concat(this._message.parameters);
                 }
                 this._appendFormattedArguments(element, args);
-                break;
+                return;
 
             case WI.ConsoleMessage.MessageType.Assert:
                 var args = [WI.UIString("Assertion Failed")];
@@ -268,54 +268,74 @@ WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
                         args = args.concat(this._message.parameters);
                 }
                 this._appendFormattedArguments(element, args);
-                break;
+                return;
 
             case WI.ConsoleMessage.MessageType.Dir:
                 var obj = this._message.parameters ? this._message.parameters[0] : undefined;
                 this._appendFormattedArguments(element, ["%O", obj]);
-                break;
+                return;
 
             case WI.ConsoleMessage.MessageType.Table:
                 var args = this._message.parameters;
                 element.appendChild(this._formatParameterAsTable(args));
                 this._extraParameters = null;
-                break;
+                return;
 
             case WI.ConsoleMessage.MessageType.StartGroup:
             case WI.ConsoleMessage.MessageType.StartGroupCollapsed:
                 var args = this._message.parameters || [this._message.messageText || WI.UIString("Group")];
                 this._formatWithSubstitutionString(args, element);
                 this._extraParameters = null;
-                break;
+                return;
 
             case WI.ConsoleMessage.MessageType.Timing: {
                 let args = [this._message.messageText];
                 if (this._extraParameters)
                     args = args.concat(this._extraParameters);
                 this._appendFormattedArguments(element, args);
-                break;
+                return;
             }
 
             case WI.ConsoleMessage.MessageType.Image: {
-                let img = element.appendChild(document.createElement("img"));
-                img.classList.add("console-image", "show-grid");
-                img.src = this._message.messageText;
-                img.setAttribute("filename", WI.FileUtilities.screenshotString() + ".png");
-                img.addEventListener("load", (event) => {
-                    if (img.width >= img.height)
-                        img.width = img.width / window.devicePixelRatio;
-                    else
-                        img.height = img.height / window.devicePixelRatio;
-                });
-                break;
-            }
+                if (this._message.level === WI.ConsoleMessage.MessageLevel.Log) {
+                    if (this._message.parameters.length > 1) {
+                        this._appendFormattedArguments(element, this._message.parameters.slice(1));
 
-            default:
-                var args = this._message.parameters || [this._message.messageText];
-                this._appendFormattedArguments(element, args);
+                        element.appendChild(document.createElement("hr"));
+                    }
+
+                    let target = this._message.parameters[0];
+                    if (target === "Viewport")
+                        target = WI.UIString("Viewport");
+                    this._appendFormattedArguments(element, [target]);
+
+                    if (this._message.messageText) {
+                        let img = element.appendChild(document.createElement("img"));
+                        img.classList.add("show-grid");
+                        img.src = this._message.messageText;
+                        img.setAttribute("filename", WI.FileUtilities.screenshotString() + ".png");
+                        img.addEventListener("load", (event) => {
+                            if (img.width >= img.height)
+                                img.width = img.width / window.devicePixelRatio;
+                            else
+                                img.height = img.height / window.devicePixelRatio;
+                        });
+                    }
+                    return;
+                }
+
+                if (this._message.level === WI.ConsoleMessage.MessageLevel.Error) {
+                    let args = [this._message.messageText];
+                    if (this._extraParameters)
+                        args = args.concat(this._extraParameters);
+                    this._appendFormattedArguments(element, args);
+                    return;
+                }
+
+                console.assert();
                 break;
             }
-            return;
+            }
         }
 
         // FIXME: Better handle WI.ConsoleMessage.MessageSource.Network once it has request info.
@@ -944,7 +964,7 @@ WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
 
     _handleContextMenu(event)
     {
-        let image = event.target.closest(".console-image");
+        let image = event.target.closest(".console-image > .console-message-body > img");
         if (!image)
             return;