fune/toolkit/content/tests/chrome/test_custom_element_base.xhtml
Emilio Cobos Álvarez 4e978b56b5 Bug 1689816 - Implement <tabpanels> and <deck> without XUL layout. r=Gijs,Jamie,morgan,preferences-reviewers,mconley,TYLin
Gijs for front-end bits, layout for the new CSS properties and the
removal of nsDeckFrame / nsStackLayout, Jamie and Morgan for the a11y
changes.

As discussed in the bug, the main tricky part here is handling a11y
correctly. For <deck>, that's trivial (just use `visibility: hidden` to
hide the panels visually, while removing the unselected panels from the
a11y tree).

For <tabpanels> however we need to do something special. We do want to
hide stuff visually, but we want to preserve the contents in the a11y
tree.

For that, the easiest fix is introducing a new privileged CSS property
(-moz-subtree-hidden-only-visually), which takes care of not painting
the frame, but marks stuff offscreen in the accessibility tree. This is
not intended to be a property used widely.

Other than that, the changes are relatively straight-forward, though
some of the accessible/mac changes I could get a sanity-check on.

Differential Revision: https://phabricator.services.mozilla.com/D157875
2022-09-27 04:18:16 +00:00

363 lines
14 KiB
HTML

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
<window title="Custom Element Base Class Tests"
onload="runTests();"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<!-- test results are displayed in the html:body -->
<body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
<button id="one"/>
<simpleelement id="two" style="-moz-user-focus: normal;"/>
<simpleelement id="three" disabled="true" style="-moz-user-focus: normal;"/>
<button id="four"/>
<inherited-element-declarative foo="fuagra" empty-string=""></inherited-element-declarative>
<inherited-element-derived foo="fuagra"></inherited-element-derived>
<inherited-element-shadowdom-declarative foo="fuagra" empty-string=""></inherited-element-shadowdom-declarative>
<inherited-element-imperative foo="fuagra" empty-string=""></inherited-element-imperative>
<inherited-element-beforedomloaded foo="fuagra" empty-string=""></inherited-element-beforedomloaded>
<!-- test code running before page load goes here -->
<script type="application/javascript"><![CDATA[
class InheritAttrsChangeBeforDOMLoaded extends MozXULElement {
static get inheritedAttributes() {
return {
"label": "foo",
};
}
connectedCallback() {
this.append(MozXULElement.parseXULToFragment(`<label />`));
this.label = this.querySelector("label");
this.initializeAttributeInheritance();
is(this.label.getAttribute("foo"), "fuagra",
"InheritAttrsChangeBeforDOMLoaded: attribute should be propagated #1");
this.setAttribute("foo", "chuk&gek");
is(this.label.getAttribute("foo"), "chuk&gek",
"InheritAttrsChangeBeforDOMLoaded: attribute should be propagated #2");
}
}
customElements.define("inherited-element-beforedomloaded",
InheritAttrsChangeBeforDOMLoaded);
]]>
</script>
<!-- test code goes here -->
<script type="application/javascript"><![CDATA[
SimpleTest.waitForExplicitFinish();
async function runTests() {
ok(MozXULElement, "MozXULElement defined on the window");
testMixin();
testBaseControl();
testBaseControlMixin();
testBaseText();
testParseXULToFragment();
testInheritAttributes();
await testCustomInterface();
let htmlWin = await new Promise(resolve => {
let htmlIframe = document.createXULElement("iframe");
htmlIframe.src = "file_empty.xhtml";
htmlIframe.onload = () => resolve(htmlIframe.contentWindow);
document.documentElement.appendChild(htmlIframe);
});
ok(htmlWin.MozXULElement, "MozXULElement defined on a chrome HTML window");
SimpleTest.finish();
}
function testMixin() {
ok(MozElements.MozElementMixin, "Mixin exists");
let MixedHTMLElement = MozElements.MozElementMixin(HTMLElement);
ok(MixedHTMLElement.insertFTLIfNeeded, "Mixed in class contains helper functions");
}
function testBaseControl() {
ok(MozElements.BaseControl, "BaseControl exists");
ok("disabled" in MozElements.BaseControl.prototype,
"BaseControl prototype contains base control attributes");
}
function testBaseControlMixin() {
ok(MozElements.BaseControlMixin, "Mixin exists");
let MixedHTMLSpanElement = MozElements.MozElementMixin(HTMLSpanElement);
let HTMLSpanBaseControl = MozElements.BaseControlMixin(MixedHTMLSpanElement);
ok("disabled" in HTMLSpanBaseControl.prototype, "Mixed in class prototype contains base control attributes");
}
function testBaseText() {
ok(MozElements.BaseText, "BaseText exists");
ok("label" in MozElements.BaseText.prototype,
"BaseText prototype inherits BaseText attributes");
ok("disabled" in MozElements.BaseText.prototype,
"BaseText prototype inherits BaseControl attributes");
}
function testParseXULToFragment() {
ok(MozXULElement.parseXULToFragment, "parseXULToFragment helper exists");
let frag = MozXULElement.parseXULToFragment(`<deck id='foo' />`);
ok(DocumentFragment.isInstance(frag));
document.documentElement.appendChild(frag);
let deck = document.documentElement.lastChild;
ok(deck instanceof MozXULElement, "instance of MozXULElement");
ok(XULElement.isInstance(deck), "instance of XULElement");
is(deck.id, "foo", "attribute set");
is(deck.selectedIndex, 0, "Custom Element is property attached");
deck.remove();
info("Checking that whitespace text is removed but non-whitespace text isn't");
let boxWithWhitespaceText = MozXULElement.parseXULToFragment(`<box> </box>`).querySelector("box");
is(boxWithWhitespaceText.textContent, "", "Whitespace removed");
let boxWithNonWhitespaceText = MozXULElement.parseXULToFragment(`<box>foo</box>`).querySelector("box");
is(boxWithNonWhitespaceText.textContent, "foo", "Non-whitespace not removed");
try {
// we didn't encode the & as &amp;
MozXULElement.parseXULToFragment(`<box id="foo=1&bar=2"/>`);
ok(false, "parseXULToFragment should've thrown an exception for not-well-formed XML");
}
catch (ex) {
is(ex.message, "not well-formed XML", "parseXULToFragment threw the wrong message");
}
}
function testInheritAttributes() {
class InheritsElementDeclarative extends MozXULElement {
static get inheritedAttributes() {
return {
"label": "text=label,foo,empty-string,bardo=bar",
"unmatched": "foo", // Make sure we don't throw on unmatched selectors
};
}
connectedCallback() {
this.textContent = "";
this.append(MozXULElement.parseXULToFragment(`<label />`));
this.label = this.querySelector("label");
this.initializeAttributeInheritance();
}
}
customElements.define("inherited-element-declarative", InheritsElementDeclarative);
let declarativeEl = document.querySelector("inherited-element-declarative");
ok(declarativeEl, "declarative inheritance element exists");
class InheritsElementDerived extends InheritsElementDeclarative {
static get inheritedAttributes() {
return { label: "renamedfoo=foo" };
}
}
customElements.define("inherited-element-derived", InheritsElementDerived);
class InheritsElementShadowDOMDeclarative extends MozXULElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
static get inheritedAttributes() {
return {
"label": "text=label,foo,empty-string,bardo=bar",
"unmatched": "foo", // Make sure we don't throw on unmatched selectors
};
}
connectedCallback() {
this.shadowRoot.textContent = "";
this.shadowRoot.append(MozXULElement.parseXULToFragment(`<label />`));
this.label = this.shadowRoot.querySelector("label");
this.initializeAttributeInheritance();
}
}
customElements.define("inherited-element-shadowdom-declarative", InheritsElementShadowDOMDeclarative);
let shadowDOMDeclarativeEl = document.querySelector("inherited-element-shadowdom-declarative");
ok(shadowDOMDeclarativeEl, "declarative inheritance element with shadow DOM exists");
class InheritsElementImperative extends MozXULElement {
static get observedAttributes() {
return [ "label", "foo", "empty-string", "bar" ];
}
attributeChangedCallback(name, oldValue, newValue) {
if (this.label && oldValue != newValue) {
this.inherit();
}
}
inherit() {
let map = {
"label": [[ "label", "text" ]],
"foo": [[ "label", "foo" ]],
"empty-string": [[ "label", "empty-string" ]],
"bar": [[ "label", "bardo" ]],
};
for (let attr of InheritsElementImperative.observedAttributes) {
this.inheritAttribute(map[attr], attr);
}
}
connectedCallback() {
// Typically `initializeAttributeInheritance` handles this for us:
this._inheritedElements = null;
this.textContent = "";
this.append(MozXULElement.parseXULToFragment(`<label />`));
this.label = this.querySelector("label");
this.inherit();
}
}
customElements.define("inherited-element-imperative", InheritsElementImperative);
let imperativeEl = document.querySelector("inherited-element-imperative");
ok(imperativeEl, "imperative inheritance element exists");
function checkElement(el) {
is(el.label.getAttribute("foo"), "fuagra", "predefined attribute @foo");
ok(el.label.hasAttribute("empty-string"), "predefined attribute @empty-string");
ok(!el.label.hasAttribute("bardo"), "predefined attribute @bardo");
ok(!el.label.textContent, "predefined attribute @label");
el.setAttribute("empty-string", "not-empty-anymore");
is(el.label.getAttribute("empty-string"), "not-empty-anymore",
"attribute inheritance: empty-string");
el.setAttribute("label", "label-test");
is(el.label.textContent, "label-test",
"attribute inheritance: text=label attribute change");
el.setAttribute("bar", "bar-test");
is(el.label.getAttribute("bardo"), "bar-test",
"attribute inheritance: `=` mapping");
el.label.setAttribute("bardo", "changed-from-child");
is(el.label.getAttribute("bardo"), "changed-from-child",
"attribute inheritance: doesn't apply when host attr hasn't changed and child attr was changed");
el.label.removeAttribute("bardo");
ok(!el.label.hasAttribute("bardo"),
"attribute inheritance: doesn't apply when host attr hasn't changed and child attr was removed");
el.setAttribute("bar", "changed-from-host");
is(el.label.getAttribute("bardo"), "changed-from-host",
"attribute inheritance: does apply when host attr has changed and child attr was changed");
el.removeAttribute("bar");
ok(!el.label.hasAttribute("bardo"),
"attribute inheritance: does apply when host attr has been removed");
el.setAttribute("bar", "changed-from-host-2");
is(el.label.getAttribute("bardo"), "changed-from-host-2",
"attribute inheritance: does apply when host attr has changed after being removed");
// Restore to the original state so this can be ran again with the same element:
el.removeAttribute("label");
el.removeAttribute("bar");
}
for (let el of [declarativeEl, shadowDOMDeclarativeEl, imperativeEl]) {
info(`Running checks for ${el.tagName}`);
checkElement(el);
info(`Remove and re-add ${el.tagName} to make sure attribute inheritance still works`);
el.replaceWith(el);
checkElement(el);
}
let derivedEl = document.querySelector("inherited-element-derived");
ok(derivedEl, "derived inheritance element exists");
ok(!derivedEl.label.hasAttribute("foo"),
"attribute inheritance: base class attribute is not applied in derived class that overrides it");
ok(derivedEl.label.hasAttribute("renamedfoo"),
"attribute inheritance: attribute defined in derived class is present");
}
async function testCustomInterface() {
class SimpleElement extends MozXULElement {
get disabled() {
return this.getAttribute("disabled") == "true";
}
set disabled(val) {
if (val) this.setAttribute("disabled", "true");
else this.removeAttribute("disabled");
}
get tabIndex() {
return parseInt(this.getAttribute("tabIndex")) || 0;
}
set tabIndex(val) {
if (val) this.setAttribute("tabIndex", val);
else this.removeAttribute("tabIndex");
}
}
MozXULElement.implementCustomInterface(SimpleElement, [Ci.nsIDOMXULControlElement]);
customElements.define("simpleelement", SimpleElement);
let twoElement = document.getElementById("two");
is(document.documentElement.getCustomInterfaceCallback, undefined,
"No getCustomInterfaceCallback on non-custom element");
is(typeof twoElement.getCustomInterfaceCallback, "function",
"getCustomInterfaceCallback available on custom element when set");
is(document.documentElement.QueryInterface, undefined,
"Non-custom element should not have a QueryInterface implementation");
// Try various ways to get the custom interface.
let asControl = twoElement.getCustomInterfaceCallback(Ci.nsIDOMXULControlElement);
// XXX: Switched to from ok() to todo_is() in Bug 1467712. Follow up in 1500967
// Not sure if this was suppose to simply check for existence or equality?
todo_is(asControl, twoElement, "getCustomInterface returns interface implementation ");
asControl = twoElement.QueryInterface(Ci.nsIDOMXULControlElement);
ok(asControl, "QueryInterface to nsIDOMXULControlElement");
ok(Node.isInstance(asControl), "Control is a Node");
// Now make sure that the custom element handles focus/tabIndex as needed by shitfing
// focus around and enabling/disabling the simple elements.
// Enable Full Keyboard Access emulation on Mac
await SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]});
ok(!twoElement.disabled, "two is enabled");
ok(document.getElementById("three").disabled, "three is disabled");
await SimpleTest.promiseFocus();
ok(document.hasFocus(), "has focus");
// This should skip the disabled simpleelement.
synthesizeKey("VK_TAB");
is(document.activeElement.id, "one", "Tab 1");
synthesizeKey("VK_TAB");
is(document.activeElement.id, "two", "Tab 2");
synthesizeKey("VK_TAB");
is(document.activeElement.id, "four", "Tab 3");
twoElement.disabled = true;
is(twoElement.getAttribute("disabled"), "true", "two disabled after change");
synthesizeKey("VK_TAB", { shiftKey: true });
is(document.activeElement.id, "one", "Tab 1");
info("Checking that interfaces get inherited automatically with implementCustomInterface");
class ExtendedElement extends SimpleElement { }
MozXULElement.implementCustomInterface(ExtendedElement, [Ci.nsIDOMXULSelectControlElement]);
customElements.define("extendedelement", ExtendedElement);
const extendedInstance = document.createXULElement("extendedelement");
ok(extendedInstance.QueryInterface(Ci.nsIDOMXULSelectControlElement), "interface applied");
ok(extendedInstance.QueryInterface(Ci.nsIDOMXULControlElement), "inherited interface applied");
}
]]>
</script>
</window>