Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>MozVisualPicker Tests</title>
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
<link
rel="stylesheet"
/>
<script
type="module"
src="chrome://global/content/elements/moz-visual-picker.mjs"
></script>
<script src="lit-test-helpers.js"></script>
<style>
.slotted {
height: 72px;
width: 72px;
border-radius: var(--border-radius-medium);
background-color: var(--background-color-box);
padding: var(--space-medium);
}
</style>
<script>
let html;
let defaultTemplate;
let testHelpers = new InputTestHelpers();
async function keyboardNavigate(picker, direction) {
let keyCode = `KEY_Arrow${
direction.charAt(0).toUpperCase() + direction.slice(1)
}`;
synthesizeKey(keyCode);
await picker.updateComplete;
}
async function renderVisualPicker(
type = "radio",
template = defaultTemplate
) {
let renderTarget = await testHelpers.renderTemplate(template);
let picker = renderTarget.querySelector("moz-visual-picker");
let items = [
...renderTarget.querySelectorAll("moz-visual-picker-item"),
];
let [firstItem, secondItem, thirdItem] = items;
picker.type = type;
await picker.updateComplete;
return { picker, items, firstItem, secondItem, thirdItem };
}
add_setup(async function setup() {
({ html } = await testHelpers.setupLit());
let templateFn = attrs => html`
<moz-visual-picker ${attrs}>
<moz-visual-picker-item checked value="first">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
<moz-visual-picker-item value="second">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
<moz-visual-picker-item value="third">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
</moz-visual-picker>
`;
defaultTemplate = templateFn(
testHelpers.spread({ label: "Visual picker label" })
);
testHelpers.setupTests({ templateFn });
});
add_task(async function testVisualPickerProperties() {
const TEST_LABEL = "Testing...";
const TEST_DESCRIPTION = "Testing description..";
const TEST_SUPPORT_PAGE = "Testing support page..";
let renderTarget = await testHelpers.renderTemplate(defaultTemplate);
let picker = renderTarget.querySelector("moz-visual-picker");
let items = [
...renderTarget.querySelectorAll("moz-visual-picker-item"),
];
is(
picker.fieldset.label,
"Visual picker label",
"Visual picker supports a label."
);
picker.label = TEST_LABEL;
picker.description = TEST_DESCRIPTION;
picker.supportPage = TEST_SUPPORT_PAGE;
await picker.updateComplete;
is(
picker.fieldset.label,
TEST_LABEL,
"Visual picker label is updated."
);
is(
picker.fieldset.description,
TEST_DESCRIPTION,
"Visual picker description text is set."
);
is(
picker.fieldset.supportPage,
TEST_SUPPORT_PAGE,
"Visual picker support page is set."
);
is(
picker.fieldset.getAttribute("role"),
"radiogroup",
"Visual picker uses the 'radiogroup' role by default."
);
items.forEach(item => {
is(
item.itemEl.getAttribute("role"),
"radio",
"Visual picker items use the 'radio' role by default."
);
});
picker.type = "listbox";
await picker.updateComplete;
is(
picker.fieldset.getAttribute("role"),
"listbox",
"Visual picker uses the 'listbox' role after the type is changed."
);
items.forEach(item => {
is(
item.itemEl.getAttribute("role"),
"option",
"Visual picker items use the 'option' role after the picker type is changed."
);
});
});
add_task(async function testVisualPickerItemLabel() {
let labelledItemsTemplate = html`
<moz-visual-picker
type="listbox"
label="Testing labels"
value="first"
>
<moz-visual-picker-item value="first" label="first">
</moz-visual-picker-item>
<moz-visual-picker-item value="second" label="second">
</moz-visual-picker-item>
<moz-visual-picker-item value="third" label="third">
</moz-visual-picker-item>
</moz-visual-picker>
`;
let renderTarget = await testHelpers.renderTemplate(
labelledItemsTemplate
);
let items = [
...renderTarget.querySelectorAll("moz-visual-picker-item"),
];
let labels = ["first", "second", "third"];
labels.forEach((label, i) => {
is(
items[i].shadowRoot.textContent.trim(),
label,
"Visual picker items support a visible label."
);
});
items.forEach(async (item, i) => {
let itemLabel = labels[i];
// Verify that label is set via markup.
is(
items.shadowRoot.textContent.trim(),
itemLabel,
"Visual picker items support a visible label."
);
// Clear the visible label and set an aria label instead
item.label = "";
item.setAttribute("aria-label", itemLabel);
await item.updateComplete;
ok(
!item.shadowRoot.textContent.trim(),
"Visual picker item no longer has a visible label."
);
is(
item.itemEl.getAttribute("aria-label"),
itemLabel,
"aria-label is set on the inner element."
);
// Clear the aria-label and rely on slotted content to provide a label.
item.ariaLabel = "";
let textEl = document.createElement("p");
textEl.textContent = itemLabel;
item.append(textEl);
await item.updateComplete;
is(
item.shadowRoot.textContent.trim(),
"Visual picker has a visible label again."
);
ok(
item.ariaLabelledByElements.includes(textEl),
"Visual picker item is labelled by the slotted content."
);
});
});
add_task(async function testChangingPickerValue() {
let items, picker;
async function verifySelectedPickerItem(selectedItem, focusedItem) {
let ariaProperty =
picker.type == "radio" ? "aria-checked" : "aria-selected";
ok(selectedItem.checked, "The selected picker item is checked.");
is(
selectedItem.itemEl.getAttribute(ariaProperty),
"true",
`The checked item has the correct ${ariaProperty} value.`
);
if (focusedItem) {
is(focusedItem.itemEl.tabIndex, 0, "The active item is focusable.");
is(
document.activeElement,
focusedItem,
`Expected ${focusedItem.value} item to be focused`
);
}
items.forEach(item => {
let isChecked = item == selectedItem;
let tabIndex = item == (focusedItem ?? selectedItem) ? 0 : -1;
is(
item.checked,
isChecked,
`${item.value} item is ${isChecked ? "" : "not"} checked`
);
is(
item.itemEl.getAttribute(ariaProperty),
isChecked ? "true" : "false",
`${item.value} item has the expected ${ariaProperty} value`
);
is(
item.itemEl.tabIndex,
tabIndex,
`${item.value} item has the expected tabIndex`
);
});
is(
picker.value,
selectedItem.value,
"Picker value matches the value of the checked item."
);
}
async function verifyValueChange(type) {
({ picker, items } = await renderVisualPicker(type));
let [firstItem, secondItem, thirdItem] = items;
// Ensure picker is visible in the test harness.
picker.focus();
picker.scrollIntoView();
info(`Verifying ways of changing the value of a ${type} picker`);
// Verify the initial state.
verifySelectedPickerItem(firstItem);
// Verify changing the checked property directly.
secondItem.checked = true;
await picker.updateComplete;
verifySelectedPickerItem(secondItem);
// Verify clicking on a picker item to change checked state.
synthesizeMouseAtCenter(thirdItem, {});
await picker.updateComplete;
verifySelectedPickerItem(thirdItem, thirdItem);
// Verify changing the picker value sets the selected state of child items.
picker.value = "first";
await picker.updateComplete;
let focusedItem = type == "listbox" ? thirdItem : null;
verifySelectedPickerItem(firstItem, focusedItem);
}
await verifyValueChange("radio");
await verifyValueChange("listbox");
});
// Verify that settings a value on the group works as expected creating
// the elements programmatically via document.createElement. We ran into
// issues with this in moz-visual-picker-item-group.
add_task(async function testProgrammaticVisualPickerCreation() {
let visualPicker = document.createElement("moz-visual-picker");
visualPicker.label = "Created with document.createElement";
visualPicker.value = "second";
let firstItem = document.createElement("moz-visual-picker-item");
firstItem.value = "first";
firstItem.label = "first";
let secondItem = document.createElement("moz-visual-picker-item");
secondItem.value = "second";
secondItem.label = "second";
visualPicker.append(firstItem, secondItem);
testHelpers.render(html``, testHelpers.renderTarget);
testHelpers.renderTarget.append(visualPicker);
await visualPicker.updateComplete;
ok(!firstItem.checked, "The first item is not checked.");
ok(secondItem.checked, "The second item is checked.");
visualPicker.value = "first";
await visualPicker.updateComplete;
ok(firstItem.checked, "The first item is checked.");
ok(!secondItem.checked, "The second item is no longer checked.");
});
// Verify that keyboard navigation works as expected.
add_task(async function testRadioKeyboardNavigation() {
let { firstItem, secondItem, thirdItem, picker } =
await renderVisualPicker();
// Ensure picker is visible in the test harness.
picker.focus();
picker.scrollIntoView();
const navigate = direction => keyboardNavigate(picker, direction);
function validateActiveElement(item) {
is(
document.activeElement,
item,
"Focus moves to the expected picker item."
);
is(picker.value, item.value, "Visual picker value is updated.");
}
synthesizeMouseAtCenter(firstItem, {});
await picker.updateComplete;
await navigate("down");
validateActiveElement(secondItem);
await navigate("down");
validateActiveElement(thirdItem);
await navigate("down");
validateActiveElement(firstItem);
await navigate("up");
validateActiveElement(thirdItem);
await navigate("up");
validateActiveElement(secondItem);
await navigate("right");
validateActiveElement(thirdItem);
await navigate("right");
validateActiveElement(firstItem);
await navigate("left");
validateActiveElement(thirdItem);
await navigate("left");
validateActiveElement(secondItem);
// Validate that disabled items get skipped over.
thirdItem.disabled = true;
await picker.updateComplete;
await navigate("down");
validateActiveElement(firstItem);
await navigate("up");
validateActiveElement(secondItem);
thirdItem.disabled = false;
await picker.updateComplete;
// Validate left/right keys have opposite effect for RTL locales.
await SpecialPowers.pushPrefEnv({
set: [["intl.l10n.pseudo", "bidi"]],
});
await navigate("left");
validateActiveElement(thirdItem);
await navigate("left");
validateActiveElement(firstItem);
await navigate("right");
validateActiveElement(thirdItem);
await navigate("right");
validateActiveElement(secondItem);
secondItem.click();
await picker.updateComplete;
validateActiveElement(secondItem);
await SpecialPowers.popPrefEnv();
});
// Validate behavior when the picker has no value/no item is selected by default.
add_task(async function testNoItemSelected() {
let template = html`
<button tabindex="0">Before picker</button>
<moz-visual-picker name="test-name" label="No item selected">
<moz-visual-picker-item value="first">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
<moz-visual-picker-item value="second">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
<moz-visual-picker-item value="third">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
</moz-visual-picker>
<button tabindex="0" id="after">After picker item></button>
`;
async function validateNoItemSelected(type) {
let { picker, items, firstItem, secondItem, thirdItem } =
await renderVisualPicker(type, template);
let [beforeButton, afterButton] = document.querySelectorAll("button");
info(
`Verifying behavior when no item is selected for ${type} picker`
);
ok(!picker.value, "Visual picker does not have a value.");
items.forEach(item =>
ok(!item.checked, "All picker items are unselected.")
);
beforeButton.focus();
synthesizeKey("KEY_Tab", {});
is(
document.activeElement,
firstItem,
"First picker item is tab focusable when all items un-checked."
);
[secondItem, thirdItem].forEach(item =>
is(
item.itemEl.getAttribute("tabindex"),
"-1",
"All other items are not tab focusable."
)
);
synthesizeKey("KEY_Tab", {});
is(
document.activeElement,
afterButton,
"Tab moves focus out of the visual picker."
);
synthesizeKey("KEY_Tab", { shiftKey: true });
is(
document.activeElement,
firstItem,
"Focus moves back to the first picker item."
);
synthesizeKey("KEY_ArrowDown", {});
is(
document.activeElement,
secondItem,
"Focus moves to the second picker item with down arrow keypress."
);
if (type == "listbox") {
synthesizeKey(" ");
}
is(
picker.value,
secondItem.value,
"Picker value updates to second picker item value."
);
synthesizeKey("KEY_Tab");
secondItem.checked = false;
await picker.updateComplete;
synthesizeKey("KEY_Tab", { shiftKey: true });
ok(
!picker.value,
"Picker value is un-set when all picker items un-checked programmatically."
);
is(
document.activeElement,
firstItem,
"First picker item becomes focusable again."
);
synthesizeKey(" ");
is(
picker.value,
firstItem.value,
"Hitting space selects the focused item."
);
synthesizeKey("KEY_Tab", { shiftKey: true });
firstItem.disabled = true;
secondItem.disabled = true;
await picker.updateComplete;
synthesizeKey("KEY_Tab");
is(
document.activeElement,
thirdItem,
"First non-disabled picker item is focusable when all items are un-checked."
);
synthesizeKey("KEY_Enter");
is(
picker.value,
thirdItem.value,
"Hitting enter selects the focused item."
);
}
await validateNoItemSelected("radio");
await validateNoItemSelected("listbox");
});
// Verify expected events emitted in the correct order.
add_task(async function testPickerEvents() {
async function validatePickerEvents(type) {
let { picker, items, firstItem, secondItem, thirdItem } =
await renderVisualPicker(type);
let { trackEvent, verifyEvents } = testHelpers.getInputEventHelpers();
// Ensure picker is visible in the test harness.
picker.focus();
picker.scrollIntoView();
items.forEach(item => {
item.addEventListener("click", trackEvent);
item.addEventListener("input", trackEvent);
item.addEventListener("change", trackEvent);
});
picker.addEventListener("change", trackEvent);
picker.addEventListener("input", trackEvent);
// Verify that clicking on a item emits the right events in the correct order.
synthesizeMouseAtCenter(thirdItem.itemEl, {});
await TestUtils.waitForTick();
verifyEvents([
{
type: "click",
value: "third",
localName: "moz-visual-picker-item",
checked: true,
},
{
type: "input",
value: "third",
localName: "moz-visual-picker-item",
checked: true,
},
{ type: "input", value: "third", localName: "moz-visual-picker" },
{
type: "change",
value: "third",
localName: "moz-visual-picker-item",
checked: true,
},
{ type: "change", value: "third", localName: "moz-visual-picker" },
]);
// Verify that keyboard navigation emits the right events in the correct order.
synthesizeKey("KEY_ArrowUp", {});
await picker.updateComplete;
await TestUtils.waitForTick();
if (type == "radio") {
info(
"For radio pickers arrow key navigation changes the value and emits change and input events."
);
is(picker.value, secondItem.value, "picker value is updated.");
verifyEvents([
{
type: "input",
value: "second",
localName: "moz-visual-picker",
},
{
type: "change",
value: "second",
localName: "moz-visual-picker",
},
]);
} else {
info(
"For listbox pickers arrow key navigation does not change the value or emit events."
);
is(picker.value, thirdItem.value, "picker value is updated.");
verifyEvents([]);
}
// Verify that changing the group's value directly doesn't emit any events.
picker.value = firstItem.value;
await picker.updateComplete;
ok(firstItem.checked, "Expected item is checked.");
await TestUtils.waitForTick();
verifyEvents([]);
// Verify that changing a item's checked state directly doesn't emit any events.
secondItem.checked = true;
await picker.updateComplete;
is(picker.value, secondItem.value, "Picker value is updated.");
await TestUtils.waitForTick();
verifyEvents([]);
// Verify activating item with space emits proper events.
picker.value = "";
await picker.updateComplete;
ok(!firstItem.checked, "The first item is not selected.");
firstItem.focus();
synthesizeKey(" ");
await TestUtils.waitForTick();
verifyEvents([
{
type: "click",
value: "first",
localName: "moz-visual-picker-item",
checked: true,
},
{
type: "input",
value: "first",
localName: "moz-visual-picker-item",
checked: true,
},
{ type: "input", value: "first", localName: "moz-visual-picker" },
{
type: "change",
value: "first",
localName: "moz-visual-picker-item",
checked: true,
},
{ type: "change", value: "first", localName: "moz-visual-picker" },
]);
}
await validatePickerEvents("radio");
await validatePickerEvents("listbox");
});
// Verifies listbox picker roles, states, and properties meet these requirements:
add_task(async function testListboxPickerRoleStatesProperties() {
const LISTBOX_LABEL = "I'm a listbox";
let listboxTemplate = html`
<moz-visual-picker
type="listbox"
label=${LISTBOX_LABEL}
value="first"
>
<moz-visual-picker-item value="first">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
<moz-visual-picker-item value="second">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
<moz-visual-picker-item value="third">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
</moz-visual-picker>
`;
let renderTarget = await testHelpers.renderTemplate(listboxTemplate);
let picker = renderTarget.querySelector("moz-visual-picker");
let items = [
...renderTarget.querySelectorAll("moz-visual-picker-item"),
];
let [firstItem, secondItem, thirdItem] = items;
// Ensure picker is visible in the test harness.
picker.focus();
picker.scrollIntoView();
is(
picker.fieldset.getAttribute("role"),
"listbox",
"The top level picker element has a role of 'listbox'."
);
is(
picker.fieldset.shadowRoot
.querySelector("fieldset")
.getAttribute("aria-orientation"),
"horizontal",
"The top level picker element's aria-orientation is set to 'horizontal'"
);
let slottedItems = picker.shadowRoot
.querySelector("slot:not([name])")
.assignedElements();
items.forEach(item => {
is(
item.itemEl.getAttribute("role"),
"option",
"Each picker item has a role of 'option'."
);
ok(
slottedItems.includes(item),
"Item is contained by the listbox element."
);
});
let legend = picker.fieldset.shadowRoot.querySelector("legend");
is(legend.innerText, LISTBOX_LABEL, "The listbox element is labelled.");
ok(BrowserTestUtils.isVisible(legend), "The listbox label is visible.");
ok(firstItem.checked, "The first item is selected.");
is(
firstItem.itemEl.getAttribute("aria-selected"),
"true",
"aria-selected is set on the selected item."
);
[secondItem, thirdItem].forEach(item => {
ok(!item.checked, "All other items are unchecked.");
is(
item.itemEl.getAttribute("aria-selected"),
"false",
"All other items have aria-selected set to false."
);
});
synthesizeMouseAtCenter(thirdItem, {});
await picker.updateComplete;
ok(thirdItem.checked, "The third item is now selected.");
is(
thirdItem.itemEl.getAttribute("aria-selected"),
"true",
"aria-selected is set on the selected item."
);
[firstItem, secondItem].forEach(item => {
ok(
!item.checked,
"All other items are unchecked because only one item can be selected at a time."
);
is(
item.itemEl.getAttribute("aria-selected"),
"false",
"All other items have aria-selected set to false."
);
});
});
// Verifies listbox picker keyboard interactions meet these requirements:
add_task(async function testListboxPickerInteractions() {
let listboxTemplate = html`
<button tabindex="0">Before picker</button>
<moz-visual-picker type="listbox" label="Another listbox">
<moz-visual-picker-item value="first">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
<moz-visual-picker-item value="second">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
<moz-visual-picker-item value="third">
<div class="slotted">In the slot</div>
</moz-visual-picker-item>
</moz-visual-picker>
`;
let renderTarget = await testHelpers.renderTemplate(listboxTemplate);
let picker = renderTarget.querySelector("moz-visual-picker");
let items = [
...renderTarget.querySelectorAll("moz-visual-picker-item"),
];
let [firstItem, secondItem, thirdItem] = items;
let beforeButton = renderTarget.querySelector("button");
const navigate = direction => keyboardNavigate(picker, direction);
beforeButton.focus();
synthesizeKey("KEY_Tab", {});
is(
document.activeElement,
firstItem,
"First item is focused when no items are selected and the picker receives focus."
);
synthesizeKey("KEY_Tab", { shiftKey: true });
isnot(
document.activeElement,
firstItem,
"Item loses focus when the picker is blurred."
);
picker.value = "third";
await picker.updateComplete;
synthesizeKey("KEY_Tab", {});
ok(
thirdItem.checked,
"Third item is selected after picker value changes."
);
is(
document.activeElement,
thirdItem,
"The selected item is focused when when the picker receives focus."
);
await navigate("down");
is(
document.activeElement,
thirdItem,
"Focus does not wrap, so the third item stays focused after hitting the down arrow."
);
ok(!firstItem.checked, "Selection does not follow focus.");
is(picker.value, "third", "The picker value is unchanged.");
await navigate("left");
is(
document.activeElement,
secondItem,
"The second item is focused after hitting the left arrow from the third item."
);
ok(!secondItem.checked, "Selection does not follow focus.");
is(
picker.value,
"third",
"The picker value is not changed by navigation."
);
await navigate("up");
is(
document.activeElement,
firstItem,
"The first item is focused after hitting the up arrow from the second item."
);
ok(!firstItem.checked, "Selection does not follow focus.");
is(
picker.value,
"third",
"The picker value is not changed by navigation."
);
await navigate("left");
is(
document.activeElement,
firstItem,
"Focus does not wrap, so the first item stays focused after hitting the left arrow."
);
ok(!firstItem.checked, "Selection does not follow focus.");
is(
picker.value,
"third",
"The picker value is not changed by navigation."
);
await navigate("right");
is(
document.activeElement,
secondItem,
"The second item is focused after hitting the right arrow from the first item."
);
ok(!secondItem.checked, "Selection does not follow focus.");
is(
picker.value,
"third",
"The picker value is not changed by navigation."
);
synthesizeKey(" ");
ok(
secondItem.checked,
"Second item is selected when space bar is pressed."
);
is(
picker.value,
"second",
"The picker value changes when a new item is selected."
);
await navigate("left");
synthesizeKey("KEY_Enter");
ok(
firstItem.checked,
"First item is selected when enter key is pressed."
);
is(
picker.value,
"first",
"The picker value changes when a new item is selected."
);
});
</script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
</body>
</html>