browser_rules_variables-jump-to-definition.js (14223B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Tests that the rule view custom properties (css variables) jump to definition works 7 // as expected. 8 9 const TEST_URI = ` 10 <style type='text/css'> 11 @property --my-registered-color { 12 syntax: "<color>"; 13 inherits: true; 14 initial-value: gold; 15 } 16 17 :root { 18 --my-color-1: tomato; 19 --my-color-2: cyan; 20 --my-color-3: green; 21 } 22 23 @starting-style { 24 #testid { 25 --my-color-1: hotpink; 26 } 27 } 28 29 #testid { 30 color: var(--my-color-1); 31 background-color: var(--my-registered-color); 32 border-width: 1px; 33 outline-width: var(--undefined); 34 35 @starting-style { 36 outline-color: var(--my-color-1, var(--my-color-2)); 37 } 38 } 39 40 h1 { 41 background-color: linear-gradient(var(--my-color-1), var(--my-color-2, var(--my-color-3))); 42 } 43 44 h2 { 45 --my-color-2: chartreuse; 46 color: var(--my-color-1); 47 } 48 49 h2::after { 50 --my-color-1: azure; 51 content: "-"; 52 color: var(--my-color-1); 53 } 54 55 h2::before { 56 --my-color-1: blueviolet; 57 content: "+"; 58 color: var(--my-color-1, var(--my-color-2, var(--my-color-3))); 59 } 60 61 </style> 62 <h1 id='testid'>Styled Node</h1> 63 <h2>sub</h2> 64 `; 65 66 add_task(async function () { 67 await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); 68 const { inspector, view } = await openRuleView(); 69 await selectNode("h1#testid", inspector); 70 71 info("Check that the correct rules are visible"); 72 is( 73 view.styleDocument.querySelectorAll(`.ruleview-rule`).length, 74 7, 75 "Should have 7 rules." 76 ); 77 78 let rule = getRuleViewRuleEditor(view, 2).rule; 79 is(rule.selectorText, "#testid", "Second rule is #testid."); 80 81 info( 82 "Check that property not using variable don't have a jump to definition button" 83 ); 84 let variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, { 85 "border-width": "1px", 86 }); 87 is( 88 variableButtonEls.length, 89 0, 90 "border-width property does not have custom properties and no variable jump to definition is displayed." 91 ); 92 93 info( 94 "Check that property using unset variable don't have a jump to definition button" 95 ); 96 variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, { 97 "outline-width": "var(--undefined)", 98 }); 99 is( 100 variableButtonEls.length, 101 0, 102 "outline-width property has an undefined custom properties, so no variable jump to definition is displayed." 103 ); 104 105 info( 106 "Check that there's a jump to definition button for `color: var(--my-color-1)`" 107 ); 108 variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, { 109 color: "var(--my-color-1)", 110 }); 111 is( 112 variableButtonEls.length, 113 1, 114 "color property has custom property and variable jump to definition is displayed." 115 ); 116 117 info("Click the --my-color-1 jump to definition button"); 118 await highlightProperty(view, variableButtonEls[0], "--my-color-1", "tomato"); 119 120 info( 121 "Check that there's a jump to definition button for `background-color var(--my-registered-color)`" 122 ); 123 variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, { 124 "background-color": "var(--my-registered-color)", 125 }); 126 is( 127 variableButtonEls.length, 128 1, 129 "background-color property has a registered custom property and variable jump to definition is displayed." 130 ); 131 132 info("Click the --my-registered-color jump to definition button"); 133 // Collapse the `@property` section to check that it gets expanded when clicking on the 134 // jump to definition button. 135 const registerPropertyToggle = view.styleDocument.querySelector( 136 `[aria-controls="registered-properties-container"]` 137 ); 138 registerPropertyToggle.click(); 139 is( 140 registerPropertyToggle.ariaExpanded, 141 "false", 142 "@property section is collapsed" 143 ); 144 145 const onHighlightProperty = view.once("element-highlighted"); 146 variableButtonEls[0].click(); 147 const highlightedElement = await onHighlightProperty; 148 is( 149 highlightedElement.querySelector(".ruleview-registered-property-name") 150 .innerText, 151 "--my-registered-color", 152 "The expected element was highlighted" 153 ); 154 is( 155 registerPropertyToggle.ariaExpanded, 156 "true", 157 "@property section is expanded after clicking on the jump to definition button" 158 ); 159 160 info( 161 "Check that there are multiple jump to definition buttons when using multiple variables" 162 ); 163 rule = getRuleViewRuleEditor(view, 4).rule; 164 is(rule.selectorText, "h1", "Fifth rule is h1."); 165 variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, { 166 "background-color": 167 "linear-gradient(var(--my-color-1), var(--my-color-2, var(--my-color-3)))", 168 }); 169 Assert.deepEqual( 170 [...variableButtonEls].map(el => el.dataset.variableName), 171 ["--my-color-1", "--my-color-2", "--my-color-3"] 172 ); 173 174 info(`Click the "--my-color-2" variable jump to definition button`); 175 await highlightProperty(view, variableButtonEls[1], "--my-color-2", "cyan"); 176 177 info(`Click the fallback "--my-color-3" variable jump to definition button`); 178 await highlightProperty(view, variableButtonEls[2], "--my-color-3", "green"); 179 180 info("Check that we can jump in @starting-style rule`"); 181 rule = getRuleViewRuleEditor(view, 1).rule; 182 ok(rule.isInStartingStyle(), "Got expected starting style rule"); 183 variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, { 184 "outline-color": "var(--my-color-1, var(--my-color-2))", 185 }); 186 187 Assert.deepEqual( 188 [...variableButtonEls].map(el => el.dataset.variableName), 189 ["--my-color-1", "--my-color-2"] 190 ); 191 192 info( 193 "Click the --my-color-1 jump to definition button in @starting-style rule" 194 ); 195 await highlightProperty( 196 view, 197 variableButtonEls[0], 198 "--my-color-1", 199 "hotpink" 200 ); 201 202 info( 203 "Click the --my-color-2 jump to definition button in @starting-style rule" 204 ); 205 await highlightProperty(view, variableButtonEls[1], "--my-color-2", "cyan"); 206 207 info("Check that jump to definition works well with pseudo elements"); 208 await selectNode("h2", inspector); 209 210 info("Expand the pseudo element section"); 211 const pseudoElementToggle = view.styleDocument.querySelector( 212 `[aria-controls="pseudo-elements-container"]` 213 ); 214 // sanity check 215 is( 216 pseudoElementToggle.ariaExpanded, 217 "false", 218 "pseudo element section is collapsed at first" 219 ); 220 pseudoElementToggle.click(); 221 is( 222 pseudoElementToggle.ariaExpanded, 223 "true", 224 "pseudo element section is now expanded" 225 ); 226 227 rule = getRuleViewRuleEditor(view, 1, 0).rule; 228 is(rule.selectorText, "h2::after", "First rule is h2::after"); 229 230 await highlightProperty( 231 view, 232 getJumpToDefinitionButtonForDeclaration(rule, { 233 color: "var(--my-color-1)", 234 })[0], 235 "--my-color-1", 236 "azure" 237 ); 238 239 rule = getRuleViewRuleEditor(view, 1, 1).rule; 240 is(rule.selectorText, "h2::before", "First rule is h2::before"); 241 242 variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, { 243 color: "var(--my-color-1, var(--my-color-2, var(--my-color-3)))", 244 }); 245 await highlightProperty( 246 view, 247 variableButtonEls[0], 248 "--my-color-1", 249 // definition in h2::before 250 "blueviolet" 251 ); 252 await highlightProperty( 253 view, 254 variableButtonEls[1], 255 "--my-color-2", 256 // definition in h2 257 "chartreuse" 258 ); 259 await highlightProperty( 260 view, 261 variableButtonEls[2], 262 "--my-color-3", 263 // definition in :root 264 "green" 265 ); 266 267 rule = getRuleViewRuleEditor(view, 4).rule; 268 is(rule.selectorText, "h2", "Got expected h2 rule"); 269 await highlightProperty( 270 view, 271 getJumpToDefinitionButtonForDeclaration(rule, { 272 color: "var(--my-color-1)", 273 })[0], 274 "--my-color-1", 275 // definition in :root 276 "tomato" 277 ); 278 }); 279 280 add_task(async function checkClearSearch() { 281 const fillerDeclarations = Array.from({ length: 50 }, (_, i) => ({ 282 name: `line-height`, 283 value: i.toString(), 284 overridden: i !== 49, 285 })); 286 287 await addTab( 288 "data:text/html;charset=utf-8," + 289 encodeURIComponent(` 290 <style type='text/css'> 291 :root { 292 --my-color-1: tomato; 293 } 294 295 h1#title { 296 ${ 297 // Add a lot of declaration so the --my-color-1 298 // declaration would be out of view 299 fillerDeclarations 300 .map(({ name, value }) => `${name}: ${value};`) 301 .join("") 302 } 303 } 304 305 h1 { 306 --my-unique-var: var(--my-color-1); 307 } 308 </style> 309 <h1 id="title">Filter</h1> 310 `) 311 ); 312 313 const { inspector, view } = await openRuleView(); 314 await selectNode("h1", inspector); 315 316 info("Check that search is cleared when clicking on the jump button"); 317 await setSearchFilter(view, "--my-unique-var"); 318 319 // check that the rule view is filtered as expected 320 await checkRuleViewContent(view, [ 321 { selector: "element", selectorEditable: false, declarations: [] }, 322 { 323 selector: "h1", 324 declarations: [ 325 { 326 name: "--my-unique-var", 327 value: "var(--my-color-1)", 328 highlighted: true, 329 }, 330 ], 331 }, 332 ]); 333 const rule = getRuleViewRuleEditor(view, 1).rule; 334 is(rule.selectorText, "h1", "Got expected rule"); 335 await highlightProperty( 336 view, 337 getJumpToDefinitionButtonForDeclaration(rule, { 338 "--my-unique-var": "var(--my-color-1)", 339 })[0], 340 "--my-color-1", 341 // definition in :root 342 "tomato" 343 ); 344 is(view.searchField.value, "", "Search input was cleared"); 345 346 // check that the rule view is no longer filtered 347 await checkRuleViewContent(view, [ 348 { selector: "element", selectorEditable: false, declarations: [] }, 349 { selector: "h1#title", declarations: fillerDeclarations }, 350 { 351 selector: "h1", 352 declarations: [{ name: "--my-unique-var", value: "var(--my-color-1)" }], 353 }, 354 { 355 header: "Inherited from html", 356 }, 357 { 358 selector: ":root", 359 inherited: true, 360 declarations: [{ name: "--my-color-1", value: "tomato" }], 361 }, 362 ]); 363 }); 364 365 add_task(async function checkJumpToUnusedVariable() { 366 await addTab( 367 "data:text/html;charset=utf-8," + 368 encodeURIComponent(` 369 <style> 370 :where(h3) { 371 ${Array.from({ length: 15 }, (_, i) => `--unused-${i}: ${i};`).join("\n")} 372 } 373 374 h3 { 375 --another-unused: var(--unused-5); 376 } 377 </style> 378 <h3>for unused variables</h3> 379 `) 380 ); 381 382 const { inspector, view } = await openRuleView(); 383 await selectNode("h1", inspector); 384 385 info("Check that jump to definition of unused variables do work"); 386 // If you have 2 rules, one with hidden custom properties, and the other one with 387 // custom properties not being hidden because we're not entering the threshold 388 // Make sure the clicking the Jump to definition button will reveal the hidden property 389 await selectNode("h3", inspector); 390 391 await checkRuleViewContent(view, [ 392 { 393 selector: "element", 394 selectorEditable: false, 395 declarations: [], 396 }, 397 { 398 selector: "h3", 399 declarations: [{ name: "--another-unused", value: "var(--unused-5)" }], 400 }, 401 { 402 // Contains the hidden variables 403 selector: ":where(h3)", 404 // All the variables are hidden 405 declarations: [], 406 }, 407 ]); 408 409 is( 410 getUnusedVariableButton(view, 2)?.textContent, 411 "Show 15 unused custom CSS properties", 412 "Show unused variables button has expected text" 413 ); 414 415 const rule = getRuleViewRuleEditor(view, 1).rule; 416 is(rule.selectorText, "h3", "Got expected rule"); 417 418 const variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, { 419 "--another-unused": "var(--unused-5)", 420 }); 421 is(variableButtonEls.length, 1, "There's one jump to variable button"); 422 await highlightProperty(view, variableButtonEls[0], "--unused-5", "5"); 423 424 await checkRuleViewContent(view, [ 425 { 426 selector: "element", 427 selectorEditable: false, 428 declarations: [], 429 }, 430 { 431 selector: "h3", 432 declarations: [{ name: "--another-unused", value: "var(--unused-5)" }], 433 }, 434 { 435 // Contains the hidden variables 436 selector: ":where(h3)", 437 declarations: [{ name: "--unused-5", value: "5" }], 438 }, 439 ]); 440 441 is( 442 getUnusedVariableButton(view, 2)?.textContent, 443 "Show 14 unused custom CSS properties", 444 "Show unused variables button has expected text" 445 ); 446 }); 447 448 function getJumpToDefinitionButtonForDeclaration(rule, declaration) { 449 const [[name, value]] = Object.entries(declaration); 450 const textProp = rule.textProps.find(prop => { 451 return prop.name === name && prop.value === value; 452 }); 453 454 if (!textProp) { 455 throw Error(`Declaration ${name}:${value} not found on rule`); 456 } 457 458 return textProp.editor.element.querySelectorAll(".ruleview-variable-link"); 459 } 460 461 /** 462 * Click on the provided jump to definition button and wait for the element-highlighted 463 * event to be emitted. 464 * 465 * @param {RuleView} view 466 * @param {Element} jumpToDefinitionButton 467 * @param {string} expectedPropertyName: The name of the property that should be highlighted 468 * @param {string} expectedPropertyValue: The value of the property that should be highlighted 469 */ 470 async function highlightProperty( 471 view, 472 jumpToDefinitionButton, 473 expectedPropertyName, 474 expectedPropertyValue 475 ) { 476 info(`Highlight "${expectedPropertyName}: ${expectedPropertyValue}"`); 477 const onHighlightProperty = view.once("element-highlighted"); 478 jumpToDefinitionButton.click(); 479 const highlightedElement = await onHighlightProperty; 480 is( 481 highlightedElement.querySelector(".ruleview-propertyname").innerText, 482 expectedPropertyName, 483 "The expected element was highlighted" 484 ); 485 is( 486 highlightedElement.querySelector(".ruleview-propertyvalue").innerText, 487 expectedPropertyValue, 488 "The expected element was highlighted" 489 ); 490 491 // check that the declaration we jumped to is into view 492 ok( 493 isInViewport(highlightedElement, view.styleWindow), 494 `Highlighted element is in view` 495 ); 496 } 497 498 function isInViewport(element, win) { 499 const { top, left, bottom, right } = element.getBoundingClientRect(); 500 return ( 501 top >= 0 && 502 bottom <= win.innerHeight && 503 left >= 0 && 504 right <= win.innerWidth 505 ); 506 }