/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; requestLongerTimeout(2); /* import-globals-from ../../mochitest/name.js */ /* import-globals-from ../../mochitest/attributes.js */ loadScripts( { name: "name.js", dir: MOCHITESTS_DIR }, { name: "attributes.js", dir: MOCHITESTS_DIR } ); /** * Rules for name tests that are inspired by * accessible/tests/mochitest/name/markuprules.xul * * Each element in the list of rules represents a name calculation rule for a * particular test case. * * The rules have the following format: * { attr } - calculated from attribute * { elm } - calculated from another element * { fromsubtree } - calculated from element's subtree * */ const ARIARule = [{ attr: "aria-labelledby" }, { attr: "aria-label" }]; const HTMLControlHeadRule = [...ARIARule, { elm: "label" }]; const rules = { CSSContent: [{ elm: "style" }, { fromsubtree: true }], HTMLCell: [...ARIARule, { fromsubtree: true }, { attr: "title" }], HTMLControl: [ ...HTMLControlHeadRule, { fromsubtree: true }, { attr: "title" }, ], HTMLElm: [...ARIARule, { attr: "title" }], HTMLImg: [...ARIARule, { attr: "alt" }, { attr: "title" }], HTMLImgEmptyAlt: [...ARIARule, { attr: "title" }, { attr: "alt" }], HTMLInputButton: [ ...HTMLControlHeadRule, { attr: "value" }, { attr: "title" }, ], HTMLInputImage: [ ...HTMLControlHeadRule, { attr: "alt" }, { attr: "value" }, { attr: "title" }, ], HTMLInputImageNoValidSrc: [ ...HTMLControlHeadRule, { attr: "alt" }, { attr: "value" }, ], HTMLInputReset: [...HTMLControlHeadRule, { attr: "value" }], HTMLInputSubmit: [...HTMLControlHeadRule, { attr: "value" }], HTMLLink: [...ARIARule, { fromsubtree: true }, { attr: "title" }], HTMLLinkImage: [...ARIARule, { fromsubtree: true }, { attr: "title" }], HTMLOption: [ ...ARIARule, { attr: "label" }, { fromsubtree: true }, { attr: "title" }, ], HTMLTable: [ ...ARIARule, { elm: "caption" }, { attr: "summary" }, { attr: "title" }, ], }; const markupTests = [ { id: "btn", ruleset: "HTMLControl", markup: ` test2 test3 `, expected: ["test2 test3", "test1", "test4", "press me", "test5"], }, { id: "btn", ruleset: "HTMLInputButton", markup: ` test2 test3 `, expected: [ "test2 test3", "test1", "test4", "name from value", "name from title", ], }, { id: "btn-submit", ruleset: "HTMLInputSubmit", markup: ` test2 test3 `, expected: ["test2 test3", "test1", "test4", "name from value"], }, { id: "btn-reset", ruleset: "HTMLInputReset", markup: ` test2 test3 `, expected: ["test2 test3", "test1", "test4", "name from value"], }, { id: "btn-image", ruleset: "HTMLInputImage", markup: ` test2 test3 `, expected: [ "test2 test3", "test1", "test4", "name from alt", "name from value", "name from title", ], }, { id: "btn-image", ruleset: "HTMLInputImageNoValidSrc", markup: ` test2 test3 `, expected: [ "test2 test3", "test1", "test4", "name from alt", "name from value", ], }, { id: "opt", ruleset: "HTMLOption", markup: ` test2 test3 `, expected: ["test2 test3", "test1", "test4", "option1", "test5"], }, { id: "img", ruleset: "HTMLImg", markup: ` test2 test3 Mozilla logo`, expected: [ "test2 test3", "Logo of Mozilla", "Mozilla logo", "This is a logo", ], }, { id: "tc", ruleset: "HTMLCell", markup: ` test2 test3

This is a paragraph

This is a link
  • This is a list
`, expected: [ "test2 test3", "test1", "This is a paragraph This is a link \u2022 This is a list", "test5", ], }, { id: "gc", ruleset: "HTMLCell", markup: ` test2 test3

This is a paragraph

This is a link
  • Listitem1
  • Listitem2
`, expected: [ "test2 test3", "test1", "This is a paragraph This is a link \u2022 Listitem1 \u2022 Listitem2", "This is a paragraph This is a link This is a list", ], }, { id: "t", ruleset: "HTMLTable", markup: ` lby_tst6_1 lby_tst6_2
caption_tst6
cell1 cell2
`, expected: [ "lby_tst6_1 lby_tst6_2", "arialabel_tst6", "caption_tst6", "summary_tst6", "title_tst6", ], }, { id: "btn", ruleset: "CSSContent", markup: `
`, expected: ["do not press me", "press me"], }, { // TODO: uncomment when Bug-1256382 is resoved. // id: 'li', // ruleset: 'CSSContent', // markup: ` // // `, // expected: ['1. Listitem', `${String.fromCharCode(0x2022)} Listitem`] // }, { id: "a", ruleset: "HTMLLink", markup: ` test2 test3 test5`, expected: ["test2 test3", "test1", "test5", "test4"], }, { id: "a-img", ruleset: "HTMLLinkImage", markup: ` test2 test3 test5`, expected: ["test2 test3", "test1", "test5", "test4"], }, ]; /** * Test accessible name that is calculated from an attribute, remove the * attribute before proceeding to the next name test. If attribute removal * results in a reorder or text inserted event - wait for it. If accessible * becomes defunct, update its reference using the one that is attached to one * of the above events. * * @param {object} browser current "tabbrowser" element * @param {object} target { acc, id } structure that contains an * accessible and its content element * id. * @param {object} rule current attr rule for name calculation * @param {[type]} expected expected name value */ async function testAttrRule(browser, target, rule, expected) { let { id, acc } = target; let { attr } = rule; testName(acc, expected); let nameChange = waitForEvent(EVENT_NAME_CHANGE, id); await invokeContentTask(browser, [id, attr], (contentId, contentAttr) => { content.document.getElementById(contentId).removeAttribute(contentAttr); }); let event = await nameChange; // Update accessible just in case it is now defunct. target.acc = findAccessibleChildByID(event.accessible, id); } /** * Test accessible name that is calculated from an element name, remove the * element before proceeding to the next name test. If element removal results * in a reorder event - wait for it. If accessible becomes defunct, update its * reference using the one that is attached to a possible reorder event. * * @param {object} browser current "tabbrowser" element * @param {object} target { acc, id } structure that contains an * accessible and its content element * id. * @param {object} rule current elm rule for name calculation * @param {[type]} expected expected name value */ async function testElmRule(browser, target, rule, expected) { let { id, acc } = target; let { elm } = rule; testName(acc, expected); let nameChange = waitForEvent(EVENT_NAME_CHANGE, id); await invokeContentTask(browser, [elm], contentElm => { content.document.querySelector(`${contentElm}`).remove(); }); let event = await nameChange; // Update accessible just in case it is now defunct. target.acc = findAccessibleChildByID(event.accessible, id); } /** * Test accessible name that is calculated from its subtree, remove the subtree * and wait for a reorder event before proceeding to the next name test. If * accessible becomes defunct, update its reference using the one that is * attached to a reorder event. * * @param {object} browser current "tabbrowser" element * @param {object} target { acc, id } structure that contains an * accessible and its content element * id. * @param {object} rule current subtree rule for name calculation * @param {[type]} expected expected name value */ async function testSubtreeRule(browser, target, rule, expected) { let { id, acc } = target; testName(acc, expected); let nameChange = waitForEvent(EVENT_NAME_CHANGE, id); await invokeContentTask(browser, [id], contentId => { let elm = content.document.getElementById(contentId); while (elm.firstChild) { elm.firstChild.remove(); } }); let event = await nameChange; // Update accessible just in case it is now defunct. target.acc = findAccessibleChildByID(event.accessible, id); } /** * Iterate over a list of rules and test accessible names for each one of the * rules. * * @param {object} browser current "tabbrowser" element * @param {object} target { acc, id } structure that contains an * accessible and its content element * id. * @param {Array} ruleset A list of rules to test a target with * @param {Array} expected A list of expected name value for each rule */ async function testNameRule(browser, target, ruleset, expected) { for (let i = 0; i < ruleset.length; ++i) { let rule = ruleset[i]; let testFn; if (rule.attr) { testFn = testAttrRule; } else if (rule.elm) { testFn = testElmRule; } else if (rule.fromsubtree) { testFn = testSubtreeRule; } await testFn(browser, target, rule, expected[i]); } } markupTests.forEach(({ id, ruleset, markup, expected }) => addAccessibleTask( markup, async function (browser, accDoc) { const observer = { observe(subject) { const event = subject.QueryInterface(nsIAccessibleEvent); console.log(eventToString(event)); }, }; Services.obs.addObserver(observer, "accessible-event"); // Find a target accessible from an accessible subtree. let acc = findAccessibleChildByID(accDoc, id); let target = { id, acc }; await testNameRule(browser, target, rules[ruleset], expected); Services.obs.removeObserver(observer, "accessible-event"); }, { iframe: true, remoteIframe: true } ) ); /** * Generic test cases ported from mochitest/name/test_general.html */ addAccessibleTask( `

text
text1 text2
nomore text
nomore hidden text2 hidden2

text
more text

textTEXTtext

textimage


text
text

Choose country from.

Country

  
14
i am visible i am hidden
menuitem 2

This is a paragraph inside the article.

heading

aria_heading
15

Image: x^2 + y^2 + z^2

Text: x2 + y2 + z2

subtree
ab
ab
ab
ab
label
label
root
sub
label
This content should not be included in the grouping's label.
abc
a

b

c
a

b
shadowButtonVisibleText shadowButtonHiddenText
before code sup sub ins del after `, async function testGeneric(browser, docAcc) { // aria-label function testName_(id, expectedName) { const acc = findAccessibleChildByID(docAcc, id); testName(acc, expectedName); } // Simple label provided via ARIA testName_("btn_simple_aria_label", "I am a button"); // aria-label and aria-labelledby, expect aria-labelledby testName_("btn_both_aria_labels", "text I am a button, two"); // //////////////////////////////////////////////////////////////////////// // aria-labelledby // Single relation. The value of 'aria-labelledby' contains the ID of // an element. Gets the name from text node of that element. testName_("btn_labelledby_text", "text"); // Multiple relations. The value of 'aria-labelledby' contains the IDs // of elements. Gets the name from text nodes of those elements. testName_("btn_labelledby_texts", "text1 text2"); // //////////////////////////////////////////////////////////////////////// // Name from named accessible testName_("input_labelledby_namedacc", "Data"); // //////////////////////////////////////////////////////////////////////// // Name from subtree (single relation labelled_by). // Gets the name from text nodes contained by nested elements testName_("btn_labelledby_mixed", "nomore text"); // Gets the name from text nodes contained by nested elements, ignores // hidden elements (bug 443081). testName_("btn_labelledby_mixed_hidden_child", "nomore text2"); // Gets the name from hidden text nodes contained by nested elements, // (label element is hidden entirely), (bug 443081). testName_("btn_labelledby_mixed_hidden", "lala more hidden text"); // Gets the name from text nodes contained by nested elements having block // representation (every text node value in the name should be divided by // spaces) testName_("btn_labelledby_mixed_block", "text more text"); // Gets the name from text nodes contained by html:td. The text nodes // should not be divided by spaces. testName_("btn_labelledby_mixed_table", "textTEXTtext"); // Gets the name from image accessible. testName_("btn_labelledby_mixed_img", "text image"); // Gets the name from input accessibles // Note: if input have label elements then the name isn't calculated // from them. testName_( "btn_labelledby_mixed_input", "input button Submit Query Reset Submit Query" ); // Gets the name from the title of object element. testName_("btn_labelledby_mixed_object", "object"); // Gets the name from text nodes. Element br adds space between them. testName_("btn_labelledby_mixed_br", "text text"); // Gets the name from label content which allows name from subtree, // ignore @title attribute on label testName_("from_label_ignoretitle", "Country:"); // Gets the name from html:p content, which doesn't allow name from // subtree, ignore @title attribute on label testName_("from_p_ignoretitle", "Choose country from."); // Gets the name from html:input value, ignore @title attribute on input testName_("from_input_ignoretitle", "Custom country"); // Insert spaces around the control's value to not jam sibling text nodes testName_("insert_spaces_around_control", "start value end"); // Gets the name from @title, ignore whitespace content testName_("from_label_ignore_ws_subtree", "about"); // role="alert" doesn't get name from subtree... testName_("alert", null); // but the subtree is used if referenced by aria-labelledby. testName_("inputLabelledByAlert", "Error"); // //////////////////////////////////////////////////////////////////////// // label element // The label element contains the button. The name of the button is // calculated from the content of the label. // Note: the name does not contain the content of the button. testName_("btn_label_inside", "texttext"); // The label element and the button are placed in the same form. Gets // the name from the label subtree. testName_("btn_label_inform", "in form"); // The label element is placed outside of form where the button is. // Take into account the label. testName_("btn_label_outform", "out form"); // The label element and the button are in the same document. Gets the // name from the label subtree. testName_("btn_label_indocument", "in document"); // Multiple label elements for single button testName_("btn_label_multi", "label1 label2"); // Multiple controls inside a label element testName_("ctrl_in_label_1", "Enable a button control"); testName_("ctrl_in_label_2", "button"); // //////////////////////////////////////////////////////////////////////// // name from children // ARIA role button is presented allowing the name calculation from // children. testName_("btn_children", "14"); // html:button, no name from content testName_("btn_nonamefromcontent", null); // ARIA role option is presented allowing the name calculation from // visible children (bug 443081). testName_("lb_opt1_children_hidden", "i am visible"); // Get the name from subtree of menuitem crossing role nothing to get // the name from its children. testName_("tablemenuitem", "menuitem 1"); // Get the name from child acronym title attribute rather than from // acronym content. testName_("label_with_acronym", "O A T F World Wide Web"); testName_("testArticle", "Test article"); testName_("h1", "heading"); testName_("aria_heading", "aria_heading"); // //////////////////////////////////////////////////////////////////////// // title attribute // If nothing is left. Let's try title attribute. testName_("btn_title", "title"); // //////////////////////////////////////////////////////////////////////// // textarea name // textarea's name should not have the value, which initially is specified // by a text child. testName_("textareawithchild", "Story is ended."); // new textarea name should not reflect the value change. let valueChanged = waitForEvent( EVENT_TEXT_VALUE_CHANGE, "textareawithchild" ); await invokeContentTask(browser, [], () => { content.document.getElementById("textareawithchild").value = "Bar"; }); await valueChanged; testName_("textareawithchild", "Story is ended."); // //////////////////////////////////////////////////////////////////////// // controls having a value used as a part of computed name testName_("ctrlvalue_progressbar:input", "foo 5 baz"); testName_("ctrlvalue_scrollbar:input", "foo 5 baz"); testName_("ctrlvalue_slider:input", "foo 5 baz"); testName_("ctrlvalue_spinbutton:input", "foo 5 baz"); testName_("ctrlvalue_combobox:input", "foo 5 baz"); testName_("ctrlvalue_meter:input", "foo 5 baz"); // /////////////////////////////////////////////////////////////////////// // label with nested combobox (test for 'f' item of name computation guide) testName_("comboinstart", "One day(s)."); testName_("combo3", "day(s)."); testName_("textboxinstart", "Two days."); testName_("textbox1", "days."); testName_("comboinmiddle", "Subscribe to ATOM feed."); testName_("combo4", "Subscribe to feed."); testName_( "comboinmiddle2", "Play the Haliluya sound when new mail arrives" ); testName_("combo5", null); // label isn't used as a name for control testName_("checkbox", "Play the Haliluya sound when new mail arrives"); testName_( "comboinmiddle3", "Play the Haliluya sound when new mail arrives" ); testName_("combo6", "Play the sound when new mail arrives"); testName_("comboinend", "This day was sunny"); testName_("combo7", "This day was"); testName_("textboxinend", "This day was sunny"); testName_("textbox2", "This day was"); // placeholder testName_("ph_password", "a placeholder"); testName_("ph_text", "a placeholder"); testName_("ph_textarea", "a placeholder"); testName_("ph_text2", "a label"); testName_("ph_textarea2", "a label"); testName_("ph_text3", "a label"); // Test equation image testName_("img_eq", "x^2 + y^2 + z^2"); testName_("input_img_eq", "x^2 + y^2 + z^2"); testName_("txt_eq", "x^2 + y^2 + z^2"); // ////////////////////////////////////////////////////////////////////// // tests for duplicate announcement of content testName_("test_note", null); // ////////////////////////////////////////////////////////////////////// // Tests for name from sub tree of tr element. // By default, we want no name. testName_("NoNameForTR", null); testName_("NoNameForNonStandardTR", null); // But we want it if the tr has an ARIA role. testName_("NameForTRBecauseStrongARIA", "a b"); // But not if it is a weak (landmark) ARIA role testName_("NoNameForTRBecauseWeakARIA", null); // Name from sub tree of grouping if requested by other accessible. testName_("grouping", null); testName_("requested_name_from_grouping", "label"); testName_("listitem_containing_block_tbody", "label"); // Groupings shouldn't be included when calculating from the subtree of // a treeitem. testName_("treeitem_containing_grouping", "root"); // Name from subtree of grouping labelled by an ancestor. testName_("grouping_labelledby_ancestor", "label"); // Name from subtree of a container containing text nodes and inline // elements. There should be no spaces because everything is inline. testName_("container_text_inline", "abc"); // Name from subtree of a container containing text nodes and block // elements. There should be a space on both sides of the block. testName_("container_text_block", "a b c"); // Name from subtree of a container containing text nodes and empty // block elements. There should be space on either side of the blocks, but // not a double space. testName_("container_text_emptyblock", "a b"); let reordered = waitForEvent(EVENT_REORDER, "shadowHost"); await invokeContentTask(browser, [], () => { const shadowHost = content.document.getElementById("shadowHost"); const shadowRoot = shadowHost.attachShadow({ mode: "open" }); shadowRoot.append( content.document .getElementById("shadowTemplate") .content.cloneNode(true) ); }); await reordered; const shadowRoot = findAccessibleChildByID(docAcc, "shadowHost"); testName(shadowRoot.firstChild, "shadowButtonVisibleText"); testName(shadowRoot.lastChild, "shadowButtonHiddenText"); // Label from a hidden element containing script and style elements. testName_("buttonScriptStyle", "content"); // Name from subtree on a link containing , etc. testName_("linkWithCodeSupSubInsDel", "before code sup sub ins del after"); }, { topLevel: true, chrome: true } );