Web Inspector: provide a way to capture a screenshot of a node from within the page
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 15 Mar 2019 08:12:21 +0000 (08:12 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 15 Mar 2019 08:12:21 +0000 (08:12 +0000)
https://bugs.webkit.org/show_bug.cgi?id=194279
<rdar://problem/10731573>

Reviewed by Joseph Pecoraro.

Source/JavaScriptCore:

Add `console.screenshot` functionality, which displays a screenshot of a given object (if
able) within Web Inspector's Console tab. From there, it can be viewed and saved.

Currently, `console.screenshot` will
 - capture an image of a `Node` (if provided)
 - capture an image of the viewport if nothing is provided

* inspector/protocol/Console.json:
Add `Image` enum value to `ConsoleMessage` type.
* runtime/ConsoleTypes.h:
* inspector/ConsoleMessage.h:
* inspector/ConsoleMessage.cpp:
(Inspector::messageTypeValue):

* runtime/ConsoleClient.h:
* runtime/ConsoleObject.cpp:
(JSC::ConsoleObject::finishCreation):
(JSC::consoleProtoFuncScreenshot): Added.

* inspector/JSGlobalObjectConsoleClient.h:
* inspector/JSGlobalObjectConsoleClient.cpp:
(Inspector::JSGlobalObjectConsoleClient::screenshot): Added.

Source/WebCore:

Test: inspector/console/console-screenshot.html

Add `console.screenshot` functionality, which displays a screenshot of a given object (if
able) within Web Inspector's Console tab. From there, it can be viewed and saved.

Currently, `console.screenshot` will
 - capture an image of a `Node` (if provided)
 - capture an image of the viewport if nothing is provided

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

* workers/WorkerConsoleClient.h:
* workers/WorkerConsoleClient.cpp:
(WebCore::WorkerConsoleClient::screenshot): Added.
* worklets/WorkletConsoleClient.h:
* worklets/WorkletConsoleClient.cpp:
(WebCore::WorkletConsoleClient::screenshot): Added.

* inspector/CommandLineAPIModuleSource.js:
(CommandLineAPIImpl.prototype.screenshot): Added.

* inspector/InspectorInstrumentation.h:

Source/WebInspectorUI:

Add `console.screenshot` functionality, which displays a screenshot of a given object (if
able) within Web Inspector's Console tab. From there, it can be viewed and saved.

Currently, `console.screenshot` will
 - capture an image of a `Node` (if provided)
 - capture an image of the viewport if nothing is provided

* UserInterface/Models/ConsoleMessage.js:
(WI.ConsoleMessage):
* UserInterface/Views/ConsoleCommandView.js:
(WI.ConsoleCommandView.prototype.render):
* UserInterface/Views/ConsoleMessageView.js:
(WI.ConsoleMessageView.prototype.render):
(WI.ConsoleMessageView.prototype.toClipboardString):
(WI.ConsoleMessageView.prototype._appendMessageTextAndArguments):
(WI.ConsoleMessageView.prototype._appendSavedResultIndex):
(WI.ConsoleMessageView.prototype._appendStackTrace):
(WI.ConsoleMessageView.prototype._makeExpandable):
(WI.ConsoleMessageView.prototype._handleContextMenu): Added.
* UserInterface/Views/ConsoleMessageView.css:
(.console-user-command.special-user-log > .console-message-body): Added.
(.console-message-body): Added.
(.console-message-body > span): Added.
(.console-message-body > span > :matches(.console-message-enclosed, .console-message-preview, .console-message-preview-divider)): Added.
(.console-message-body > .console-image): Added.
(.console-message-body > .show-grid): Added.
(.console-error-level .console-message-body): Added.
(.console-warning-level .console-message-body): Added.
(.console-log-level.console-image-container::before): Added.
(.console-user-command > .console-message-body): Added.
(.console-warning-level .console-message-body): Added.
(.console-error-level .console-message-body): Added.
(.console-user-command > .console-message-body): Added.
(.console-user-command.special-user-log > .console-message-text): Deleted.
(.console-message-text): Deleted.
(.console-message-text > span): Deleted.
(.console-message-text > span > :matches(.console-message-enclosed, .console-message-preview, .console-message-preview-divider)): Deleted.
(.console-error-level .console-message-text): Deleted.
(.console-warning-level .console-message-text): Deleted.
(.console-user-command > .console-message-text): Deleted.
(.console-warning-level .console-message-text): Deleted.
(.console-error-level .console-message-text): Deleted.
(.console-user-command > .console-message-text): Deleted.
* UserInterface/Views/LogContentView.css:
(.search-in-progress .console-item:not(.filtered-out-by-search).special-user-log .console-message-text .highlighted): Added.
(.search-in-progress .console-item:not(.filtered-out-by-search).special-user-log .console-message-body .highlighted): Deleted.
Renamed variables/classes to be more semantically correct when the content is an image.
 - `_messageTextElement` to `_messageBodyElement` (JS)
 - `.console-message-text` to `.console-message-body` (CSS)

* UserInterface/Controllers/JavaScriptLogViewController.js:
(WI.JavaScriptLogViewController.prototype.renderPendingMessages):

* UserInterface/Views/Main.css:
(:matches(img, canvas).show-grid):
(@media (prefers-color-scheme: dark) :matches(img, canvas).show-grid):

* UserInterface/Base/FileUtilities.js:
(WI.FileUtilities.screenshotString): Added.

* UserInterface/Models/NativeFunctionParameters.js:
* UserInterface/Controllers/JavaScriptRuntimeCompletionProvider.js:

* UserInterface/Images/ConsoleImage.svg: Copied from UserInterface/Images/Canvas.svg.
* Localizations/en.lproj/localizedStrings.js:

LayoutTests:

* js/console.html:
* js/console-expected.txt:
* inspector/console/console-screenshot.html: Added.
* inspector/console/console-screenshot-expected.txt: Added.
* http/tests/inspector/dom/cross-domain-inspected-node-access-expected.txt:

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

38 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/inspector/dom/cross-domain-inspected-node-access-expected.txt
LayoutTests/inspector/console/console-screenshot-expected.txt [new file with mode: 0644]
LayoutTests/inspector/console/console-screenshot.html [new file with mode: 0644]
LayoutTests/js/console-expected.txt
LayoutTests/js/console.html
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/ConsoleMessage.cpp
Source/JavaScriptCore/inspector/ConsoleMessage.h
Source/JavaScriptCore/inspector/JSGlobalObjectConsoleClient.cpp
Source/JavaScriptCore/inspector/JSGlobalObjectConsoleClient.h
Source/JavaScriptCore/inspector/protocol/Console.json
Source/JavaScriptCore/runtime/ConsoleClient.h
Source/JavaScriptCore/runtime/ConsoleObject.cpp
Source/JavaScriptCore/runtime/ConsoleTypes.h
Source/WebCore/ChangeLog
Source/WebCore/inspector/CommandLineAPIModuleSource.js
Source/WebCore/inspector/InspectorInstrumentation.h
Source/WebCore/page/PageConsoleClient.cpp
Source/WebCore/page/PageConsoleClient.h
Source/WebCore/workers/WorkerConsoleClient.cpp
Source/WebCore/workers/WorkerConsoleClient.h
Source/WebCore/worklets/WorkletConsoleClient.cpp
Source/WebCore/worklets/WorkletConsoleClient.h
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/FileUtilities.js
Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js
Source/WebInspectorUI/UserInterface/Controllers/JavaScriptRuntimeCompletionProvider.js
Source/WebInspectorUI/UserInterface/Images/ConsoleImage.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Models/ConsoleMessage.js
Source/WebInspectorUI/UserInterface/Models/NativeFunctionParameters.js
Source/WebInspectorUI/UserInterface/Views/ConsoleCommandView.js
Source/WebInspectorUI/UserInterface/Views/ConsoleMessageView.css
Source/WebInspectorUI/UserInterface/Views/ConsoleMessageView.js
Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js
Source/WebInspectorUI/UserInterface/Views/LogContentView.css
Source/WebInspectorUI/UserInterface/Views/Main.css

index 7b8dd06..29ddbd8 100644 (file)
@@ -1,3 +1,17 @@
+2019-03-15  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: provide a way to capture a screenshot of a node from within the page
+        https://bugs.webkit.org/show_bug.cgi?id=194279
+        <rdar://problem/10731573>
+
+        Reviewed by Joseph Pecoraro.
+
+        * js/console.html:
+        * js/console-expected.txt:
+        * inspector/console/console-screenshot.html: Added.
+        * inspector/console/console-screenshot-expected.txt: Added.
+        * http/tests/inspector/dom/cross-domain-inspected-node-access-expected.txt:
+
 2019-03-14  Sihui Liu  <sihui_liu@apple.com>
 
         IndexedDB: re-enable some leak tests
index 89c9c53..9f857e9 100644 (file)
@@ -1,5 +1,5 @@
-CONSOLE MESSAGE: line 43: Blocked a frame with origin "http://127.0.0.1:8000" from accessing a frame with origin "http://localhost:8000". Protocols, domains, and ports must match.
-CONSOLE MESSAGE: line 43: Blocked a frame with origin "http://localhost:8000" from accessing a frame with origin "http://127.0.0.1:8000". Protocols, domains, and ports must match.
+CONSOLE MESSAGE: line 44: Blocked a frame with origin "http://127.0.0.1:8000" from accessing a frame with origin "http://localhost:8000". Protocols, domains, and ports must match.
+CONSOLE MESSAGE: line 44: Blocked a frame with origin "http://localhost:8000" from accessing a frame with origin "http://127.0.0.1:8000". Protocols, domains, and ports must match.
 Test that code evaluated in the main frame cannot access $0 that resolves to a node in a frame from a different domain. Bug 105423.
 
 
diff --git a/LayoutTests/inspector/console/console-screenshot-expected.txt b/LayoutTests/inspector/console/console-screenshot-expected.txt
new file mode 100644 (file)
index 0000000..ac66198
--- /dev/null
@@ -0,0 +1,23 @@
+CONSOLE MESSAGE: [object HTMLDivElement]
+CONSOLE MESSAGE: [object HTMLDivElement]
+CONSOLE MESSAGE: Could not capture screenshot
+Tests for the console.screenshot API.
+
+
+== Running test suite: console.screenshot
+-- Running test case: console.screenshot.SingleNode
+PASS: The added message should be an image.
+PASS: The image should be a 2x2 red square.
+
+-- Running test case: console.screenshot.MultipleNodes
+PASS: The added message should be an image.
+PASS: The image should be a 2x2 blue square.
+
+-- Running test case: console.screenshot.DetachedNode
+PASS: Could not capture screenshot
+
+-- Running test case: console.screenshot.NoArguments
+PASS: The added message should be an image.
+PASS: The image width should be greater than 2px.
+PASS: The image height should be greater than 2px.
+
diff --git a/LayoutTests/inspector/console/console-screenshot.html b/LayoutTests/inspector/console/console-screenshot.html
new file mode 100644 (file)
index 0000000..867694a
--- /dev/null
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+
+function createDetachedTest3()
+{
+    let div = document.createElement("div");
+    div.id = "test3";
+    return div;
+}
+
+function test()
+{
+    InspectorTest.debug();
+
+    // 2x2 red square
+    const redSquareDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABNJREFUCB1j/M/AAEQMDEwgAgQAHxcCAmtAm/sAAAAASUVORK5CYII=";
+
+    // 2x2 blue square
+    const blueSquareDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABRJREFUCB1jZGD4/58BCJhABAgAAB0ZAgJSPDJ6AAAAAElFTkSuQmCC";
+
+    // 2x2 green square
+    const greenSquareDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABNJREFUCB1jZGhg+M8ABEwgAgQAFigBgxp1H6oAAAAASUVORK5CYII=";
+
+    let suite = InspectorTest.createAsyncSuite("console.screenshot");
+
+    function addTest({name, expression, imageMessageAddedCallback, shouldError}) {
+        suite.addTestCase({
+            name,
+            test(resolve, reject) {
+                let listener = WI.consoleManager.addEventListener(WI.ConsoleManager.Event.MessageAdded, async (event) => {
+                    let {message} = event.data;
+
+                    let isError = message.level === WI.ConsoleMessage.MessageLevel.Error;
+                    if (isError || message.type === WI.ConsoleMessage.MessageType.Image) {
+                        WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.MessageAdded, listener);
+
+                        if (isError)
+                            InspectorTest.expectThat(shouldError, message.messageText);
+                        else
+                            InspectorTest.expectEqual(message.type, WI.ConsoleMessage.MessageType.Image, "The added message should be an image.");
+
+                        if (imageMessageAddedCallback)
+                            await imageMessageAddedCallback(message);
+
+                        resolve();
+                        return;
+                    }
+                });
+
+                InspectorTest.evaluateInPage(expression)
+                .catch(reject);
+            },
+        });
+    }
+
+    addTest({
+        name: "console.screenshot.SingleNode",
+        expression: `console.screenshot(document.querySelector("#test1"))`,
+        imageMessageAddedCallback(message) {
+            InspectorTest.expectEqual(message.messageText, redSquareDataURL, "The image should be a 2x2 red square.");
+        },
+    });
+
+    addTest({
+        name: "console.screenshot.MultipleNodes", 
+        expression: `console.screenshot(document.querySelector("#test2"), document.querySelector("#test1"))`,
+        imageMessageAddedCallback(message) {
+            InspectorTest.expectEqual(message.messageText, blueSquareDataURL, "The image should be a 2x2 blue square.");
+        },
+    });
+
+    addTest({
+        name: "console.screenshot.DetachedNode",  
+        expression: `console.screenshot(createDetachedTest3())`,
+        shouldError: true,
+    });
+
+    addTest({
+        name: "console.screenshot.NoArguments", 
+        expression: `console.screenshot()`,
+        async imageMessageAddedCallback(message) {
+            InspectorTest.assert(message.messageText !== redSquareDataURL, "The imag should not be a 2x2 red square.");
+            InspectorTest.assert(message.messageText !== blueSquareDataURL, "The imag should not be a 2x2 blue square.");
+            InspectorTest.assert(message.messageText !== greenSquareDataURL, "The imag should not be a 2x2 green square.");
+
+            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.");
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Tests for the console.screenshot API.</p>
+    <div id="test1"></div>
+    <div id="test2"></div>
+    <div id="test3"></div>
+    <style>
+    #test1 {
+        width: 2px;
+        height: 2px;
+        background-color: red;
+    }
+    #test2 {
+        width: 2px;
+        height: 2px;
+        background-color: blue;
+    }
+    #test3 {
+        width: 2px;
+        height: 2px;
+        background-color: green;
+    }
+    </style>
+</body>
+</html>
index 073aa92..b6bf724 100644 (file)
@@ -177,6 +177,13 @@ PASS descriptor.configurable is true
 PASS descriptor.writable is true
 PASS descriptor.enumerable is true
 
+console.screenshot
+PASS typeof console.screenshot is "function"
+PASS console.screenshot.length is 0
+PASS descriptor.configurable is true
+PASS descriptor.writable is true
+PASS descriptor.enumerable is true
+
 PASS Object.getOwnPropertyNames(console).length is enumerablePropertyCount
 
 fuzzing of target for console.record
@@ -218,6 +225,18 @@ PASS console.recordEnd(window.canvas) did not throw exception.
 PASS console.record(window.canvas) did not throw exception.
 PASS console.record(window.canvas) did not throw exception.
 PASS console.record(window.canvas) did not throw exception.
+
+fuzzing of target for console.screenshot
+PASS console.screenshot() did not throw exception.
+PASS console.screenshot(undefined) did not throw exception.
+PASS console.screenshot(null) did not throw exception.
+PASS console.screenshot(1) did not throw exception.
+PASS console.screenshot("test") did not throw exception.
+PASS console.screenshot([]) did not throw exception.
+PASS console.screenshot({}) did not throw exception.
+PASS console.screenshot(window) did not throw exception.
+PASS console.screenshot(console) did not throw exception.
+
 PASS successfullyParsed is true
 
 TEST COMPLETE
index e5637c6..b046f5b 100644 (file)
@@ -47,7 +47,7 @@ for (property in console) {
 debug("");
 shouldBe("Object.getOwnPropertyNames(console).length", "enumerablePropertyCount");
 
-const consoleRecordArguments = [
+const fuzzingArguments = [
        `undefined`,
        `null`,
        `1`,
@@ -61,18 +61,18 @@ const consoleRecordArguments = [
 debug("");
 debug("fuzzing of target for console.record");
 shouldNotThrow(`console.record()`);
-for (let argument of consoleRecordArguments)
+for (let argument of fuzzingArguments)
        shouldNotThrow(`console.record(${argument})`);
 
 debug("");
 debug("fuzzing of options for console.record");
-for (let argument of consoleRecordArguments)
+for (let argument of fuzzingArguments)
        shouldNotThrow(`console.record({}, ${argument})`);
 
 debug("");
 debug("fuzzing of target for console.recordEnd");
 shouldNotThrow(`console.recordEnd()`);
-for (let argument of consoleRecordArguments)
+for (let argument of fuzzingArguments)
        shouldNotThrow(`console.recordEnd(${argument})`);
 
 debug("");
@@ -84,6 +84,14 @@ shouldNotThrow(`console.record(window.canvas)`);
 shouldNotThrow(`console.record(window.canvas)`);
 shouldNotThrow(`console.record(window.canvas)`);
 
+debug("");
+debug("fuzzing of target for console.screenshot");
+shouldNotThrow(`console.screenshot()`);
+for (let argument of fuzzingArguments)
+    shouldNotThrow(`console.screenshot(${argument})`);
+
+debug("");
+
 </script>
 <script src="../resources/js-test-post.js"></script>
 <canvas id="canvas"></canvas>
index 2f17231..8fcfa2d 100644 (file)
@@ -1,3 +1,34 @@
+2019-03-15  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: provide a way to capture a screenshot of a node from within the page
+        https://bugs.webkit.org/show_bug.cgi?id=194279
+        <rdar://problem/10731573>
+
+        Reviewed by Joseph Pecoraro.
+
+        Add `console.screenshot` functionality, which displays a screenshot of a given object (if
+        able) within Web Inspector's Console tab. From there, it can be viewed and saved.
+
+        Currently, `console.screenshot` will
+         - capture an image of a `Node` (if provided)
+         - capture an image of the viewport if nothing is provided
+
+        * inspector/protocol/Console.json:
+        Add `Image` enum value to `ConsoleMessage` type.
+        * runtime/ConsoleTypes.h:
+        * inspector/ConsoleMessage.h:
+        * inspector/ConsoleMessage.cpp:
+        (Inspector::messageTypeValue):
+
+        * runtime/ConsoleClient.h:
+        * runtime/ConsoleObject.cpp:
+        (JSC::ConsoleObject::finishCreation):
+        (JSC::consoleProtoFuncScreenshot): Added.
+
+        * inspector/JSGlobalObjectConsoleClient.h:
+        * inspector/JSGlobalObjectConsoleClient.cpp:
+        (Inspector::JSGlobalObjectConsoleClient::screenshot): Added.
+
 2019-03-14  Yusuke Suzuki  <ysuzuki@apple.com>
 
         [JSC] Retain PrivateName of Symbol before passing it to operations potentially incurring GC
index f73deef..f501e10 100644 (file)
@@ -191,6 +191,7 @@ static Protocol::Console::ConsoleMessage::Type messageTypeValue(MessageType type
     case MessageType::Timing: return Protocol::Console::ConsoleMessage::Type::Timing;
     case MessageType::Profile: return Protocol::Console::ConsoleMessage::Type::Profile;
     case MessageType::ProfileEnd: return Protocol::Console::ConsoleMessage::Type::ProfileEnd;
+    case MessageType::Image: return Protocol::Console::ConsoleMessage::Type::Image;
     }
     return Protocol::Console::ConsoleMessage::Type::Log;
 }
index f3706db..0a92f5b 100644 (file)
@@ -55,7 +55,7 @@ public:
     ConsoleMessage(MessageSource, MessageType, MessageLevel, const String& message, unsigned long requestIdentifier = 0);
     ConsoleMessage(MessageSource, MessageType, MessageLevel, const String& message, const String& url, unsigned line, unsigned column, JSC::ExecState* = nullptr, unsigned long requestIdentifier = 0);
     ConsoleMessage(MessageSource, MessageType, MessageLevel, const String& message, Ref<ScriptCallStack>&&, unsigned long requestIdentifier = 0);
-    ConsoleMessage(MessageSource, MessageType, MessageLevel, const String& message, Ref<ScriptArguments>&&, JSC::ExecState*, unsigned long requestIdentifier = 0);
+    ConsoleMessage(MessageSource, MessageType, MessageLevel, const String& message, Ref<ScriptArguments>&&, JSC::ExecState* = nullptr, unsigned long requestIdentifier = 0);
     ConsoleMessage(MessageSource, MessageType, MessageLevel, Vector<JSONLogValue>&&, JSC::ExecState*, unsigned long requestIdentifier = 0);
     ~ConsoleMessage();
 
index 096b9b7..79e0275 100644 (file)
@@ -168,6 +168,11 @@ void JSGlobalObjectConsoleClient::timeStamp(ExecState*, Ref<ScriptArguments>&&)
 void JSGlobalObjectConsoleClient::record(ExecState*, Ref<ScriptArguments>&&) { }
 void JSGlobalObjectConsoleClient::recordEnd(ExecState*, Ref<ScriptArguments>&&) { }
 
+void JSGlobalObjectConsoleClient::screenshot(ExecState*, Ref<ScriptArguments>&&)
+{
+    warnUnimplemented("console.screenshot"_s);
+}
+
 void JSGlobalObjectConsoleClient::warnUnimplemented(const String& method)
 {
     String message = method + " is currently ignored in JavaScript context inspection.";
index 448917e..2e169c2 100644 (file)
@@ -55,6 +55,7 @@ protected:
     void timeStamp(JSC::ExecState*, Ref<ScriptArguments>&&) override;
     void record(JSC::ExecState*, Ref<ScriptArguments>&&) override;
     void recordEnd(JSC::ExecState*, Ref<ScriptArguments>&&) override;
+    void screenshot(JSC::ExecState*, Ref<ScriptArguments>&&) override;
 
 private:
     void warnUnimplemented(const String& method);
index 8fff4a9..f3adc8f 100644 (file)
@@ -31,7 +31,7 @@
                 { "name": "source", "$ref": "ChannelSource"},
                 { "name": "level", "type": "string", "enum": ["log", "info", "warning", "error", "debug"], "description": "Message severity." },
                 { "name": "text", "type": "string", "description": "Message text." },
-                { "name": "type", "type": "string", "optional": true, "enum": ["log", "dir", "dirxml", "table", "trace", "clear", "startGroup", "startGroupCollapsed", "endGroup", "assert", "timing", "profile", "profileEnd"], "description": "Console message type." },
+                { "name": "type", "type": "string", "optional": true, "enum": ["log", "dir", "dirxml", "table", "trace", "clear", "startGroup", "startGroupCollapsed", "endGroup", "assert", "timing", "profile", "profileEnd", "image"], "description": "Console message type." },
                 { "name": "url", "type": "string", "optional": true, "description": "URL of the message origin." },
                 { "name": "line", "type": "integer", "optional": true, "description": "Line number in the resource that generated this message." },
                 { "name": "column", "type": "integer", "optional": true, "description": "Column number on the line in the resource that generated this message." },
index 852c193..53abb50 100644 (file)
@@ -64,6 +64,7 @@ public:
     virtual void timeStamp(ExecState*, Ref<Inspector::ScriptArguments>&&) = 0;
     virtual void record(ExecState*, Ref<Inspector::ScriptArguments>&&) = 0;
     virtual void recordEnd(ExecState*, Ref<Inspector::ScriptArguments>&&) = 0;
+    virtual void screenshot(ExecState*, Ref<Inspector::ScriptArguments>&&) = 0;
 
 private:
     enum ArgumentRequirement { ArgumentRequired, ArgumentNotRequired };
index 417d640..37c40d3 100644 (file)
@@ -59,6 +59,7 @@ static EncodedJSValue JSC_HOST_CALL consoleProtoFuncGroupCollapsed(ExecState*);
 static EncodedJSValue JSC_HOST_CALL consoleProtoFuncGroupEnd(ExecState*);
 static EncodedJSValue JSC_HOST_CALL consoleProtoFuncRecord(ExecState*);
 static EncodedJSValue JSC_HOST_CALL consoleProtoFuncRecordEnd(ExecState*);
+static EncodedJSValue JSC_HOST_CALL consoleProtoFuncScreenshot(ExecState*);
 
 const ClassInfo ConsoleObject::s_info = { "Console", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(ConsoleObject) };
 
@@ -99,6 +100,7 @@ void ConsoleObject::finishCreation(VM& vm, JSGlobalObject* globalObject)
     JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("groupEnd", consoleProtoFuncGroupEnd, static_cast<unsigned>(PropertyAttribute::None), 0);
     JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("record", consoleProtoFuncRecord, static_cast<unsigned>(PropertyAttribute::None), 0);
     JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("recordEnd", consoleProtoFuncRecordEnd, static_cast<unsigned>(PropertyAttribute::None), 0);
+    JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("screenshot", consoleProtoFuncScreenshot, static_cast<unsigned>(PropertyAttribute::None), 0);
 }
 
 static String valueToStringWithUndefinedOrNullCheck(ExecState* exec, JSValue value)
@@ -391,4 +393,14 @@ static EncodedJSValue JSC_HOST_CALL consoleProtoFuncRecordEnd(ExecState* exec)
     return JSValue::encode(jsUndefined());
 }
 
+static EncodedJSValue JSC_HOST_CALL consoleProtoFuncScreenshot(ExecState* exec)
+{
+    ConsoleClient* client = exec->lexicalGlobalObject()->consoleClient();
+    if (!client)
+        return JSValue::encode(jsUndefined());
+
+    client->screenshot(exec, Inspector::createScriptArguments(exec, 0));
+    return JSValue::encode(jsUndefined());
+}
+
 } // namespace JSC
index fffebed..9c409fc 100644 (file)
@@ -60,6 +60,7 @@ enum class MessageType {
     Timing,
     Profile,
     ProfileEnd,
+    Image,
 };
 
 enum class MessageLevel : uint8_t {
index b9d982d..9a33341 100644 (file)
@@ -1,3 +1,37 @@
+2019-03-15  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: provide a way to capture a screenshot of a node from within the page
+        https://bugs.webkit.org/show_bug.cgi?id=194279
+        <rdar://problem/10731573>
+
+        Reviewed by Joseph Pecoraro.
+
+        Test: inspector/console/console-screenshot.html
+
+        Add `console.screenshot` functionality, which displays a screenshot of a given object (if
+        able) within Web Inspector's Console tab. From there, it can be viewed and saved.
+
+        Currently, `console.screenshot` will
+         - capture an image of a `Node` (if provided)
+         - capture an image of the viewport if nothing is provided
+
+        * page/PageConsoleClient.h:
+        * page/PageConsoleClient.cpp:
+        (WebCore::PageConsoleClient::addMessage):
+        (WebCore::PageConsoleClient::screenshot): Added.
+
+        * workers/WorkerConsoleClient.h:
+        * workers/WorkerConsoleClient.cpp:
+        (WebCore::WorkerConsoleClient::screenshot): Added.
+        * worklets/WorkletConsoleClient.h:
+        * worklets/WorkletConsoleClient.cpp:
+        (WebCore::WorkletConsoleClient::screenshot): Added.
+
+        * inspector/CommandLineAPIModuleSource.js:
+        (CommandLineAPIImpl.prototype.screenshot): Added.
+
+        * inspector/InspectorInstrumentation.h:
+
 2019-03-14  Yusuke Suzuki  <ysuzuki@apple.com>
 
         [JSC] Retain PrivateName of Symbol before passing it to operations potentially incurring GC
index 0fecdeb..18d6326 100644 (file)
@@ -83,6 +83,7 @@ CommandLineAPI.methods = [
     "profile",
     "profileEnd",
     "queryObjects",
+    "screenshot",
     "table",
     "unmonitorEvents",
     "values",
@@ -200,6 +201,11 @@ CommandLineAPIImpl.prototype = {
         return inspectedWindow.console.table.apply(inspectedWindow.console, arguments)
     },
 
+    screenshot: function()
+    {
+        return inspectedWindow.console.screenshot.apply(inspectedWindow.console, arguments)
+    },
+
     /**
      * @param {Object} object
      * @param {Array.<string>|string=} types
index ad99aad..885f3d0 100644 (file)
@@ -98,7 +98,7 @@ enum class StorageType;
 
 struct WebSocketFrame;
 
-#define FAST_RETURN_IF_NO_FRONTENDS(value) if (LIKELY(!hasFrontends())) return value;
+#define FAST_RETURN_IF_NO_FRONTENDS(value) if (LIKELY(!InspectorInstrumentation::hasFrontends())) return value;
 
 class InspectorInstrumentation {
 public:
index 9f65531..c12368f 100644 (file)
 #include "ChromeClient.h"
 #include "Document.h"
 #include "Frame.h"
+#include "FrameSnapshotting.h"
 #include "HTMLCanvasElement.h"
 #include "ImageBitmapRenderingContext.h"
+#include "ImageBuffer.h"
 #include "InspectorController.h"
 #include "InspectorInstrumentation.h"
+#include "IntRect.h"
 #include "JSCanvasRenderingContext2D.h"
 #include "JSExecState.h"
 #include "JSHTMLCanvasElement.h"
 #include "JSImageBitmapRenderingContext.h"
+#include "JSNode.h"
 #include "JSOffscreenCanvas.h"
+#include "Node.h"
 #include "OffscreenCanvas.h"
 #include "Page.h"
 #include "ScriptableDocumentParser.h"
@@ -119,7 +124,7 @@ static void getParserLocationForConsoleMessage(Document* document, String& url,
 
 void PageConsoleClient::addMessage(std::unique_ptr<Inspector::ConsoleMessage>&& consoleMessage)
 {
-    if (consoleMessage->source() != MessageSource::CSS && !m_page.usesEphemeralSession()) {
+    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())
@@ -258,4 +263,46 @@ void PageConsoleClient::recordEnd(JSC::ExecState* state, Ref<ScriptArguments>&&
         InspectorInstrumentation::didFinishRecordingCanvasFrame(*context, true);
 }
 
+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);
+    }
+
+    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 (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()));
+    }
+
+    addMessage(std::make_unique<Inspector::ConsoleMessage>(MessageSource::ConsoleAPI, MessageType::Image, MessageLevel::Log, dataURL));
+}
+
 } // namespace WebCore
index ba6140d..52c2fdc 100644 (file)
@@ -76,6 +76,7 @@ protected:
     void timeStamp(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
     void record(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
     void recordEnd(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
+    void screenshot(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
 
 private:
     Page& m_page;
index c354a56..bab2da6 100644 (file)
@@ -76,4 +76,6 @@ void WorkerConsoleClient::timeStamp(JSC::ExecState*, Ref<ScriptArguments>&&) { }
 void WorkerConsoleClient::record(JSC::ExecState*, Ref<ScriptArguments>&&) { }
 void WorkerConsoleClient::recordEnd(JSC::ExecState*, Ref<ScriptArguments>&&) { }
 
+void WorkerConsoleClient::screenshot(JSC::ExecState*, Ref<ScriptArguments>&&) { }
+
 } // namespace WebCore
index 1f79bf9..ef38587 100644 (file)
@@ -52,6 +52,7 @@ protected:
     void timeStamp(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
     void record(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
     void recordEnd(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
+    void screenshot(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
 
 private:
     WorkerGlobalScope& m_workerGlobalScope;
index 89506b9..5dcb68e 100644 (file)
@@ -66,5 +66,7 @@ void WorkletConsoleClient::timeStamp(JSC::ExecState*, Ref<ScriptArguments>&&) {
 void WorkletConsoleClient::record(JSC::ExecState*, Ref<ScriptArguments>&&) { }
 void WorkletConsoleClient::recordEnd(JSC::ExecState*, Ref<ScriptArguments>&&) { }
 
+void WorkletConsoleClient::screenshot(JSC::ExecState*, Ref<ScriptArguments>&&) { }
+
 } // namespace WebCore
 #endif
index 520f759..7a581c8 100644 (file)
@@ -54,6 +54,7 @@ private:
     void timeStamp(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) final;
     void record(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) final;
     void recordEnd(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) final;
+    void screenshot(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) final;
 
     WorkletGlobalScope& m_workletGlobalScope;
 };
index 1e9e826..13ae870 100644 (file)
@@ -1,3 +1,77 @@
+2019-03-15  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: provide a way to capture a screenshot of a node from within the page
+        https://bugs.webkit.org/show_bug.cgi?id=194279
+        <rdar://problem/10731573>
+
+        Reviewed by Joseph Pecoraro.
+
+        Add `console.screenshot` functionality, which displays a screenshot of a given object (if
+        able) within Web Inspector's Console tab. From there, it can be viewed and saved.
+
+        Currently, `console.screenshot` will
+         - capture an image of a `Node` (if provided)
+         - capture an image of the viewport if nothing is provided
+
+        * UserInterface/Models/ConsoleMessage.js:
+        (WI.ConsoleMessage):
+        * UserInterface/Views/ConsoleCommandView.js:
+        (WI.ConsoleCommandView.prototype.render):
+        * UserInterface/Views/ConsoleMessageView.js:
+        (WI.ConsoleMessageView.prototype.render):
+        (WI.ConsoleMessageView.prototype.toClipboardString):
+        (WI.ConsoleMessageView.prototype._appendMessageTextAndArguments):
+        (WI.ConsoleMessageView.prototype._appendSavedResultIndex):
+        (WI.ConsoleMessageView.prototype._appendStackTrace):
+        (WI.ConsoleMessageView.prototype._makeExpandable):
+        (WI.ConsoleMessageView.prototype._handleContextMenu): Added.
+        * UserInterface/Views/ConsoleMessageView.css:
+        (.console-user-command.special-user-log > .console-message-body): Added.
+        (.console-message-body): Added.
+        (.console-message-body > span): Added.
+        (.console-message-body > span > :matches(.console-message-enclosed, .console-message-preview, .console-message-preview-divider)): Added.
+        (.console-message-body > .console-image): Added.
+        (.console-message-body > .show-grid): Added.
+        (.console-error-level .console-message-body): Added.
+        (.console-warning-level .console-message-body): Added.
+        (.console-log-level.console-image-container::before): Added.
+        (.console-user-command > .console-message-body): Added.
+        (.console-warning-level .console-message-body): Added.
+        (.console-error-level .console-message-body): Added.
+        (.console-user-command > .console-message-body): Added.
+        (.console-user-command.special-user-log > .console-message-text): Deleted.
+        (.console-message-text): Deleted.
+        (.console-message-text > span): Deleted.
+        (.console-message-text > span > :matches(.console-message-enclosed, .console-message-preview, .console-message-preview-divider)): Deleted.
+        (.console-error-level .console-message-text): Deleted.
+        (.console-warning-level .console-message-text): Deleted.
+        (.console-user-command > .console-message-text): Deleted.
+        (.console-warning-level .console-message-text): Deleted.
+        (.console-error-level .console-message-text): Deleted.
+        (.console-user-command > .console-message-text): Deleted.
+        * UserInterface/Views/LogContentView.css:
+        (.search-in-progress .console-item:not(.filtered-out-by-search).special-user-log .console-message-text .highlighted): Added.
+        (.search-in-progress .console-item:not(.filtered-out-by-search).special-user-log .console-message-body .highlighted): Deleted.
+        Renamed variables/classes to be more semantically correct when the content is an image.
+         - `_messageTextElement` to `_messageBodyElement` (JS)
+         - `.console-message-text` to `.console-message-body` (CSS)
+
+        * UserInterface/Controllers/JavaScriptLogViewController.js:
+        (WI.JavaScriptLogViewController.prototype.renderPendingMessages):
+
+        * UserInterface/Views/Main.css:
+        (:matches(img, canvas).show-grid):
+        (@media (prefers-color-scheme: dark) :matches(img, canvas).show-grid):
+
+        * UserInterface/Base/FileUtilities.js:
+        (WI.FileUtilities.screenshotString): Added.
+
+        * UserInterface/Models/NativeFunctionParameters.js:
+        * UserInterface/Controllers/JavaScriptRuntimeCompletionProvider.js:
+
+        * UserInterface/Images/ConsoleImage.svg: Copied from UserInterface/Images/Canvas.svg.
+        * Localizations/en.lproj/localizedStrings.js:
+
 2019-03-14  Nikita Vasilyev  <nvasilyev@apple.com>
 
         Web Inspector: Styles: Jump to effective property button doesn't hide after overridden property become effective
index bba5a66..44eb84a 100644 (file)
@@ -864,6 +864,7 @@ localizedStrings["SVG"] = "SVG";
 localizedStrings["Samples"] = "Samples";
 localizedStrings["Save %d"] = "Save %d";
 localizedStrings["Save File"] = "Save File";
+localizedStrings["Save Image"] = "Save Image";
 localizedStrings["Save Selected"] = "Save Selected";
 localizedStrings["Save configuration"] = "Save configuration";
 localizedStrings["Saved States"] = "Saved States";
index e8deaaf..ccc06dd 100644 (file)
  */
 
 WI.FileUtilities = class FileUtilities {
+    static screenshotString()
+    {
+        let date = new Date;
+        let values = [
+            date.getFullYear(),
+            Number.zeroPad(date.getMonth() + 1, 2),
+            Number.zeroPad(date.getDate(), 2),
+            Number.zeroPad(date.getHours(), 2),
+            Number.zeroPad(date.getMinutes(), 2),
+            Number.zeroPad(date.getSeconds(), 2),
+        ];
+        return WI.UIString("Screen Shot %s-%s-%s at %s.%s.%s").format(...values);
+    }
+
     static save(saveData, forceSaveAs)
     {
         console.assert(saveData);
index b7ec1e6..f8408e8 100644 (file)
@@ -323,7 +323,7 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob
 
         this._currentSessionOrGroup = savedCurrentConsoleGroup;
 
-        if (wasScrolledToBottom || lastMessageView instanceof WI.ConsoleCommandView || lastMessageView.message.type === WI.ConsoleMessage.MessageType.Result)
+        if (wasScrolledToBottom || lastMessageView instanceof WI.ConsoleCommandView || lastMessageView.message.type === WI.ConsoleMessage.MessageType.Result || lastMessageView.message.type === WI.ConsoleMessage.MessageType.Image)
             this.scrollToBottom();
 
         WI.quickConsole.needsLayout();
index f23df46..e71d677 100644 (file)
@@ -319,6 +319,7 @@ WI.JavaScriptRuntimeCompletionProvider._commandLineAPI = [
     "profile",
     "profileEnd",
     "queryObjects",
+    "screenshot",
     "table",
     "unmonitorEvents",
     "values",
diff --git a/Source/WebInspectorUI/UserInterface/Images/ConsoleImage.svg b/Source/WebInspectorUI/UserInterface/Images/ConsoleImage.svg
new file mode 100644 (file)
index 0000000..dbd657d
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2017 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <rect x="1.5" y="2.5" width="13" height="11" fill="none" stroke="currentColor"/>
+    <polygon points="11 8 9 11 6 7 3 11 3 12 13 12 13 11 11 8"/>
+</svg>
index 04a4008..191aa02 100644 (file)
@@ -27,10 +27,11 @@ WI.ConsoleMessage = class ConsoleMessage
 {
     constructor(target, source, level, message, type, url, line, column, repeatCount, parameters, callFrames, request)
     {
+        console.assert(target instanceof WI.Target);
         console.assert(typeof source === "string");
         console.assert(typeof level === "string");
         console.assert(typeof message === "string");
-        console.assert(target instanceof WI.Target);
+        console.assert(!type || Object.values(WI.ConsoleMessage.MessageType).includes(type));
         console.assert(!parameters || parameters.every((x) => x instanceof WI.RemoteObject));
 
         this._target = target;
@@ -127,6 +128,7 @@ WI.ConsoleMessage.MessageType = {
     Timing: "timing",
     Profile: "profile",
     ProfileEnd: "profileEnd",
+    Image: "image",
     Result: "result", // Frontend Only.
 };
 
index 126f400..ccbb66b 100644 (file)
@@ -175,6 +175,7 @@ WI.NativeConstructorFunctionParameters = {
         profileEnd: "name",
         record: "object, [options]",
         recordEnd: "object",
+        screenshot: "[node]",
         table: "data, [columns]",
         takeHeapSnapshot: "[label]",
         time: "name = \"default\"",
index 434c6aa..8326a55 100644 (file)
@@ -49,7 +49,7 @@ WI.ConsoleCommandView = class ConsoleCommandView extends WI.Object
             this._element.classList.add(this._className);
 
         this._formattedCommandElement = this._element.appendChild(document.createElement("span"));
-        this._formattedCommandElement.classList.add("console-message-text");
+        this._formattedCommandElement.classList.add("console-message-body");
         this._formattedCommandElement.textContent = this._commandText;
 
         // FIXME: <https://webkit.org/b/143545> Web Inspector: LogContentView should use higher level objects
index 3a2909f..1184333 100644 (file)
@@ -29,7 +29,7 @@
     min-height: 21px;
 }
 
-.console-user-command.special-user-log > .console-message-text {
+.console-user-command.special-user-log > .console-message-body {
     padding: 0 6px 1px;
     border-radius: 3px;
     border: 1px solid transparent;
     border-radius: 7px;
 }
 
-.console-message-text {
+.console-message-body {
     white-space: pre-wrap;
 }
 
-.console-message-text > span {
+.console-message-body > span {
     -webkit-user-select: text;
 }
 
-.console-message-text > span > :matches(.console-message-enclosed, .console-message-preview, .console-message-preview-divider) {
+.console-message-body > span > :matches(.console-message-enclosed, .console-message-preview, .console-message-preview-divider) {
     -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-message.expandable .console-top-level-message::before {
     display: inline-block;
 
@@ -160,7 +171,7 @@ body[dir=rtl] .console-message.expandable .console-top-level-message::before {
     border-color: hsl(0, 100%, 92%);
 }
 
-.console-error-level .console-message-text {
+.console-error-level .console-message-body {
     color: hsl(0, 75%, 45%);
 }
 
@@ -169,7 +180,7 @@ body[dir=rtl] .console-message.expandable .console-top-level-message::before {
     border-color: hsl(40, 100%, 90%);
 }
 
-.console-warning-level .console-message-text {
+.console-warning-level .console-message-body {
     color: hsl(30, 90%, 35%);
 }
 
@@ -182,6 +193,10 @@ body[dir=rtl] .console-message.expandable .console-top-level-message::before {
     content: url(../Images/Log.svg);
 }
 
+.console-log-level.console-image-container::before {
+    content: url(../Images/ConsoleImage.svg);
+}
+
 .console-info-level::before {
     content: url(../Images/Info.svg);
 }
@@ -212,7 +227,7 @@ body[dir=rtl] .console-message.expandable .console-top-level-message::before {
     padding-top: 1px;
 }
 
-.console-user-command > .console-message-text {
+.console-user-command > .console-message-body {
     color: hsl(209, 100%, 50%);
     -webkit-user-select: text;
 }
@@ -290,15 +305,15 @@ body[dir=rtl] .console-message.expandable .console-top-level-message::before {
         background-color: unset;
     }
 
-    .console-warning-level .console-message-text {
+    .console-warning-level .console-message-body {
         color: hsl(53, 80%, 55%);
     }
 
-    .console-error-level .console-message-text {
+    .console-error-level .console-message-body {
         color: hsl(10, 100%, 70%);
     }
 
-    .console-user-command > .console-message-text {
+    .console-user-command > .console-message-body {
         color: hsl(209, 100%, 70%);
     }
 
index 4cf58fd..a6c23c6 100644 (file)
@@ -88,16 +88,20 @@ WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
         // FIXME: The location link should include stack trace information.
         this._appendLocationLink();
 
-        this._messageTextElement = this._element.appendChild(document.createElement("span"));
-        this._messageTextElement.classList.add("console-top-level-message");
-        this._messageTextElement.classList.add("console-message-text");
-        this._appendMessageTextAndArguments(this._messageTextElement);
+        this._messageBodyElement = this._element.appendChild(document.createElement("span"));
+        this._messageBodyElement.classList.add("console-top-level-message", "console-message-body");
+        this._appendMessageTextAndArguments(this._messageBodyElement);
         this._appendSavedResultIndex();
 
         this._appendExtraParameters();
         this._appendStackTrace();
 
         this._renderRepeatCount();
+
+        if (this._message.type === WI.ConsoleMessage.MessageType.Image) {
+            this._element.classList.add("console-image-container");
+            this._element.addEventListener("contextmenu", this._handleContextMenu.bind(this));
+        }
     }
 
     get element()
@@ -197,7 +201,7 @@ WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
 
     toClipboardString(isPrefixOptional)
     {
-        let clipboardString = this._messageTextElement.innerText.removeWordBreakCharacters();
+        let clipboardString = this._messageBodyElement.innerText.removeWordBreakCharacters();
         if (this._message.savedResultIndex)
             clipboardString = clipboardString.replace(/\s*=\s*(\$\d+)$/, "");
 
@@ -284,6 +288,20 @@ WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
                 this._extraParameters = null;
                 break;
 
+            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;
+            }
+
             default:
                 var args = this._message.parameters || [this._message.messageText];
                 this._appendFormattedArguments(element, args);
@@ -313,7 +331,7 @@ WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
         if (this._objectTree)
             this._objectTree.appendTitleSuffix(savedVariableElement);
         else
-            this._messageTextElement.appendChild(savedVariableElement);
+            this._messageBodyElement.appendChild(savedVariableElement);
     }
 
     _appendLocationLink()
@@ -403,7 +421,7 @@ WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
             this.expand();
 
         this._stackTraceElement = this._element.appendChild(document.createElement("div"));
-        this._stackTraceElement.classList.add("console-message-text", "console-message-stack-trace-container");
+        this._stackTraceElement.classList.add("console-message-body", "console-message-stack-trace-container");
 
         var callFramesElement = new WI.StackTraceView(this._message.stackTrace).element;
         this._stackTraceElement.appendChild(callFramesElement);
@@ -920,6 +938,26 @@ WI.ConsoleMessageView = class ConsoleMessageView extends WI.Object
         this._element.classList.add("expandable");
 
         this._boundClickHandler = this.toggle.bind(this);
-        this._messageTextElement.addEventListener("click", this._boundClickHandler);
+        this._messageBodyElement.addEventListener("click", this._boundClickHandler);
+    }
+
+    _handleContextMenu(event)
+    {
+        let image = event.target.closest(".console-image");
+        if (!image)
+            return;
+
+        let contextMenu = WI.ContextMenu.createFromEvent(event);
+
+        contextMenu.appendItem(WI.UIString("Save Image"), () => {
+            const forceSaveAs = true;
+            WI.FileUtilities.save({
+                url: encodeURI("web-inspector:///" + image.getAttribute("filename")),
+                content: parseDataURL(this._message.messageText).data,
+                base64Encoded: true,
+            }, forceSaveAs);
+        });
+
+        contextMenu.appendSeparator();
     }
 };
index d17d8b4..b290ce0 100644 (file)
@@ -250,18 +250,8 @@ WI.appendContextMenuItemsForDOMNode = function(contextMenu, domNode, options = {
                     return;
                 }
 
-                let date = new Date;
-                let values = [
-                    date.getFullYear(),
-                    Number.zeroPad(date.getMonth() + 1, 2),
-                    Number.zeroPad(date.getDate(), 2),
-                    Number.zeroPad(date.getHours(), 2),
-                    Number.zeroPad(date.getMinutes(), 2),
-                    Number.zeroPad(date.getSeconds(), 2),
-                ];
-                let filename = WI.UIString("Screen Shot %s-%s-%s at %s.%s.%s").format(...values);
                 WI.FileUtilities.save({
-                    url: encodeURI(`web-inspector:///${filename}.png`),
+                    url: encodeURI(`web-inspector:///${WI.FileUtilities.screenshotString()}.png`),
                     content: parseDataURL(dataURL).data,
                     base64Encoded: true,
                 });
index 9a368ec..aca9090 100644 (file)
@@ -226,7 +226,7 @@ body[dir=rtl] .console-group-title::before {
     background-color: hsla(53, 83%, 53%, 0.75);
 }
 
-.search-in-progress .console-item:not(.filtered-out-by-search).special-user-log .console-message-text .highlighted {
+.search-in-progress .console-item:not(.filtered-out-by-search).special-user-log .console-message-body .highlighted {
     color: var(--selected-foreground-color);
     background-color: var(--selected-background-color-highlight);
 
index 15ed4a2..3a535e7 100644 (file)
@@ -411,12 +411,16 @@ body[dir=rtl] .go-to-arrow {
 }
 
 :matches(img, canvas).show-grid {
-    background-image: linear-gradient(315deg, transparent 75%, hsl(0, 0%, 95%) 75%),
-                      linear-gradient(45deg, transparent 75%, hsl(0, 0%, 95%) 75%),
-                      linear-gradient(315deg, hsl(0, 0%, 95%) 25%, transparent 25%),
-                      linear-gradient(45deg, hsl(0, 0%, 95%) 25%, transparent 25%);
+    background-color: var(--checkerboard-light-square);
+    background-image: linear-gradient(315deg, transparent 75%, var(--checkerboard-dark-square) 75%),
+                      linear-gradient(45deg, transparent 75%, var(--checkerboard-dark-square) 75%),
+                      linear-gradient(315deg, var(--checkerboard-dark-square) 25%, transparent 25%),
+                      linear-gradient(45deg, var(--checkerboard-dark-square) 25%, transparent 25%);
     background-size: 20px 20px;
     background-position: 10px 10px, 10px 0px, 0 0, 0 10px;
+
+    --checkerboard-light-square: transparent;
+    --checkerboard-dark-square: hsl(0, 0%, 95%);
 }
 
 .device-settings-content {
@@ -479,8 +483,7 @@ body[dir=rtl] .go-to-arrow {
     }
 
     :matches(img, canvas).show-grid {
-        background-color: white;
+        --checkerboard-light-square: white;
         --checkerboard-dark-square: hsl(0, 0%, 80%);
-        background-image: linear-gradient(315deg, transparent 75%, var(--checkerboard-dark-square) 75%), linear-gradient(45deg, transparent 75%, var(--checkerboard-dark-square) 75%), linear-gradient(315deg, var(--checkerboard-dark-square) 25%, transparent 25%), linear-gradient(45deg, var(--checkerboard-dark-square) 25%, transparent 25%);
     }
 }