summaryrefslogtreecommitdiffstats
path: root/toolkit/content/tests/chrome/test_custom_element_base.xhtml
blob: 77cc8819a34d59dbe2c4aa855ea0bb5299edd72b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
<?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>