head.js (9663B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 /* eslint no-unused-vars: [2, {"vars": "local"}] */ 5 6 /* global _snapshots */ 7 8 "use strict"; 9 10 var { require } = ChromeUtils.importESModule( 11 "resource://devtools/shared/loader/Loader.sys.mjs" 12 ); 13 var { Assert } = ChromeUtils.importESModule( 14 "resource://testing-common/Assert.sys.mjs" 15 ); 16 var { gDevTools } = require("resource://devtools/client/framework/devtools.js"); 17 var { BrowserLoader } = ChromeUtils.importESModule( 18 "resource://devtools/shared/loader/browser-loader.sys.mjs" 19 ); 20 var { 21 DevToolsServer, 22 } = require("resource://devtools/server/devtools-server.js"); 23 var { 24 DevToolsClient, 25 } = require("resource://devtools/client/devtools-client.js"); 26 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 27 var { Toolbox } = require("resource://devtools/client/framework/toolbox.js"); 28 29 var { require: browserRequire } = BrowserLoader({ 30 baseURI: "resource://devtools/client/shared/", 31 window, 32 }); 33 34 const React = browserRequire("devtools/client/shared/vendor/react"); 35 const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom"); 36 const dom = browserRequire("devtools/client/shared/vendor/react-dom-factories"); 37 const TestUtils = browserRequire( 38 "devtools/client/shared/vendor/react-dom-test-utils" 39 ); 40 41 const TestRenderer = browserRequire( 42 "devtools/client/shared/vendor/react-test-renderer" 43 ); 44 45 var EXAMPLE_URL = "https://example.com/browser/browser/devtools/shared/test/"; 46 47 SimpleTest.registerCleanupFunction(() => { 48 window._snapshots = null; 49 }); 50 51 function forceRender(comp) { 52 return setState(comp, {}).then(() => setState(comp, {})); 53 } 54 55 // All tests are asynchronous. 56 SimpleTest.waitForExplicitFinish(); 57 58 function onNextAnimationFrame(fn) { 59 return () => requestAnimationFrame(() => requestAnimationFrame(fn)); 60 } 61 62 function setState(component, newState) { 63 return new Promise(resolve => { 64 component.setState(newState, onNextAnimationFrame(resolve)); 65 }); 66 } 67 68 function dumpn(msg) { 69 dump(`SHARED-COMPONENTS-TEST: ${msg}\n`); 70 } 71 72 /** 73 * Tree View 74 */ 75 76 const TEST_TREE_VIEW = { 77 A: { label: "A", value: "A" }, 78 B: { label: "B", value: "B" }, 79 C: { label: "C", value: "C" }, 80 D: { label: "D", value: "D" }, 81 E: { label: "E", value: "E" }, 82 F: { label: "F", value: "F" }, 83 G: { label: "G", value: "G" }, 84 H: { label: "H", value: "H" }, 85 I: { label: "I", value: "I" }, 86 J: { label: "J", value: "J" }, 87 K: { label: "K", value: "K" }, 88 L: { label: "L", value: "L" }, 89 }; 90 91 TEST_TREE_VIEW.children = { 92 A: [TEST_TREE_VIEW.B, TEST_TREE_VIEW.C, TEST_TREE_VIEW.D], 93 B: [TEST_TREE_VIEW.E, TEST_TREE_VIEW.F, TEST_TREE_VIEW.G], 94 C: [TEST_TREE_VIEW.H, TEST_TREE_VIEW.I], 95 D: [TEST_TREE_VIEW.J], 96 E: [TEST_TREE_VIEW.K, TEST_TREE_VIEW.L], 97 F: [], 98 G: [], 99 H: [], 100 I: [], 101 J: [], 102 K: [], 103 L: [], 104 }; 105 106 const TEST_TREE_VIEW_INTERFACE = { 107 provider: { 108 getChildren: x => TEST_TREE_VIEW.children[x.label], 109 hasChildren: x => !!TEST_TREE_VIEW.children[x.label].length, 110 getLabel: x => x.label, 111 getValue: x => x.value, 112 getKey: x => x.label, 113 getType: () => "string", 114 }, 115 object: TEST_TREE_VIEW.A, 116 columns: [{ id: "default" }, { id: "value" }], 117 }; 118 119 /** 120 * Tree 121 */ 122 123 var TEST_TREE_INTERFACE = { 124 getParent: x => TEST_TREE.parent[x], 125 getChildren: x => TEST_TREE.children[x], 126 renderItem: (x, depth, focused) => 127 "-".repeat(depth) + x + ":" + focused + "\n", 128 getRoots: () => ["A", "M"], 129 getKey: x => "key-" + x, 130 itemHeight: 1, 131 onExpand: x => TEST_TREE.expanded.add(x), 132 onCollapse: x => TEST_TREE.expanded.delete(x), 133 isExpanded: x => TEST_TREE.expanded.has(x), 134 }; 135 136 function isRenderedTree(actual, expectedDescription, msg) { 137 const expected = expectedDescription.map(x => x + "\n").join(""); 138 dumpn(`Expected tree:\n${expected}`); 139 dumpn(`Actual tree:\n${actual}`); 140 is(actual, expected, msg); 141 } 142 143 function isAccessibleTree(tree, options = {}) { 144 const treeNode = tree.refs.tree; 145 is(treeNode.getAttribute("tabindex"), "0", "Tab index is set"); 146 is(treeNode.getAttribute("role"), "tree", "Tree semantics is present"); 147 if (options.hasActiveDescendant) { 148 ok( 149 treeNode.hasAttribute("aria-activedescendant"), 150 "Tree has an active descendant set" 151 ); 152 } 153 154 const treeNodes = [...treeNode.querySelectorAll(".tree-node")]; 155 for (const node of treeNodes) { 156 ok(node.id, "TreeNode has an id"); 157 is(node.getAttribute("role"), "treeitem", "Tree item semantics is present"); 158 is( 159 parseInt(node.getAttribute("aria-level"), 10), 160 parseInt(node.getAttribute("data-depth"), 10) + 1, 161 "Aria level attribute is set correctly" 162 ); 163 } 164 } 165 166 // Encoding of the following tree/forest: 167 // 168 // A 169 // |-- B 170 // | |-- E 171 // | | |-- K 172 // | | `-- L 173 // | |-- F 174 // | `-- G 175 // |-- C 176 // | |-- H 177 // | `-- I 178 // `-- D 179 // `-- J 180 // M 181 // `-- N 182 // `-- O 183 var TEST_TREE = { 184 children: { 185 A: ["B", "C", "D"], 186 B: ["E", "F", "G"], 187 C: ["H", "I"], 188 D: ["J"], 189 E: ["K", "L"], 190 F: [], 191 G: [], 192 H: [], 193 I: [], 194 J: [], 195 K: [], 196 L: [], 197 M: ["N"], 198 N: ["O"], 199 O: [], 200 }, 201 parent: { 202 A: null, 203 B: "A", 204 C: "A", 205 D: "A", 206 E: "B", 207 F: "B", 208 G: "B", 209 H: "C", 210 I: "C", 211 J: "D", 212 K: "E", 213 L: "E", 214 M: null, 215 N: "M", 216 O: "N", 217 }, 218 expanded: new Set(), 219 }; 220 221 /** 222 * Frame 223 */ 224 function checkFrameString({ 225 el, 226 file, 227 line, 228 column, 229 source, 230 functionName, 231 shouldLink, 232 tooltip, 233 locationPrefix, 234 }) { 235 const $ = selector => el.querySelector(selector); 236 237 const $func = $(".frame-link-function-display-name"); 238 const $source = $(".frame-link-source"); 239 const $locationPrefix = $(".frame-link-prefix"); 240 const $filename = $(".frame-link-filename"); 241 const $line = $(".frame-link-line"); 242 243 is($filename.textContent, file, "Correct filename"); 244 is( 245 el.getAttribute("data-line"), 246 line ? `${line}` : null, 247 "Expected `data-line` found" 248 ); 249 is( 250 el.getAttribute("data-column"), 251 column ? `${column}` : null, 252 "Expected `data-column` found" 253 ); 254 is($source.getAttribute("title"), tooltip, "Correct tooltip"); 255 is($source.tagName, shouldLink ? "A" : "SPAN", "Correct linkable status"); 256 if (shouldLink) { 257 is($source.getAttribute("href"), source, "Correct source"); 258 } 259 260 if (line != null) { 261 let lineText = `:${line}`; 262 if (column != null) { 263 lineText += `:${column}`; 264 } 265 266 is($line.textContent, lineText, "Correct line number"); 267 } else { 268 ok(!$line, "Should not have an element for `line`"); 269 } 270 271 if (functionName != null) { 272 is($func.textContent, functionName, "Correct function name"); 273 } else { 274 ok(!$func, "Should not have an element for `functionName`"); 275 } 276 277 if (locationPrefix != null) { 278 is($locationPrefix.textContent, locationPrefix, "Correct prefix"); 279 } else { 280 ok(!$locationPrefix, "Should not have an element for `locationPrefix`"); 281 } 282 } 283 284 function checkSmartFrameString({ el, location, functionName, tooltip }) { 285 const $ = selector => el.querySelector(selector); 286 287 const $func = $(".title"); 288 const $location = $(".location"); 289 290 is($location.textContent, location, "Correct filename"); 291 is(el.getAttribute("title"), tooltip, "Correct tooltip"); 292 if (functionName != null) { 293 is($func.textContent, functionName, "Correct function name"); 294 } else { 295 ok(!$func, "Should not have an element for `functionName`"); 296 } 297 } 298 299 function renderComponent(component, props) { 300 const el = React.createElement(component, props, {}); 301 // By default, renderIntoDocument() won't work for stateless components, but 302 // it will work if the stateless component is wrapped in a stateful one. 303 // See https://github.com/facebook/react/issues/4839 304 const wrappedEl = dom.span({}, [el]); 305 const renderedComponent = TestUtils.renderIntoDocument(wrappedEl); 306 return ReactDOM.findDOMNode(renderedComponent).children[0]; 307 } 308 309 /** 310 * Creates a React Component for testing 311 * 312 * @param {string} factory - factory object of the component to be created 313 * @param {object} props - React props for the component 314 * @returns {object} - container Node, Object with React component 315 * and querySelector function with $ as name. 316 */ 317 async function createComponentTest(factory, props) { 318 const container = document.createElement("div"); 319 document.body.appendChild(container); 320 321 const component = ReactDOM.render(factory(props), container); 322 await forceRender(component); 323 324 return { 325 container, 326 component, 327 $: s => container.querySelector(s), 328 element: container.firstChild, 329 }; 330 } 331 332 async function waitFor(condition = () => true, delay = 50) { 333 do { 334 const res = condition(); 335 if (res) { 336 return res; 337 } 338 await new Promise(resolve => setTimeout(resolve, delay)); 339 } while (true); 340 } 341 342 /** 343 * Matches a component tree rendererd using TestRenderer to a given expected JSON 344 * snapshot. 345 * 346 * @param {string} name 347 * Name of the function derived from a test [step] name. 348 * @param {object} el 349 * React element to be rendered using TestRenderer. 350 */ 351 function matchSnapshot(name, el) { 352 if (!_snapshots) { 353 is(false, "No snapshots were loaded into test."); 354 } 355 356 const snapshot = _snapshots[name]; 357 if (snapshot === undefined) { 358 is(false, `Snapshot for "${name}" not found.`); 359 } 360 361 const renderer = TestRenderer.create(el, {}); 362 const tree = renderer.toJSON(); 363 364 is( 365 JSON.stringify(tree, (key, value) => 366 typeof value === "function" ? value.toString() : value 367 ), 368 JSON.stringify(snapshot), 369 name 370 ); 371 }