Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
-
CI results over the last 21 days
- Manifest: toolkit/content/tests/chrome/chrome.toml
<?xml version="1.0"?>
<window title="Menu Access Key Conflicts Test"
<label id="anchor" value="anchor"/>
<menupopup id="distinct">
<menuitem label="One" accesskey="o"/>
<menu label="Two" accesskey="t">
<menupopup>
<menuitem label="Three"/>
<menuitem label="Four"/>
</menupopup>
</menu>
</menupopup>
<menupopup id="none">
<menuitem label="One"/>
<menuseparator/>
<menuitem label="Two"/>
</menupopup>
<menupopup id="duplicate">
<menuitem label="Two" accesskey="t"/>
<menuitem label="Three" accesskey="T"/>
<menuitem label="Four" accesskey="f"/>
</menupopup>
<menupopup id="mixed">
<menuitem label="One" accesskey="o"/>
<menugroup>
<menuitem label="Two"/>
</menugroup>
<menuitem label="Three"/>
</menupopup>
<menupopup id="hidden">
<menuitem label="One" accesskey="o"/>
<menuitem label="Two" hidden="true"/>
</menupopup>
<menupopup id="disabled">
<menuitem label="One" accesskey="o"/>
<menuitem label="Two" disabled="true"/>
<menuitem label="Three" accesskey="o" disabled="true"/>
<menuitem label="Note" disabled="true" role="note"/>
</menupopup>
<menupopup id="unlabeled">
<menugroup>
<menuitem aria-label="Back"/>
<menuitem aria-label="Forward" accesskey="f"/>
</menugroup>
<menuitem label="Full Screen" accesskey="f"/>
</menupopup>
<menupopup id="tracked" accesskey-conflicts-bug="2073885">
<menuitem label="One" accesskey="o"/>
<menuitem label="Two" accesskey="o"/>
<menuitem label="Three"/>
</menupopup>
<menupopup id="intended">
<menuitem label="One" accesskey="o"/>
<menuitem label="Two" accesskey="o" intended-duplicate-accesskey="true"/>
<menuitem label="Three" accesskey="o"/>
</menupopup>
<menupopup id="intended-only">
<menuitem label="One" accesskey="o"/>
<menuitem label="Two" accesskey="o" intended-duplicate-accesskey="true"/>
</menupopup>
<menupopup id="auto">
<menuitem label="Copy" accesskey="c"/>
<menuitem label="correct" auto-accesskey=""/>
<menugroup>
<menuitem label=""Quoted" title" auto-accesskey=""/>
</menugroup>
<menuitem label="🦊 Fox" auto-accesskey=""/>
<menuitem label="翻译 English" auto-accesskey=""/>
<menuitem label="⁴ʰ fourth" auto-accesskey=""/>
<menu label="Submenu" auto-accesskey="">
<menupopup/>
</menu>
</menupopup>
<menupopup id="auto-nested">
<menuitem label="Copy" accesskey="c"/>
<menugroup>
<menugroup>
<menuitem label="Nested" auto-accesskey=""/>
</menugroup>
</menugroup>
</menupopup>
<menupopup id="auto-disabled">
<menuitem label="One" accesskey="1" disabled="true"/>
<menuitem label="翻译" auto-accesskey=""/>
</menupopup>
<menupopup id="auto-disabled-keyed">
<menuitem label="Paste" accesskey="p" disabled="true"/>
<menuitem label="Manage"/>
<menuitem label="Run" auto-accesskey=""/>
</menupopup>
<menupopup id="auto-others-unkeyed">
<menuitem label="Manage"/>
<menuitem label="Move" auto-accesskey="" accesskey="m"/>
</menupopup>
<menupopup id="auto-translated">
<menuitem id="auto-translated-item"/>
<menuitem label="Run" auto-accesskey=""/>
</menupopup>
<menupopup id="translated">
<menuitem label="Undo" accesskey="u"/>
<menuitem id="translated-item"/>
</menupopup>
<menupopup id="retranslated">
<menuitem label="One" accesskey="o"/>
<menuitem id="retranslated-item" label="Two" accesskey="o"/>
</menupopup>
<menupopup id="auto-digits">
<menuitem label="One" accesskey="1"/>
<menuitem label="翻译" auto-accesskey=""/>
<menuitem label="2 columns" auto-accesskey=""/>
<menuitem label="🦊" auto-accesskey=""/>
</menupopup>
<script class="testbody" type="application/javascript">
<![CDATA[
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const { Assert } = ChromeUtils.importESModule(
);
const assert = new Assert((err, message) =>
ok(!err, err ? err.message : message)
);
async function openAndCollectErrors(id, onShown) {
let errors = [];
let sentinel = new Promise(resolve => {
SpecialPowers.registerConsoleListener(msg => {
if (msg.message == "SENTINEL") {
resolve();
} else if (msg.errorMessage?.startsWith("AccessKeyConflictError:")) {
errors.push(msg.errorMessage);
}
});
});
let popup = document.getElementById(id);
let shown = new Promise(resolve =>
popup.addEventListener(
"popupshown",
() => {
onShown?.();
resolve();
},
{ once: true }
)
);
popup.openPopup(document.getElementById("anchor"), "after_start");
await shown;
if (document.hasPendingL10nMutations) {
await new Promise(resolve =>
document.addEventListener("L10nMutationsFinished", resolve, {
once: true,
})
);
}
// The check runs in a system-group listener, after this one.
await new Promise(resolve => SimpleTest.executeSoon(resolve));
let hidden = new Promise(resolve =>
popup.addEventListener("popuphidden", resolve, { once: true })
);
popup.hidePopup();
await hidden;
SpecialPowers.postConsoleSentinel();
await sentinel;
return errors;
}
add_setup(async function setup() {
// The check skips native menus, so open the test menus as XUL popups on
// macOS. testNativeMenuSkipped opens a native one.
await SpecialPowers.pushPrefEnv({
set: [["widget.macos.native-anchored-menus", false]],
});
L10nRegistry.getInstance().registerSources([
L10nFileSource.createMock(
"accesskey-test",
"app",
["en-US"],
"/localization/{locale}/",
[
{
path: "/localization/en-US/accesskey-test.ftl",
source: `
undo =
.label = Undo
.accesskey = u
redo =
.label = Redo
.accesskey = r
`,
},
]
),
]);
SimpleTest.registerCleanupFunction(() =>
L10nRegistry.getInstance().removeSources(["accesskey-test"])
);
link.rel = "localization";
link.href = "accesskey-test.ftl";
document.documentElement.append(link);
});
add_task(async function testDistinctAccessKeys() {
is(
(await openAndCollectErrors("distinct")).length,
0,
"Distinct accesskeys on every item are fine, submenu items don't count"
);
});
add_task(async function testNoAccessKeys() {
is(
(await openAndCollectErrors("none")).length,
0,
"No accesskey on any item is fine"
);
});
add_task(async function testDuplicateAccessKey() {
let errors = await openAndCollectErrors("duplicate");
is(errors.length, 1, "A shared accesskey is reported once");
assert.stringContains(
errors[0],
`menupopup#duplicate: accesskey "t" is shared by "Two", "Three"`,
"Message names the key and both items"
);
assert.stringContains(
errors[0],
"Fix a shared accesskey",
"Message recommends remedies for the problem it found"
);
ok(
!errors[0].includes("Fix a missing accesskey"),
"Message recommends nothing for a problem it didn't find"
);
});
add_task(async function testIntendedDuplicate() {
let errors = await openAndCollectErrors("intended");
is(errors.length, 1, "Unmarked items sharing the key are still reported");
assert.stringContains(
errors[0],
`accesskey "o" is shared by "One", "Three"`,
"Unmarked items sharing the key are reported"
);
ok(
!errors[0].includes("Two"),
"An item with intended-duplicate-accesskey is left out"
);
is(
(await openAndCollectErrors("intended-only")).length,
0,
"A key shared only with a marked item is not a conflict"
);
});
add_task(async function testMixedAccessKeys() {
let errors = await openAndCollectErrors("mixed");
is(errors.length, 1, "Mixing items with and without an accesskey is reported once");
assert.stringContains(
errors[0],
`"Two", "Three" have no accesskey while "One" does`,
"Message names the items on both sides, including one in a menugroup"
);
assert.stringContains(
errors[0],
"Fix a missing accesskey",
"Message recommends remedies for the problem it found"
);
ok(
!errors[0].includes("Fix a shared accesskey"),
"Message recommends nothing for a problem it didn't find"
);
});
add_task(async function testHiddenItemIgnored() {
is(
(await openAndCollectErrors("hidden")).length,
0,
"A hidden item doesn't count as an item without an accesskey"
);
});
add_task(async function testTrackedMenuSkipped() {
is(
(await openAndCollectErrors("tracked")).length,
0,
"A menu naming its accesskey-conflicts bug is skipped"
);
});
add_task(async function testAutoAccessKeys() {
let items = [...document.querySelectorAll("#auto [auto-accesskey]")];
is(
(await openAndCollectErrors("auto")).length,
0,
"An item with auto-accesskey may share its accesskey"
);
assert.deepEqual(
items.map(item => item.getAttribute("accesskey")),
["c", "Q", "F", "E", "f", "S"],
"Each item gets its label's first typeable letter, skipping punctuation, emoji, IME scripts, superscript digits and modifier letters"
);
items[0].setAttribute("label", "Paste");
await openAndCollectErrors("auto");
is(
items[0].getAttribute("accesskey"),
"P",
"The accesskey follows a changed label when the popup opens again"
);
});
add_task(async function testAutoAccessKeysNestedAndAdded() {
let popup = document.getElementById("auto-nested");
let added = document.createXULElement("menuitem");
added.setAttribute("label", "Added");
added.setAttribute("auto-accesskey", "");
popup.addEventListener("popupshowing", () => popup.append(added), {
once: true,
});
await openAndCollectErrors("auto-nested");
is(
popup.querySelector("menugroup [auto-accesskey]").getAttribute("accesskey"),
"N",
"An item inside a nested menugroup gets an accesskey"
);
is(
added.getAttribute("accesskey"),
"A",
"An item added by a popupshowing listener gets an accesskey"
);
added.remove();
});
add_task(async function testAutoAccessKeysDisabledItems() {
await SpecialPowers.pushPrefEnv({
set: [["ui.skipNavigatingDisabledMenuItem", 1]],
});
await openAndCollectErrors("auto-disabled");
is(
document
.querySelector("#auto-disabled [auto-accesskey]")
.getAttribute("accesskey"),
"2",
"A disabled item's accesskey stays taken"
);
await openAndCollectErrors("auto-disabled-keyed");
is(
document
.querySelector("#auto-disabled-keyed [auto-accesskey]")
.getAttribute("accesskey"),
"R",
"A disabled item with an accesskey counts as another item with an accesskey"
);
await SpecialPowers.popPrefEnv();
});
add_task(async function testAutoAccessKeysOthersUnkeyed() {
let item = document.querySelector("#auto-others-unkeyed [auto-accesskey]");
is(
(await openAndCollectErrors("auto-others-unkeyed")).length,
0,
"A menu without accesskeys gets none for its auto-accesskey items"
);
ok(
!item.hasAttribute("accesskey"),
"The item doesn't take the first letter of another item's label"
);
});
add_task(async function testAutoAccessKeysAfterTranslation() {
let popup = document.getElementById("auto-translated");
popup.addEventListener(
"popupshowing",
() => {
document.l10n.setAttributes(
document.getElementById("auto-translated-item"),
"redo"
);
},
{ once: true }
);
is(
(await openAndCollectErrors("auto-translated")).length,
0,
"The auto-accesskey item may share the translated item's accesskey"
);
is(
popup.querySelector("[auto-accesskey]").getAttribute("accesskey"),
"R",
"The item gets an accesskey once the other items' translations arrive"
);
});
add_task(async function testAutoAccessKeyDigits() {
let items = [...document.querySelectorAll("#auto-digits [auto-accesskey]")];
is(
(await openAndCollectErrors("auto-digits")).length,
0,
"Items without a typeable letter get accesskeys"
);
assert.deepEqual(
items.map(item => item.getAttribute("accesskey")),
["3", "2", "4"],
"Each item without a typeable letter or digit gets the lowest unused digit"
);
});
add_task(async function testNativeMenuSkipped() {
if (AppConstants.platform != "macosx") {
ok(true, "Only macOS shows menus natively");
return;
}
if (Services.env.get("MOZ_HEADLESS")) {
ok(true, "Menus aren't native in headless mode");
return;
}
await SpecialPowers.pushPrefEnv({
set: [["widget.macos.native-anchored-menus", true]],
});
let popup = document.getElementById("duplicate");
// Opening the popup with native menus off marked it nonnative, which would
// keep it non-native.
popup.removeAttribute("nonnative");
let isNative;
let errors = await openAndCollectErrors("duplicate", () => {
isNative = popup.isNativeMenu;
});
await SpecialPowers.popPrefEnv();
ok(isNative, "The menu opens natively");
is(errors.length, 0, "A native menu is skipped");
});
add_task(async function testDisabledItems() {
let errors = await openAndCollectErrors("disabled");
is(errors.length, 1, "Disabled items are checked like any other");
assert.stringContains(
errors[0],
`accesskey "o" is shared by "One", "Three"`,
"A disabled item counts for a shared accesskey"
);
assert.stringContains(
errors[0],
`"Two" has no accesskey`,
"A disabled item counts for a missing accesskey"
);
ok(!errors[0].includes("Note"), "A disabled note counts for neither");
});
add_task(async function testUnlabeledItems() {
let errors = await openAndCollectErrors("unlabeled");
is(errors.length, 1, "Items without a label are checked for shared accesskeys");
assert.stringContains(
errors[0],
`accesskey "f" is shared by "Forward", "Full Screen"`,
"An item without a label counts for a shared accesskey"
);
ok(
!errors[0].includes("Back"),
"An item without a label doesn't count for a missing accesskey"
);
});
add_task(async function testPendingTranslation() {
let item = document.getElementById("translated-item");
let errors = await openAndCollectErrors("translated", () => {
document.l10n.setAttributes(item, "undo");
ok(document.hasPendingL10nMutations, "Translation is pending at popupshown");
});
is(item.getAttribute("label"), "Undo", "The item got its translation");
is(errors.length, 1, "The check waits for the pending translation");
assert.stringContains(
errors[0],
`accesskey "u" is shared by "Undo", "Undo"`,
"The translated item is compared"
);
});
add_task(async function testPendingRetranslation() {
let item = document.getElementById("retranslated-item");
let errors = await openAndCollectErrors("retranslated", () => {
document.l10n.setAttributes(item, "redo");
ok(document.hasPendingL10nMutations, "Translation is pending at popupshown");
});
is(item.getAttribute("label"), "Redo", "The item got its translation");
is(
errors.length,
0,
"The check compares the translation, not the label it replaces"
);
});
]]>
</script>
<p id="display"/>
<div id="content" style="display: none"/>
<pre id="test"/>
</body>
</window>