browser_spectrum.js (14889B)
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 spectrum color picker works correctly 7 8 const Spectrum = require("resource://devtools/client/shared/widgets/Spectrum.js"); 9 const { 10 accessibility: { 11 SCORES: { FAIL, AAA, AA }, 12 }, 13 } = require("resource://devtools/shared/constants.js"); 14 15 loader.lazyRequireGetter( 16 this, 17 "cssColors", 18 "resource://devtools/shared/css/color-db.js", 19 true 20 ); 21 22 const TEST_URI = CHROME_URL_ROOT + "doc_spectrum.html"; 23 const REGULAR_TEXT_PROPS = { 24 "font-size": { value: "11px" }, 25 "font-weight": { value: "bold" }, 26 opacity: { value: "1" }, 27 }; 28 const SINGLE_BG_COLOR = { 29 value: cssColors.white, 30 }; 31 const ZERO_ALPHA_COLOR = [0, 255, 255, 0]; 32 33 add_task(async function () { 34 const { host, doc } = await createHost("bottom", TEST_URI); 35 36 const container = doc.getElementById("spectrum-container"); 37 await testCreateAndDestroyShouldAppendAndRemoveElements(container); 38 await testPassingAColorAtInitShouldSetThatColor(container); 39 await testSettingAndGettingANewColor(container); 40 await testChangingColorShouldEmitEvents(container, doc); 41 await testSettingColorShoudUpdateTheUI(container); 42 await testChangingColorShouldUpdateColorPreview(container); 43 await testNotSettingTextPropsShouldNotShowContrastSection(container); 44 await testSettingTextPropsAndColorShouldUpdateContrastValue(container); 45 await testOnlySelectingLargeTextWithNonZeroAlphaShouldShowIndicator( 46 container 47 ); 48 await testSettingMultiColoredBackgroundShouldShowContrastRange(container); 49 50 host.destroy(); 51 }); 52 53 /** 54 * Helper method for extracting the rgba overlay value of the color preview's background 55 * image style. 56 * 57 * @param {string} linearGradientStr 58 * The linear gradient CSS string. 59 * @return {string} Returns the rgba string for the color overlay. 60 */ 61 function extractRgbaOverlayString(linearGradientStr) { 62 const start = linearGradientStr.indexOf("("); 63 const end = linearGradientStr.indexOf(")"); 64 65 return linearGradientStr.substring(start + 1, end + 1); 66 } 67 68 function testColorPreviewDisplay( 69 spectrum, 70 expectedRgbCssString, 71 expectedBorderColor 72 ) { 73 const { colorPreview } = spectrum; 74 const colorPreviewStyle = window.getComputedStyle(colorPreview); 75 expectedBorderColor = 76 expectedBorderColor === "transparent" 77 ? "rgba(0, 0, 0, 0)" 78 : expectedBorderColor; 79 80 spectrum.updateUI(); 81 82 // Extract the first rgba value from the linear gradient 83 const linearGradientStr = 84 colorPreviewStyle.getPropertyValue("background-image"); 85 const colorPreviewValue = extractRgbaOverlayString(linearGradientStr); 86 87 is( 88 colorPreviewValue, 89 expectedRgbCssString, 90 `Color preview should be ${expectedRgbCssString}` 91 ); 92 93 info("Test if color preview has a border or not."); 94 // Since border-color is a shorthand CSS property, using getComputedStyle will return 95 // an empty string. Instead, use one of the border sides to find the border-color value 96 // since they will all be the same. 97 const borderColorTop = colorPreviewStyle.getPropertyValue("border-top-color"); 98 is( 99 borderColorTop, 100 expectedBorderColor, 101 "Color preview border color is correct." 102 ); 103 } 104 105 async function testCreateAndDestroyShouldAppendAndRemoveElements(container) { 106 ok(container, "We have the root node to append spectrum to"); 107 is(container.childElementCount, 0, "Root node is empty"); 108 109 const s = await createSpectrum(container, cssColors.white); 110 Assert.greater( 111 container.childElementCount, 112 0, 113 "Spectrum has appended elements" 114 ); 115 116 s.destroy(); 117 is(container.childElementCount, 0, "Destroying spectrum removed all nodes"); 118 } 119 120 async function testPassingAColorAtInitShouldSetThatColor(container) { 121 const initRgba = cssColors.white; 122 123 const s = await createSpectrum(container, initRgba); 124 125 const setRgba = s.rgb; 126 127 is(initRgba[0], setRgba[0], "Spectrum initialized with the right color"); 128 is(initRgba[1], setRgba[1], "Spectrum initialized with the right color"); 129 is(initRgba[2], setRgba[2], "Spectrum initialized with the right color"); 130 is(initRgba[3], setRgba[3], "Spectrum initialized with the right color"); 131 132 s.destroy(); 133 } 134 135 async function testSettingAndGettingANewColor(container) { 136 const s = await createSpectrum(container, cssColors.black); 137 138 const colorToSet = cssColors.white; 139 s.rgb = colorToSet; 140 const newColor = s.rgb; 141 142 is(colorToSet[0], newColor[0], "Spectrum set with the right color"); 143 is(colorToSet[1], newColor[1], "Spectrum set with the right color"); 144 is(colorToSet[2], newColor[2], "Spectrum set with the right color"); 145 is(colorToSet[3], newColor[3], "Spectrum set with the right color"); 146 147 s.destroy(); 148 } 149 150 async function testChangingColorShouldEmitEventsHelper( 151 spectrum, 152 moveFn, 153 expectedColor 154 ) { 155 const onChanged = spectrum.once("changed", (rgba, color) => { 156 is(rgba[0], expectedColor[0], "New color is correct"); 157 is(rgba[1], expectedColor[1], "New color is correct"); 158 is(rgba[2], expectedColor[2], "New color is correct"); 159 is(rgba[3], expectedColor[3], "New color is correct"); 160 is(`rgba(${rgba.join(", ")})`, color, "RGBA and css color correspond"); 161 }); 162 163 moveFn(); 164 await onChanged; 165 ok(true, "Changed event was emitted on color change"); 166 } 167 168 async function testChangingColorShouldEmitEvents(container, doc) { 169 const s = await createSpectrum(container, cssColors.white); 170 171 const sendUpKey = () => EventUtils.sendKey("Up"); 172 const sendDownKey = () => EventUtils.sendKey("Down"); 173 const sendLeftKey = () => EventUtils.sendKey("Left"); 174 const sendRightKey = () => EventUtils.sendKey("Right"); 175 176 info( 177 "Test that simulating a mouse drag move event emits color changed event" 178 ); 179 const draggerMoveFn = () => 180 s.onDraggerMove(s.dragger.offsetWidth / 2, s.dragger.offsetHeight / 2); 181 testChangingColorShouldEmitEventsHelper(s, draggerMoveFn, [128, 64, 64, 1]); 182 183 info( 184 "Test that moving the dragger with arrow keys emits color changed event." 185 ); 186 // Focus on the spectrum dragger when spectrum is shown 187 s.dragger.focus(); 188 is( 189 doc.activeElement.className, 190 "spectrum-color spectrum-box", 191 "Spectrum dragger has successfully received focus." 192 ); 193 testChangingColorShouldEmitEventsHelper(s, sendDownKey, [125, 62, 62, 1]); 194 testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [125, 63, 63, 1]); 195 testChangingColorShouldEmitEventsHelper(s, sendUpKey, [127, 64, 64, 1]); 196 testChangingColorShouldEmitEventsHelper(s, sendRightKey, [128, 63, 63, 1]); 197 198 info( 199 "Test that moving the hue slider with arrow keys emits color changed event." 200 ); 201 // Tab twice to focus on hue slider 202 EventUtils.sendKey("Tab"); 203 is( 204 doc.activeElement.className, 205 "devtools-button", 206 "Eyedropper has focus now." 207 ); 208 EventUtils.sendKey("Tab"); 209 is( 210 doc.activeElement.className, 211 "spectrum-hue-input", 212 "Hue slider has successfully received focus." 213 ); 214 testChangingColorShouldEmitEventsHelper(s, sendRightKey, [128, 66, 63, 1]); 215 testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [128, 63, 63, 1]); 216 217 info( 218 "Test that moving the hue slider with arrow keys emits color changed event." 219 ); 220 // Tab to focus on alpha slider 221 EventUtils.sendKey("Tab"); 222 is( 223 doc.activeElement.className, 224 "spectrum-alpha-input", 225 "Alpha slider has successfully received focus." 226 ); 227 testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [128, 63, 63, 0.99]); 228 testChangingColorShouldEmitEventsHelper(s, sendRightKey, [128, 63, 63, 1]); 229 230 s.destroy(); 231 } 232 233 function setSpectrumProps(spectrum, props, updateUI = true) { 234 for (const prop in props) { 235 spectrum[prop] = props[prop]; 236 237 // Setting textProps implies contrast should be enabled for spectrum 238 if (prop === "textProps") { 239 spectrum.contrastEnabled = true; 240 } 241 } 242 243 if (updateUI) { 244 spectrum.updateUI(); 245 } 246 } 247 248 function testAriaAttributesOnSpectrumElements(spectrum, colorName, rgbString) { 249 for (const slider of [spectrum.dragger, spectrum.hueSlider]) { 250 is( 251 slider.getAttribute("aria-describedby"), 252 "spectrum-dragger", 253 "Slider contains the correct describedby text." 254 ); 255 is( 256 slider.getAttribute("aria-valuetext"), 257 rgbString, 258 "Slider contains the correct valuetext text." 259 ); 260 } 261 262 is( 263 spectrum.colorPreview.title, 264 colorName, 265 "Spectrum element contains the correct title text." 266 ); 267 } 268 269 async function testSettingColorShoudUpdateTheUI(container) { 270 const s = await createSpectrum(container, cssColors.white); 271 const dragHelperOriginalPos = [ 272 s.dragHelper.style.top, 273 s.dragHelper.style.left, 274 ]; 275 const alphaSliderOriginalVal = s.alphaSlider.value; 276 let hueSliderOriginalVal = s.hueSlider.value; 277 278 setSpectrumProps(s, { rgb: [50, 240, 0, 0.2] }); 279 280 Assert.notEqual( 281 s.alphaSlider.value, 282 alphaSliderOriginalVal, 283 "Alpha helper has moved" 284 ); 285 Assert.notStrictEqual( 286 s.dragHelper.style.top, 287 dragHelperOriginalPos[0], 288 "Drag helper has moved" 289 ); 290 Assert.notStrictEqual( 291 s.dragHelper.style.left, 292 dragHelperOriginalPos[1], 293 "Drag helper has moved" 294 ); 295 Assert.notStrictEqual( 296 s.hueSlider.value, 297 hueSliderOriginalVal, 298 "Hue helper has moved" 299 ); 300 testAriaAttributesOnSpectrumElements( 301 s, 302 "Closest to: lime", 303 "rgba(50, 240, 0, 0.2)", 304 0.2 305 ); 306 307 hueSliderOriginalVal = s.hueSlider.value; 308 309 setSpectrumProps(s, { rgb: [0, 255, 0, 0] }); 310 is(s.alphaSlider.value, "0", "Alpha range UI has been updated again"); 311 Assert.notStrictEqual( 312 hueSliderOriginalVal, 313 s.hueSlider.value, 314 "Hue slider should have move again" 315 ); 316 testAriaAttributesOnSpectrumElements(s, "lime", "rgba(0, 255, 0, 0)", 0); 317 318 s.destroy(); 319 } 320 321 async function testChangingColorShouldUpdateColorPreview(container) { 322 const s = await createSpectrum(container, [0, 0, 1, 1]); 323 324 info("Test that color preview is black."); 325 testColorPreviewDisplay(s, "rgb(0, 0, 1)", "transparent"); 326 327 info("Test that color preview is blue."); 328 s.rgb = [0, 0, 255, 1]; 329 testColorPreviewDisplay(s, "rgb(0, 0, 255)", "transparent"); 330 331 info("Test that color preview is red."); 332 s.rgb = [255, 0, 0, 1]; 333 testColorPreviewDisplay(s, "rgb(255, 0, 0)", "transparent"); 334 335 info("Test that color preview is white and also has a light grey border."); 336 s.rgb = cssColors.white; 337 testColorPreviewDisplay(s, "rgb(255, 255, 255)", "rgb(204, 204, 204)"); 338 339 s.destroy(); 340 } 341 342 async function testNotSettingTextPropsShouldNotShowContrastSection(container) { 343 const s = await createSpectrum(container, cssColors.white); 344 345 setSpectrumProps(s, { rgb: cssColors.black }); 346 ok( 347 !s.spectrumContrast.classList.contains("visible"), 348 "Contrast section is not shown." 349 ); 350 351 s.destroy(); 352 } 353 354 function testSpectrumContrast( 355 spectrum, 356 contrastValueEl, 357 rgb, 358 expectedValue, 359 expectedBadgeClass = "", 360 expectLargeTextIndicator = false 361 ) { 362 setSpectrumProps(spectrum, { rgb }); 363 364 is( 365 contrastValueEl.textContent, 366 expectedValue, 367 "Contrast value has the correct text." 368 ); 369 is( 370 contrastValueEl.className, 371 `accessibility-contrast-value${ 372 expectedBadgeClass ? " " + expectedBadgeClass : "" 373 }`, 374 `Contrast value contains ${expectedBadgeClass || "base"} class.` 375 ); 376 is( 377 spectrum.contrastLabel.childNodes.length === 3, 378 expectLargeTextIndicator, 379 `Large text indicator is ${expectLargeTextIndicator ? "" : "not"} shown.` 380 ); 381 } 382 383 async function testSettingTextPropsAndColorShouldUpdateContrastValue( 384 container 385 ) { 386 const s = await createSpectrum(container, cssColors.white); 387 388 ok( 389 !s.spectrumContrast.classList.contains("visible"), 390 "Contrast value is not available yet." 391 ); 392 393 info( 394 "Test that contrast ratio is calculated on setting 'textProps' and 'rgb'." 395 ); 396 setSpectrumProps( 397 s, 398 { textProps: REGULAR_TEXT_PROPS, backgroundColorData: SINGLE_BG_COLOR }, 399 false 400 ); 401 testSpectrumContrast(s, s.contrastValue, [50, 240, 234, 0.8], "1.35", FAIL); 402 403 info("Test that contrast ratio is updated when color is changed."); 404 testSpectrumContrast(s, s.contrastValue, cssColors.black, "21.00", AAA); 405 406 info("Test that contrast ratio cannot be calculated with zero alpha."); 407 testSpectrumContrast( 408 s, 409 s.contrastValue, 410 ZERO_ALPHA_COLOR, 411 "Unable to calculate" 412 ); 413 414 s.destroy(); 415 } 416 417 async function testOnlySelectingLargeTextWithNonZeroAlphaShouldShowIndicator( 418 container 419 ) { 420 let s = await createSpectrum(container, cssColors.white); 421 422 Assert.notStrictEqual( 423 s.contrastLabel.childNodes.length, 424 3, 425 "Large text indicator is initially hidden." 426 ); 427 428 info( 429 "Test that selecting large text with non-zero alpha shows large text indicator." 430 ); 431 setSpectrumProps( 432 s, 433 { 434 textProps: { 435 "font-size": { value: "24px" }, 436 "font-weight": { value: "normal" }, 437 opacity: { value: "1" }, 438 }, 439 backgroundColorData: SINGLE_BG_COLOR, 440 }, 441 false 442 ); 443 testSpectrumContrast(s, s.contrastValue, cssColors.black, "21.00", AAA, true); 444 445 info( 446 "Test that selecting large text with zero alpha hides large text indicator." 447 ); 448 testSpectrumContrast( 449 s, 450 s.contrastValue, 451 ZERO_ALPHA_COLOR, 452 "Unable to calculate" 453 ); 454 455 // Spectrum should be closed and opened again to reflect changes in text size 456 s.destroy(); 457 s = await createSpectrum(container, cssColors.white); 458 459 info("Test that selecting regular text does not show large text indicator."); 460 setSpectrumProps( 461 s, 462 { textProps: REGULAR_TEXT_PROPS, backgroundColorData: SINGLE_BG_COLOR }, 463 false 464 ); 465 testSpectrumContrast(s, s.contrastValue, cssColors.black, "21.00", AAA); 466 467 s.destroy(); 468 } 469 470 async function testSettingMultiColoredBackgroundShouldShowContrastRange( 471 container 472 ) { 473 const s = await createSpectrum(container, cssColors.white); 474 475 info( 476 "Test setting text with non-zero alpha and multi-colored bg shows contrast range and empty single contrast." 477 ); 478 setSpectrumProps( 479 s, 480 { 481 textProps: REGULAR_TEXT_PROPS, 482 backgroundColorData: { 483 min: cssColors.yellow, 484 max: cssColors.green, 485 }, 486 }, 487 false 488 ); 489 testSpectrumContrast(s, s.contrastValueMin, cssColors.white, "1.07", FAIL); 490 testSpectrumContrast(s, s.contrastValueMax, cssColors.white, "5.14", AA); 491 testSpectrumContrast(s, s.contrastValue, cssColors.white, ""); 492 ok( 493 s.spectrumContrast.classList.contains("range"), 494 "Contrast section contains range class." 495 ); 496 497 info("Test setting text with zero alpha shows error in contrast min span."); 498 testSpectrumContrast( 499 s, 500 s.contrastValueMin, 501 ZERO_ALPHA_COLOR, 502 "Unable to calculate" 503 ); 504 505 s.destroy(); 506 } 507 508 async function createSpectrum(...spectrumConstructorParams) { 509 const s = new Spectrum(...spectrumConstructorParams); 510 await waitFor(() => s.dragger.offsetHeight > 0); 511 s.show(); 512 return s; 513 }