Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- Manifest: devtools/client/shared/test/browser.toml
/* Any copyright is dedicated to the Public Domain.
"use strict";
// Tests that the spectrum color picker works correctly
const Spectrum = require("resource://devtools/client/shared/widgets/Spectrum.js");
const {
accessibility: {
SCORES: { FAIL, AAA, AA },
},
} = require("resource://devtools/shared/constants.js");
loader.lazyRequireGetter(
this,
"cssColors",
"resource://devtools/shared/css/color-db.js",
true
);
const TEST_URI = CHROME_URL_ROOT + "doc_spectrum.html";
const REGULAR_TEXT_PROPS = {
"font-size": { value: "11px" },
"font-weight": { value: "bold" },
opacity: { value: "1" },
};
const SINGLE_BG_COLOR = {
value: cssColors.white,
};
const ZERO_ALPHA_COLOR = [0, 255, 255, 0];
add_task(async function () {
const { host, doc } = await createHost("bottom", TEST_URI);
const container = doc.getElementById("spectrum-container");
await testCreateAndDestroyShouldAppendAndRemoveElements(container);
await testPassingAColorAtInitShouldSetThatColor(container);
await testSettingAndGettingANewColor(container);
await testChangingColorShouldEmitEvents(container, doc);
await testSettingColorShoudUpdateTheUI(container);
await testChangingColorShouldUpdateColorPreview(container);
await testNotSettingTextPropsShouldNotShowContrastSection(container);
await testSettingTextPropsAndColorShouldUpdateContrastValue(container);
await testOnlySelectingLargeTextWithNonZeroAlphaShouldShowIndicator(
container
);
await testSettingMultiColoredBackgroundShouldShowContrastRange(container);
host.destroy();
});
/**
* Helper method for extracting the rgba overlay value of the color preview's background
* image style.
*
* @param {String} linearGradientStr
* The linear gradient CSS string.
* @return {String} Returns the rgba string for the color overlay.
*/
function extractRgbaOverlayString(linearGradientStr) {
const start = linearGradientStr.indexOf("(");
const end = linearGradientStr.indexOf(")");
return linearGradientStr.substring(start + 1, end + 1);
}
function testColorPreviewDisplay(
spectrum,
expectedRgbCssString,
expectedBorderColor
) {
const { colorPreview } = spectrum;
const colorPreviewStyle = window.getComputedStyle(colorPreview);
expectedBorderColor =
expectedBorderColor === "transparent"
? "rgba(0, 0, 0, 0)"
: expectedBorderColor;
spectrum.updateUI();
// Extract the first rgba value from the linear gradient
const linearGradientStr =
colorPreviewStyle.getPropertyValue("background-image");
const colorPreviewValue = extractRgbaOverlayString(linearGradientStr);
is(
colorPreviewValue,
expectedRgbCssString,
`Color preview should be ${expectedRgbCssString}`
);
info("Test if color preview has a border or not.");
// Since border-color is a shorthand CSS property, using getComputedStyle will return
// an empty string. Instead, use one of the border sides to find the border-color value
// since they will all be the same.
const borderColorTop = colorPreviewStyle.getPropertyValue("border-top-color");
is(
borderColorTop,
expectedBorderColor,
"Color preview border color is correct."
);
}
async function testCreateAndDestroyShouldAppendAndRemoveElements(container) {
ok(container, "We have the root node to append spectrum to");
is(container.childElementCount, 0, "Root node is empty");
const s = await createSpectrum(container, cssColors.white);
Assert.greater(
container.childElementCount,
0,
"Spectrum has appended elements"
);
s.destroy();
is(container.childElementCount, 0, "Destroying spectrum removed all nodes");
}
async function testPassingAColorAtInitShouldSetThatColor(container) {
const initRgba = cssColors.white;
const s = await createSpectrum(container, initRgba);
const setRgba = s.rgb;
is(initRgba[0], setRgba[0], "Spectrum initialized with the right color");
is(initRgba[1], setRgba[1], "Spectrum initialized with the right color");
is(initRgba[2], setRgba[2], "Spectrum initialized with the right color");
is(initRgba[3], setRgba[3], "Spectrum initialized with the right color");
s.destroy();
}
async function testSettingAndGettingANewColor(container) {
const s = await createSpectrum(container, cssColors.black);
const colorToSet = cssColors.white;
s.rgb = colorToSet;
const newColor = s.rgb;
is(colorToSet[0], newColor[0], "Spectrum set with the right color");
is(colorToSet[1], newColor[1], "Spectrum set with the right color");
is(colorToSet[2], newColor[2], "Spectrum set with the right color");
is(colorToSet[3], newColor[3], "Spectrum set with the right color");
s.destroy();
}
async function testChangingColorShouldEmitEventsHelper(
spectrum,
moveFn,
expectedColor
) {
const onChanged = spectrum.once("changed", (rgba, color) => {
is(rgba[0], expectedColor[0], "New color is correct");
is(rgba[1], expectedColor[1], "New color is correct");
is(rgba[2], expectedColor[2], "New color is correct");
is(rgba[3], expectedColor[3], "New color is correct");
is(`rgba(${rgba.join(", ")})`, color, "RGBA and css color correspond");
});
moveFn();
await onChanged;
ok(true, "Changed event was emitted on color change");
}
async function testChangingColorShouldEmitEvents(container, doc) {
const s = await createSpectrum(container, cssColors.white);
const sendUpKey = () => EventUtils.sendKey("Up");
const sendDownKey = () => EventUtils.sendKey("Down");
const sendLeftKey = () => EventUtils.sendKey("Left");
const sendRightKey = () => EventUtils.sendKey("Right");
info(
"Test that simulating a mouse drag move event emits color changed event"
);
const draggerMoveFn = () =>
s.onDraggerMove(s.dragger.offsetWidth / 2, s.dragger.offsetHeight / 2);
testChangingColorShouldEmitEventsHelper(s, draggerMoveFn, [128, 64, 64, 1]);
info(
"Test that moving the dragger with arrow keys emits color changed event."
);
// Focus on the spectrum dragger when spectrum is shown
s.dragger.focus();
is(
doc.activeElement.className,
"spectrum-color spectrum-box",
"Spectrum dragger has successfully received focus."
);
testChangingColorShouldEmitEventsHelper(s, sendDownKey, [125, 62, 62, 1]);
testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [125, 63, 63, 1]);
testChangingColorShouldEmitEventsHelper(s, sendUpKey, [128, 64, 64, 1]);
testChangingColorShouldEmitEventsHelper(s, sendRightKey, [128, 63, 63, 1]);
info(
"Test that moving the hue slider with arrow keys emits color changed event."
);
// Tab twice to focus on hue slider
EventUtils.sendKey("Tab");
is(
doc.activeElement.className,
"devtools-button",
"Eyedropper has focus now."
);
EventUtils.sendKey("Tab");
is(
doc.activeElement.className,
"spectrum-hue-input",
"Hue slider has successfully received focus."
);
testChangingColorShouldEmitEventsHelper(s, sendRightKey, [128, 66, 63, 1]);
testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [128, 63, 63, 1]);
info(
"Test that moving the hue slider with arrow keys emits color changed event."
);
// Tab to focus on alpha slider
EventUtils.sendKey("Tab");
is(
doc.activeElement.className,
"spectrum-alpha-input",
"Alpha slider has successfully received focus."
);
testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [128, 63, 63, 0.99]);
testChangingColorShouldEmitEventsHelper(s, sendRightKey, [128, 63, 63, 1]);
s.destroy();
}
function setSpectrumProps(spectrum, props, updateUI = true) {
for (const prop in props) {
spectrum[prop] = props[prop];
// Setting textProps implies contrast should be enabled for spectrum
if (prop === "textProps") {
spectrum.contrastEnabled = true;
}
}
if (updateUI) {
spectrum.updateUI();
}
}
function testAriaAttributesOnSpectrumElements(spectrum, colorName, rgbString) {
for (const slider of [spectrum.dragger, spectrum.hueSlider]) {
is(
slider.getAttribute("aria-describedby"),
"spectrum-dragger",
"Slider contains the correct describedby text."
);
is(
slider.getAttribute("aria-valuetext"),
rgbString,
"Slider contains the correct valuetext text."
);
}
is(
spectrum.colorPreview.title,
colorName,
"Spectrum element contains the correct title text."
);
}
async function testSettingColorShoudUpdateTheUI(container) {
const s = await createSpectrum(container, cssColors.white);
const dragHelperOriginalPos = [
s.dragHelper.style.top,
s.dragHelper.style.left,
];
const alphaSliderOriginalVal = s.alphaSlider.value;
let hueSliderOriginalVal = s.hueSlider.value;
setSpectrumProps(s, { rgb: [50, 240, 234, 0.2] });
Assert.notEqual(
s.alphaSlider.value,
alphaSliderOriginalVal,
"Alpha helper has moved"
);
Assert.notStrictEqual(
s.dragHelper.style.top,
dragHelperOriginalPos[0],
"Drag helper has moved"
);
Assert.notStrictEqual(
s.dragHelper.style.left,
dragHelperOriginalPos[1],
"Drag helper has moved"
);
Assert.notStrictEqual(
s.hueSlider.value,
hueSliderOriginalVal,
"Hue helper has moved"
);
testAriaAttributesOnSpectrumElements(
s,
"Closest to: aqua",
"rgba(50, 240, 234, 0.2)",
0.2
);
hueSliderOriginalVal = s.hueSlider.value;
setSpectrumProps(s, { rgb: ZERO_ALPHA_COLOR });
is(s.alphaSlider.value, "0", "Alpha range UI has been updated again");
Assert.notStrictEqual(
hueSliderOriginalVal,
s.hueSlider.value,
"Hue slider should have move again"
);
testAriaAttributesOnSpectrumElements(s, "aqua", "rgba(0, 255, 255, 0)", 0);
s.destroy();
}
async function testChangingColorShouldUpdateColorPreview(container) {
const s = await createSpectrum(container, [0, 0, 1, 1]);
info("Test that color preview is black.");
testColorPreviewDisplay(s, "rgb(0, 0, 1)", "transparent");
info("Test that color preview is blue.");
s.rgb = [0, 0, 255, 1];
testColorPreviewDisplay(s, "rgb(0, 0, 255)", "transparent");
info("Test that color preview is red.");
s.rgb = [255, 0, 0, 1];
testColorPreviewDisplay(s, "rgb(255, 0, 0)", "transparent");
info("Test that color preview is white and also has a light grey border.");
s.rgb = cssColors.white;
testColorPreviewDisplay(s, "rgb(255, 255, 255)", "rgb(204, 204, 204)");
s.destroy();
}
async function testNotSettingTextPropsShouldNotShowContrastSection(container) {
const s = await createSpectrum(container, cssColors.white);
setSpectrumProps(s, { rgb: cssColors.black });
ok(
!s.spectrumContrast.classList.contains("visible"),
"Contrast section is not shown."
);
s.destroy();
}
function testSpectrumContrast(
spectrum,
contrastValueEl,
rgb,
expectedValue,
expectedBadgeClass = "",
expectLargeTextIndicator = false
) {
setSpectrumProps(spectrum, { rgb });
is(
contrastValueEl.textContent,
expectedValue,
"Contrast value has the correct text."
);
is(
contrastValueEl.className,
`accessibility-contrast-value${
expectedBadgeClass ? " " + expectedBadgeClass : ""
}`,
`Contrast value contains ${expectedBadgeClass || "base"} class.`
);
is(
spectrum.contrastLabel.childNodes.length === 3,
expectLargeTextIndicator,
`Large text indicator is ${expectLargeTextIndicator ? "" : "not"} shown.`
);
}
async function testSettingTextPropsAndColorShouldUpdateContrastValue(
container
) {
const s = await createSpectrum(container, cssColors.white);
ok(
!s.spectrumContrast.classList.contains("visible"),
"Contrast value is not available yet."
);
info(
"Test that contrast ratio is calculated on setting 'textProps' and 'rgb'."
);
setSpectrumProps(
s,
{ textProps: REGULAR_TEXT_PROPS, backgroundColorData: SINGLE_BG_COLOR },
false
);
testSpectrumContrast(s, s.contrastValue, [50, 240, 234, 0.8], "1.35", FAIL);
info("Test that contrast ratio is updated when color is changed.");
testSpectrumContrast(s, s.contrastValue, cssColors.black, "21.00", AAA);
info("Test that contrast ratio cannot be calculated with zero alpha.");
testSpectrumContrast(
s,
s.contrastValue,
ZERO_ALPHA_COLOR,
"Unable to calculate"
);
s.destroy();
}
async function testOnlySelectingLargeTextWithNonZeroAlphaShouldShowIndicator(
container
) {
let s = await createSpectrum(container, cssColors.white);
Assert.notStrictEqual(
s.contrastLabel.childNodes.length,
3,
"Large text indicator is initially hidden."
);
info(
"Test that selecting large text with non-zero alpha shows large text indicator."
);
setSpectrumProps(
s,
{
textProps: {
"font-size": { value: "24px" },
"font-weight": { value: "normal" },
opacity: { value: "1" },
},
backgroundColorData: SINGLE_BG_COLOR,
},
false
);
testSpectrumContrast(s, s.contrastValue, cssColors.black, "21.00", AAA, true);
info(
"Test that selecting large text with zero alpha hides large text indicator."
);
testSpectrumContrast(
s,
s.contrastValue,
ZERO_ALPHA_COLOR,
"Unable to calculate"
);
// Spectrum should be closed and opened again to reflect changes in text size
s.destroy();
s = await createSpectrum(container, cssColors.white);
info("Test that selecting regular text does not show large text indicator.");
setSpectrumProps(
s,
{ textProps: REGULAR_TEXT_PROPS, backgroundColorData: SINGLE_BG_COLOR },
false
);
testSpectrumContrast(s, s.contrastValue, cssColors.black, "21.00", AAA);
s.destroy();
}
async function testSettingMultiColoredBackgroundShouldShowContrastRange(
container
) {
const s = await createSpectrum(container, cssColors.white);
info(
"Test setting text with non-zero alpha and multi-colored bg shows contrast range and empty single contrast."
);
setSpectrumProps(
s,
{
textProps: REGULAR_TEXT_PROPS,
backgroundColorData: {
min: cssColors.yellow,
max: cssColors.green,
},
},
false
);
testSpectrumContrast(s, s.contrastValueMin, cssColors.white, "1.07", FAIL);
testSpectrumContrast(s, s.contrastValueMax, cssColors.white, "5.14", AA);
testSpectrumContrast(s, s.contrastValue, cssColors.white, "");
ok(
s.spectrumContrast.classList.contains("range"),
"Contrast section contains range class."
);
info("Test setting text with zero alpha shows error in contrast min span.");
testSpectrumContrast(
s,
s.contrastValueMin,
ZERO_ALPHA_COLOR,
"Unable to calculate"
);
s.destroy();
}
async function createSpectrum(...spectrumConstructorParams) {
const s = new Spectrum(...spectrumConstructorParams);
await waitFor(() => s.dragger.offsetHeight > 0);
s.show();
return s;
}