createProperty pushed onto the fluentProperties and mappedAttributes arrays in place, but reads them through the prototype chain. So a subclass adding a property mutated the array it inherited from an ancestor, leaking its attributes into that ancestor and every sibling. For instance moz-input-text's fluent "placeholder" leaked into moz-select. Copy the inherited array before pushing so each class gets its own. Differential Revision: https://phabricator.services.mozilla.com/D311568
276 lines
8.6 KiB
HTML
276 lines
8.6 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>MozLitElement Tests</title>
|
|
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
|
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
|
|
<link
|
|
rel="stylesheet"
|
|
href="chrome://mochikit/content/tests/SimpleTest/test.css"
|
|
/>
|
|
<script>
|
|
let html, render, renderTarget, defaultTemplate, MozLitElement;
|
|
var mockL10n;
|
|
|
|
add_setup(async function setup() {
|
|
({ html, render } =
|
|
await import("chrome://global/content/vendor/lit.all.mjs"));
|
|
({ MozLitElement } =
|
|
await import("chrome://global/content/lit-utils.mjs"));
|
|
renderTarget = document.getElementById("render");
|
|
|
|
const l10nReg = new L10nRegistry();
|
|
const fs = [
|
|
{
|
|
path: "/localization/en-US/mock.ftl",
|
|
source: `
|
|
example-label =
|
|
.label = Example label!
|
|
.message = And a message!
|
|
.tooltiptext = Also a tooltiptext!
|
|
.rename = Also renamed!
|
|
.accesskey = E
|
|
has-custom-attr =
|
|
.label = CustomAttribute!
|
|
.message = message!
|
|
.tooltiptext = tooltiptext!
|
|
.rename = rename!
|
|
.accesskey = A
|
|
.myattr = Custom attribute
|
|
`,
|
|
},
|
|
];
|
|
const source = L10nFileSource.createMock(
|
|
"test",
|
|
"app",
|
|
["en-US"],
|
|
"/localization/{locale}",
|
|
fs
|
|
);
|
|
l10nReg.registerSources([source]);
|
|
mockL10n = new DOMLocalization(["/mock.ftl"], false, l10nReg, [
|
|
"en-US",
|
|
]);
|
|
mockL10n.addResourceIds(["/mock.ftl"]);
|
|
|
|
defaultTemplate = html`
|
|
<example-element data-l10n-id="example-label"></example-element>
|
|
`;
|
|
});
|
|
|
|
async function renderTemplate(template = defaultTemplate) {
|
|
render(template, renderTarget);
|
|
return renderTarget.firstElementChild;
|
|
}
|
|
|
|
function defineExampleElement() {
|
|
class ExampleElement extends MozLitElement {
|
|
static properties = {
|
|
label: { type: String, fluent: true },
|
|
message: { type: String, fluent: true },
|
|
tooltipText: { type: String, fluent: true },
|
|
renamedAttribute: {
|
|
type: String,
|
|
fluent: true,
|
|
attribute: "rename",
|
|
},
|
|
mappedAttribute: {
|
|
type: String,
|
|
mapped: true,
|
|
attribute: "mapped",
|
|
},
|
|
ariaLabel: { type: String, mapped: true },
|
|
accessKey: { type: String, mapped: true, fluent: true },
|
|
};
|
|
|
|
render() {
|
|
return (
|
|
this.label +
|
|
this.message +
|
|
this.tooltipText +
|
|
this.renamedAttribute +
|
|
this.accessKey
|
|
);
|
|
}
|
|
|
|
updated(changes) {
|
|
if (changes.has("label") && this.label) {
|
|
this.dispatchEvent(new CustomEvent("label-changed"));
|
|
}
|
|
}
|
|
}
|
|
customElements.define("example-element", ExampleElement);
|
|
}
|
|
|
|
add_task(async function testL10nAttrs() {
|
|
let el = await renderTemplate();
|
|
window.el = el;
|
|
defineExampleElement();
|
|
is(el.shadowRoot.textContent, "", "There's no text");
|
|
is(
|
|
el.dataset.l10nAttrs,
|
|
"label,message,tooltiptext,rename,accesskey",
|
|
"data-l10n-attrs is set"
|
|
);
|
|
await new Promise(r =>
|
|
el.addEventListener("label-changed", r, { once: true })
|
|
);
|
|
is(
|
|
el.shadowRoot.textContent,
|
|
"Example label!And a message!Also a tooltiptext!Also renamed!E",
|
|
"Text rendered automatically on upgrade"
|
|
);
|
|
|
|
is(el.accessKey, "E", "accessKey is mapped with custom attribute");
|
|
ok(!el.hasAttribute("accesskey"), "renamed attribute was removed");
|
|
});
|
|
|
|
add_task(async function testCustomL10nAttrs() {
|
|
let el = await renderTemplate(
|
|
html`<example-element
|
|
data-l10n-id="has-custom-attr"
|
|
data-l10n-attrs="myattr"
|
|
></example-element>`
|
|
);
|
|
await new Promise(r =>
|
|
el.addEventListener("label-changed", r, { once: true })
|
|
);
|
|
is(
|
|
el.shadowRoot.textContent,
|
|
"CustomAttribute!message!tooltiptext!rename!A",
|
|
"Text rendered automatically on upgrade"
|
|
);
|
|
is(
|
|
el.dataset.l10nAttrs,
|
|
"label,message,tooltiptext,rename,accesskey,myattr",
|
|
"data-l10n-attrs is set"
|
|
);
|
|
is(
|
|
el.getAttribute("myattr"),
|
|
"Custom attribute",
|
|
"Custom attribute was set"
|
|
);
|
|
});
|
|
|
|
add_task(async function testMappedAttributes() {
|
|
let el = await renderTemplate(
|
|
html`<example-element
|
|
accesskey="f"
|
|
mapped="mapped-val"
|
|
aria-label="Label!"
|
|
></example-element>`
|
|
);
|
|
is(el.accessKey, "f", "accessKey property is correct");
|
|
ok(!el.hasAttribute("accesskey"), "accesskey attribute was removed");
|
|
|
|
is(
|
|
el.mappedAttribute,
|
|
"mapped-val",
|
|
"mappedAttribute is mapped with custom attribute"
|
|
);
|
|
ok(!el.hasAttribute("mapped"), "mapped attribute was removed");
|
|
|
|
is(el.ariaLabel, "Label!", "ariaLabel property is set");
|
|
ok(!el.hasAttribute("aria-label"), "aria-label was removed");
|
|
});
|
|
|
|
function defineSiblingElements() {
|
|
class SiblingBase extends MozLitElement {
|
|
static properties = {
|
|
label: { type: String, fluent: true },
|
|
description: { type: String, fluent: true },
|
|
ariaLabel: { type: String, mapped: true },
|
|
};
|
|
|
|
render() {
|
|
return this.label;
|
|
}
|
|
}
|
|
customElements.define("sibling-base", SiblingBase);
|
|
|
|
class SiblingA extends SiblingBase {
|
|
static properties = {
|
|
propA: { type: String, fluent: true },
|
|
mappedA: { type: String, mapped: true, attribute: "mapped-a" },
|
|
};
|
|
}
|
|
customElements.define("sibling-a", SiblingA);
|
|
|
|
class SiblingB extends SiblingBase {
|
|
static properties = {
|
|
propB: { type: String, fluent: true },
|
|
mappedB: { type: String, mapped: true, attribute: "mapped-b" },
|
|
};
|
|
}
|
|
customElements.define("sibling-b", SiblingB);
|
|
}
|
|
|
|
// Regression test: fluentProperties and mappedAttributes are stored on the
|
|
// class, so a subclass adding its own must not mutate the array it inherits
|
|
// from an ancestor, which would leak that subclass's attributes into its
|
|
// parent and every sibling (and, since classes finalize lazily on first
|
|
// use, do so in an order-dependent, flaky way).
|
|
add_task(async function testFluentPropertiesNotSharedAcrossSubclasses() {
|
|
defineSiblingElements();
|
|
|
|
// Rendering upgrades the elements, which sets data-l10n-attrs from each
|
|
// class's fluentProperties.
|
|
render(
|
|
html`<sibling-base></sibling-base>
|
|
<sibling-a></sibling-a>
|
|
<sibling-b></sibling-b>`,
|
|
renderTarget
|
|
);
|
|
let [baseEl, aEl, bEl] = renderTarget.children;
|
|
await Promise.all([
|
|
baseEl.updateComplete,
|
|
aEl.updateComplete,
|
|
bEl.updateComplete,
|
|
]);
|
|
|
|
is(
|
|
baseEl.dataset.l10nAttrs,
|
|
"label,description",
|
|
"Base element only exposes its own fluent attributes"
|
|
);
|
|
is(
|
|
aEl.dataset.l10nAttrs,
|
|
"label,description,propa",
|
|
"Sibling A inherits the base attributes plus its own, not sibling B's"
|
|
);
|
|
is(
|
|
bEl.dataset.l10nAttrs,
|
|
"label,description,propb",
|
|
"Sibling B inherits the base attributes plus its own, not sibling A's"
|
|
);
|
|
|
|
isDeeply(
|
|
baseEl.constructor.mappedAttributes,
|
|
[["ariaLabel", "ariaLabelAttribute"]],
|
|
"Base element only has its own mapped attribute"
|
|
);
|
|
isDeeply(
|
|
aEl.constructor.mappedAttributes,
|
|
[
|
|
["ariaLabel", "ariaLabelAttribute"],
|
|
["mappedA", "mappedAAttribute"],
|
|
],
|
|
"Sibling A has the base mapped attribute plus its own, not sibling B's"
|
|
);
|
|
isDeeply(
|
|
bEl.constructor.mappedAttributes,
|
|
[
|
|
["ariaLabel", "ariaLabelAttribute"],
|
|
["mappedB", "mappedBAttribute"],
|
|
],
|
|
"Sibling B has the base mapped attribute plus its own, not sibling A's"
|
|
);
|
|
});
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="render"></div>
|
|
</body>
|
|
</html>
|