browser_styleeditor_at_rules_sidebar.js (12201B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // https rather than chrome to improve coverage 7 const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html"; 8 const SIDEBAR_PREF = "devtools.styleeditor.showAtRulesSidebar"; 9 10 const RESIZE_W = 300; 11 const RESIZE_H = 450; 12 const LABELS = [ 13 "not all", 14 "all", 15 "(max-width: 550px)", 16 "(min-height: 300px) and (max-height: 320px)", 17 "(max-width: 750px)", 18 "", 19 "print", 20 ]; 21 const LINE_NOS = [1, 7, 19, 25, 31, 34, 39]; 22 const NEW_RULE = ` 23 @media (max-width: 750px) { 24 div { 25 color: blue; 26 @layer { 27 border-color: tomato; 28 } 29 } 30 31 @media print { 32 body { 33 filter: grayscale(100%); 34 } 35 } 36 }`; 37 38 waitForExplicitFinish(); 39 40 add_task(async function () { 41 // Enable @property rules 42 await pushPref("layout.css.properties-and-values.enabled", true); 43 // Enable anchor positioning 44 await pushPref("layout.css.anchor-positioning.enabled", true); 45 // Enable @custom-media 46 await pushPref("layout.css.custom-media.enabled", true); 47 48 const { ui } = await openStyleEditorForURL(TESTCASE_URI); 49 50 is(ui.editors.length, 4, "correct number of editors"); 51 52 info("Test first plain css editor"); 53 const plainEditor = ui.editors[0]; 54 await openEditor(plainEditor); 55 testPlainEditor(plainEditor); 56 57 info("Test editor for inline sheet with at-rules"); 58 const inlineAtRulesEditor = ui.editors[3]; 59 await openEditor(inlineAtRulesEditor); 60 await testInlineAtRulesEditor(ui, inlineAtRulesEditor); 61 62 info("Test editor with @media rules"); 63 const mediaEditor = ui.editors[1]; 64 await openEditor(mediaEditor); 65 await testMediaEditor(ui, mediaEditor); 66 67 info("Test that sidebar hides when flipping pref"); 68 await testShowHide(ui, mediaEditor); 69 70 info("Test adding a rule updates the list"); 71 await testMediaRuleAdded(ui, mediaEditor); 72 73 info("Test resizing and seeing @media matching state change"); 74 const originalWidth = window.outerWidth; 75 const originalHeight = window.outerHeight; 76 77 const onMatchesChange = ui.once("at-rules-list-changed"); 78 window.resizeTo(RESIZE_W, RESIZE_H); 79 await onMatchesChange; 80 81 testMediaMatchChanged(mediaEditor); 82 83 window.resizeTo(originalWidth, originalHeight); 84 }); 85 86 function testPlainEditor(editor) { 87 const sidebar = editor.details.querySelector(".stylesheet-sidebar"); 88 is(sidebar.hidden, true, "sidebar is hidden on editor without @media"); 89 } 90 91 async function testInlineAtRulesEditor(ui, editor) { 92 const sidebar = editor.details.querySelector(".stylesheet-sidebar"); 93 is(sidebar.hidden, false, "sidebar is showing on editor with @media"); 94 95 const entries = sidebar.querySelectorAll(".at-rule-label"); 96 is(entries.length, 14, "14 at-rules displayed in sidebar"); 97 98 await testRule({ 99 ui, 100 editor, 101 rule: entries[0], 102 conditionText: "screen", 103 matches: true, 104 line: 2, 105 type: "media", 106 }); 107 108 await testRule({ 109 ui, 110 editor, 111 rule: entries[1], 112 conditionText: "(display: flex)", 113 line: 7, 114 type: "support", 115 }); 116 117 await testRule({ 118 ui, 119 editor, 120 rule: entries[2], 121 conditionText: "(1px < height < 10000px)", 122 matches: true, 123 line: 8, 124 type: "media", 125 }); 126 127 await testRule({ 128 ui, 129 editor, 130 rule: entries[3], 131 line: 16, 132 type: "layer", 133 layerName: "myLayer", 134 }); 135 136 await testRule({ 137 ui, 138 editor, 139 rule: entries[4], 140 conditionText: "(min-width: 1px)", 141 line: 17, 142 type: "container", 143 }); 144 145 await testRule({ 146 ui, 147 editor, 148 rule: entries[5], 149 conditionText: "selector(&)", 150 line: 21, 151 type: "support", 152 }); 153 154 await testRule({ 155 ui, 156 editor, 157 rule: entries[6], 158 line: 30, 159 type: "property", 160 propertyName: "--my-property", 161 }); 162 163 await testRule({ 164 ui, 165 editor, 166 rule: entries[7], 167 line: 36, 168 type: "position-try", 169 positionTryName: "--pt-custom-bottom", 170 }); 171 172 await testRule({ 173 ui, 174 editor, 175 rule: entries[8], 176 line: 42, 177 type: "custom-media", 178 customMediaName: "--mobile-breakpoint", 179 customMediaQuery: [ 180 { text: "(width < 320px) and (height < 1420px)" }, 181 { text: ", " }, 182 { text: "not print" }, 183 ], 184 }); 185 186 await testRule({ 187 ui, 188 editor, 189 rule: entries[9], 190 line: 43, 191 type: "custom-media", 192 customMediaName: "--enabled", 193 customMediaQuery: [{ text: "true" }], 194 }); 195 196 await testRule({ 197 ui, 198 editor, 199 rule: entries[10], 200 line: 44, 201 type: "custom-media", 202 customMediaName: "--disabled", 203 customMediaQuery: [{ text: "false", matches: false }], 204 }); 205 206 await testRule({ 207 ui, 208 editor, 209 rule: entries[11], 210 line: 49, 211 type: "media", 212 conditionText: "(--mobile-breakpoint)", 213 matches: false, 214 }); 215 216 await testRule({ 217 ui, 218 editor, 219 rule: entries[12], 220 line: 53, 221 type: "media", 222 conditionText: "(--enabled)", 223 matches: false, 224 }); 225 226 await testRule({ 227 ui, 228 editor, 229 rule: entries[13], 230 line: 57, 231 type: "media", 232 conditionText: "(--disabled)", 233 matches: false, 234 }); 235 } 236 237 async function testMediaEditor(ui, editor) { 238 const sidebar = editor.details.querySelector(".stylesheet-sidebar"); 239 is(sidebar.hidden, false, "sidebar is showing on editor with @media"); 240 241 const entries = [...sidebar.querySelectorAll(".at-rule-label")]; 242 is(entries.length, 4, "four @media rules displayed in sidebar"); 243 244 await testRule({ 245 ui, 246 editor, 247 rule: entries[0], 248 conditionText: LABELS[0], 249 matches: false, 250 line: LINE_NOS[0], 251 }); 252 await testRule({ 253 ui, 254 editor, 255 rule: entries[1], 256 conditionText: LABELS[1], 257 matches: true, 258 line: LINE_NOS[1], 259 }); 260 await testRule({ 261 ui, 262 editor, 263 rule: entries[2], 264 conditionText: LABELS[2], 265 matches: false, 266 line: LINE_NOS[2], 267 }); 268 await testRule({ 269 ui, 270 editor, 271 rule: entries[3], 272 conditionText: LABELS[3], 273 matches: false, 274 line: LINE_NOS[3], 275 }); 276 } 277 278 function testMediaMatchChanged(editor) { 279 const sidebar = editor.details.querySelector(".stylesheet-sidebar"); 280 281 const cond = sidebar.querySelectorAll(".at-rule-condition")[2]; 282 is( 283 cond.textContent, 284 "(max-width: 550px)", 285 "third rule condition text is correct" 286 ); 287 ok( 288 !cond.classList.contains("media-condition-unmatched"), 289 "media rule is now matched after resizing" 290 ); 291 } 292 293 async function testShowHide(ui, editor) { 294 let sidebarChange = ui.once("at-rules-list-changed"); 295 Services.prefs.setBoolPref(SIDEBAR_PREF, false); 296 await sidebarChange; 297 298 const sidebar = editor.details.querySelector(".stylesheet-sidebar"); 299 is(sidebar.hidden, true, "sidebar is hidden after flipping pref"); 300 301 sidebarChange = ui.once("at-rules-list-changed"); 302 Services.prefs.clearUserPref(SIDEBAR_PREF); 303 await sidebarChange; 304 305 is(sidebar.hidden, false, "sidebar is showing after flipping pref back"); 306 } 307 308 async function testMediaRuleAdded(ui, editor) { 309 await editor.getSourceEditor(); 310 const sidebar = editor.details.querySelector(".stylesheet-sidebar"); 311 is( 312 sidebar.querySelectorAll(".at-rule-label").length, 313 4, 314 "4 @media rules after changing text" 315 ); 316 317 let text = editor.sourceEditor.getText(); 318 text += NEW_RULE; 319 320 const listChange = ui.once("at-rules-list-changed"); 321 editor.sourceEditor.setText(text); 322 await listChange; 323 324 const entries = [...sidebar.querySelectorAll(".at-rule-label")]; 325 is(entries.length, 7, "7 @media rules after changing text"); 326 327 await testRule({ 328 ui, 329 editor, 330 rule: entries[4], 331 conditionText: LABELS[4], 332 matches: false, 333 line: LINE_NOS[4], 334 }); 335 336 await testRule({ 337 ui, 338 editor, 339 rule: entries[5], 340 type: "layer", 341 conditionText: LABELS[5], 342 line: LINE_NOS[5], 343 }); 344 345 await testRule({ 346 ui, 347 editor, 348 rule: entries[6], 349 conditionText: LABELS[6], 350 matches: false, 351 line: LINE_NOS[6], 352 }); 353 } 354 355 /** 356 * Run assertion on given rule 357 * 358 * @param {object} options 359 * @param {StyleEditorUI} options.ui 360 * @param {StyleSheetEditor} options.editor: The editor the rule is displayed in 361 * @param {Element} options.rule: The rule element in the media sidebar 362 * @param {string} options.conditionText: at-rule condition text (for @media, @container, @support) 363 * @param {boolean} options.matches: Whether or not the document matches the rule 364 * @param {string} options.layerName: Optional name of the @layer 365 * @param {string} options.positionTryName: Name of the @position-try if type is "position-try" 366 * @param {string} options.propertyName: Name of the @property if type is "property" 367 * @param {string} options.customMediaName: Name of the @custom-media if type is "custom-media" 368 * @param {Array<object>} options.customMediaQuery: query parts of the @custom-media if type is "custom-media" 369 * @param {string} options.customMediaQuery[].text: the query string of the part of the @custom-media 370 * if type is "custom-media" 371 * @param {boolean} options.customMediaQuery[].matches: whether or not this part is style as matching, 372 * if type is "custom-media". Defaults to true. 373 * @param {number} options.line: Line of the rule 374 * @param {string} options.type: The type of the rule (container, layer, media, support, property ). 375 * Defaults to "media". 376 */ 377 async function testRule({ 378 ui, 379 editor, 380 rule, 381 conditionText = "", 382 matches, 383 layerName, 384 positionTryName, 385 propertyName, 386 customMediaName, 387 customMediaQuery, 388 line, 389 type = "media", 390 }) { 391 const atTypeEl = rule.querySelector(".at-rule-type"); 392 let name; 393 if (type === "layer") { 394 name = layerName; 395 } else if (type === "property") { 396 name = propertyName; 397 } else if (type === "position-try") { 398 name = positionTryName; 399 } 400 401 if (type === "custom-media") { 402 const atTypeChilNodes = Array.from(atTypeEl.childNodes); 403 is( 404 atTypeChilNodes.shift().textContent, 405 `@custom-media\u00A0`, 406 "label for @custom-media is correct" 407 ); 408 is( 409 atTypeChilNodes.shift().textContent, 410 `${customMediaName} `, 411 "name for @custom-media is correct" 412 ); 413 is( 414 atTypeChilNodes.length, 415 customMediaQuery.length, 416 `Got expected number of children of @custom-media (got ${JSON.stringify(atTypeChilNodes.map(n => n.textContent))})` 417 ); 418 for (let i = 0; i < atTypeChilNodes.length; i++) { 419 const node = atTypeChilNodes[i]; 420 is( 421 node.textContent, 422 customMediaQuery[i].text, 423 `Got expected text for part #${i} of @custom-media` 424 ); 425 if (customMediaQuery[i].matches ?? true) { 426 ok( 427 // handle TextNode 428 !node.classList || 429 !node.classList.contains("media-condition-unmatched"), 430 `Text for part #${i} of @custom-media ("${node.textContent}") does not have unmatching class` 431 ); 432 } else { 433 ok( 434 node.classList.contains("media-condition-unmatched"), 435 `Text for part #${i} of @custom-media ("${node.textContent}") has expected unmatching class` 436 ); 437 } 438 } 439 } else { 440 is( 441 atTypeEl.textContent, 442 `@${type}\u00A0${name ? `${name}\u00A0` : ""}`, 443 "label for at-rule type is correct" 444 ); 445 } 446 447 const cond = rule.querySelector(".at-rule-condition"); 448 is( 449 cond.textContent, 450 conditionText, 451 "condition label is correct for " + conditionText 452 ); 453 454 if (type == "media") { 455 const matched = !cond.classList.contains("media-condition-unmatched"); 456 ok( 457 matches ? matched : !matched, 458 "media rule is " + (matches ? "matched" : "unmatched") 459 ); 460 } 461 462 const ruleLine = rule.querySelector(".at-rule-line"); 463 is(ruleLine.textContent, ":" + line, "correct line number shown"); 464 465 info( 466 "Check that clicking on the rule jumps to the expected position in the stylesheet" 467 ); 468 rule.click(); 469 await waitFor( 470 () => 471 ui.selectedEditor == editor && 472 editor.sourceEditor.getCursor().line == line - 1 473 ); 474 ok(true, "Jumped to the expected location"); 475 } 476 477 /* Helpers */ 478 479 function openEditor(editor) { 480 getLinkFor(editor).click(); 481 482 return editor.getSourceEditor(); 483 } 484 485 function getLinkFor(editor) { 486 return editor.summary.querySelector(".stylesheet-name"); 487 }