browser_webconsole_context_menu_copy_object.js (8480B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 // Test the "Copy object" menu item of the webconsole is enabled only when 5 // clicking on messages that are associated with an object actor. 6 7 "use strict"; 8 9 const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html><script> 10 window.bar = { baz: 1 }; 11 console.log("foo"); 12 console.log("foo", window.bar); 13 console.log(["foo", window.bar, 2]); 14 console.group("group"); 15 console.groupCollapsed("collapsed"); 16 console.groupEnd(); 17 console.log(532); 18 console.log(true); 19 console.log(false); 20 console.log(undefined); 21 console.log(null); 22 /* Verify that the conflicting binding on user code doesn't break the 23 * functionality. */ 24 function copy() { alert("user-defined function is called"); } 25 /* Check that trying to copy an object that can't be serialized displays an error in the UI */ 26 var cyclical = {}; cyclical.cycle = cyclical; 27 console.log(cyclical); 28 29 /* Verify that custom formatters don't break copying. */ 30 window.devtoolsFormatters = [ 31 { 32 header: (obj, config) => { 33 if (!obj?.useCustomFormatter) 34 return null; 35 return [ 36 "span", 37 { "style": "color: red" }, 38 "Hello, ", 39 [ 40 "span", 41 { "style": "color: green" }, 42 "world!" 43 ] 44 ]; 45 }, 46 hasBody: (obj) => { 47 return false; 48 }, 49 } 50 ]; 51 console.log({ useCustomFormatter: true, a: 5 }); 52 </script>`; 53 const copyObjectMenuItemId = "#console-menu-copy-object"; 54 55 add_task(async function () { 56 await pushPref("devtools.custom-formatters.enabled", true); 57 58 const hud = await openNewTabAndConsole(TEST_URI); 59 60 // Reload the browser to ensure the custom formatters are picked up 61 await reloadBrowser(); 62 63 const [msgWithText, msgWithObj, msgNested] = await waitFor(() => 64 findConsoleAPIMessages(hud, "foo") 65 ); 66 ok( 67 msgWithText && msgWithObj && msgNested, 68 "Three messages should have appeared" 69 ); 70 71 const [groupMsgObj] = await waitFor(() => 72 findMessagePartsByType(hud, { 73 text: "group", 74 typeSelector: ".console-api", 75 partSelector: ".message-body", 76 }) 77 ); 78 const [collapsedGroupMsgObj] = await waitFor(() => 79 findMessagePartsByType(hud, { 80 text: "collapsed", 81 typeSelector: ".console-api", 82 partSelector: ".message-body", 83 }) 84 ); 85 const [numberMsgObj] = await waitFor(() => 86 findMessagePartsByType(hud, { 87 text: `532`, 88 typeSelector: ".console-api", 89 partSelector: ".message-body", 90 }) 91 ); 92 const [trueMsgObj] = await waitFor(() => 93 findMessagePartsByType(hud, { 94 text: `true`, 95 typeSelector: ".console-api", 96 partSelector: ".message-body", 97 }) 98 ); 99 const [falseMsgObj] = await waitFor(() => 100 findMessagePartsByType(hud, { 101 text: `false`, 102 typeSelector: ".console-api", 103 partSelector: ".message-body", 104 }) 105 ); 106 const [undefinedMsgObj] = await waitFor(() => 107 findMessagePartsByType(hud, { 108 text: `undefined`, 109 typeSelector: ".console-api", 110 partSelector: ".message-body", 111 }) 112 ); 113 const [nullMsgObj] = await waitFor(() => 114 findMessagePartsByType(hud, { 115 text: `null`, 116 typeSelector: ".console-api", 117 partSelector: ".message-body", 118 }) 119 ); 120 const [customMsgObj] = await waitFor(() => 121 findMessagePartsByType(hud, { 122 text: `Hello, world!`, 123 typeSelector: ".console-api", 124 partSelector: ".message-body", 125 }) 126 ); 127 ok(nullMsgObj, "One message with null value should have appeared"); 128 129 const text = msgWithText.querySelector(".objectBox-string"); 130 const objInMsgWithObj = msgWithObj.querySelector(".objectBox-object"); 131 const textInMsgWithObj = msgWithObj.querySelector(".objectBox-string"); 132 133 // The third message has an object nested in an array, the array is therefore the top 134 // object, the object is the nested object. 135 const topObjInMsg = msgNested.querySelector(".objectBox-array"); 136 const nestedObjInMsg = msgNested.querySelector(".objectBox-object"); 137 138 const consoleMessages = await waitFor(() => 139 findMessagePartsByType(hud, { 140 text: 'console.log("foo");', 141 typeSelector: ".console-api", 142 partSelector: ".message-location", 143 }) 144 ); 145 await testCopyObjectMenuItemDisabled(hud, consoleMessages[0]); 146 147 info(`Check "Copy object" is enabled for text only messages 148 thus copying the text`); 149 await testCopyObject(hud, text, `foo`, false); 150 151 info(`Check "Copy object" is enabled for text in complex messages 152 thus copying the text`); 153 await testCopyObject(hud, textInMsgWithObj, `foo`, false); 154 155 info("Check `Copy object` is enabled for objects in complex messages"); 156 await testCopyObject(hud, objInMsgWithObj, `{"baz":1}`, true); 157 158 info("Check `Copy object` is enabled for top object in nested messages"); 159 await testCopyObject(hud, topObjInMsg, `["foo",{"baz":1},2]`, true); 160 161 info("Check `Copy object` is enabled for nested object in nested messages"); 162 await testCopyObject(hud, nestedObjInMsg, `{"baz":1}`, true); 163 164 info("Check `Copy object` is disabled on `console.group('group')` messages"); 165 await testCopyObjectMenuItemDisabled(hud, groupMsgObj); 166 167 info(`Check "Copy object" is disabled in "console.groupCollapsed('collapsed')" 168 messages`); 169 await testCopyObjectMenuItemDisabled(hud, collapsedGroupMsgObj); 170 171 // Check for primitive objects 172 info("Check `Copy object` is enabled for numbers"); 173 await testCopyObject(hud, numberMsgObj, `532`, false); 174 175 info("Check `Copy object` is enabled for booleans"); 176 await testCopyObject(hud, trueMsgObj, `true`, false); 177 await testCopyObject(hud, falseMsgObj, `false`, false); 178 179 info("Check `Copy object` is enabled for undefined and null"); 180 await testCopyObject(hud, undefinedMsgObj, `undefined`, false); 181 await testCopyObject(hud, nullMsgObj, `null`, false); 182 183 info("Check `Copy object` is enabled for custom-formatted objects"); 184 await testCopyObject( 185 hud, 186 customMsgObj, 187 `{"useCustomFormatter":true,"a":5}`, 188 true 189 ); 190 191 info( 192 "Check `Copy object` for an object with cyclical reference displays an error in the UI" 193 ); 194 const clipboardContent = SpecialPowers.getClipboardData("text/plain"); 195 const [cyclicalMsgObj] = await waitFor(() => 196 findMessagePartsByType(hud, { 197 text: `cycle`, 198 typeSelector: ".console-api", 199 partSelector: ".message-body", 200 }) 201 ); 202 const menuPopup = await openContextMenu( 203 hud, 204 cyclicalMsgObj.querySelector(".objectBox-object") 205 ); 206 menuPopup.activateItem(menuPopup.querySelector(copyObjectMenuItemId)); 207 208 info("Wait until the notification box displays the error"); 209 const notificationBox = await waitFor(() => 210 hud.ui.document.getElementById("webconsole-notificationbox") 211 ); 212 is( 213 notificationBox.querySelector(".notification").textContent, 214 "`copy` command failed, object can’t be stringified: TypeError: cyclic object value", 215 "Notification is displayed with expected message" 216 ); 217 218 // Wait for a bit to check the clipboard isn't overridden 219 await wait(500); 220 is( 221 SpecialPowers.getClipboardData("text/plain"), 222 clipboardContent, 223 "clipboard wasn't overridden" 224 ); 225 }); 226 227 async function testCopyObject(hud, element, expectedMessage, objectInput) { 228 info("Check `Copy object` is enabled"); 229 const menuPopup = await openContextMenu(hud, element); 230 const copyObjectMenuItem = menuPopup.querySelector(copyObjectMenuItemId); 231 ok( 232 !copyObjectMenuItem.disabled, 233 "`Copy object` is enabled for object in complex message" 234 ); 235 is( 236 copyObjectMenuItem.getAttribute("accesskey"), 237 "o", 238 "`Copy object` has the right accesskey" 239 ); 240 241 const validatorFn = data => { 242 const prettifiedMessage = prettyPrintMessage(expectedMessage, objectInput); 243 return data === prettifiedMessage; 244 }; 245 246 info("Activate item `Copy object`"); 247 await waitForClipboardPromise( 248 () => menuPopup.activateItem(copyObjectMenuItem), 249 validatorFn 250 ); 251 } 252 253 async function testCopyObjectMenuItemDisabled(hud, element) { 254 const menuPopup = await openContextMenu(hud, element); 255 const copyObjectMenuItem = menuPopup.querySelector(copyObjectMenuItemId); 256 ok( 257 copyObjectMenuItem.disabled, 258 `"Copy object" is disabled for messages 259 with no variables/objects` 260 ); 261 await hideContextMenu(hud); 262 } 263 264 function prettyPrintMessage(message, isObject) { 265 return isObject ? JSON.stringify(JSON.parse(message), null, 2) : message; 266 }