summaryrefslogtreecommitdiffstats
path: root/dom/l10n/tests/mochitest
diff options
context:
space:
mode:
Diffstat (limited to 'dom/l10n/tests/mochitest')
-rw-r--r--dom/l10n/tests/mochitest/browser.ini6
-rw-r--r--dom/l10n/tests/mochitest/chrome.ini47
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/README.txt3
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/non-system-principal/README.txt3
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/non-system-principal/browser_resource_uri.js109
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/non-system-principal/localization/test.ftl4
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/non-system-principal/test.html37
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent.html90
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent_lazy.html98
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/test_docl10n.html66
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/test_docl10n.xhtml60
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/test_docl10n_lazy.html44
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/test_docl10n_ready_rejected.html29
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/test_docl10n_removeResourceIds.html59
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html54
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/test_telemetry.html83
-rw-r--r--dom/l10n/tests/mochitest/document_l10n/test_unpriv_iframe.html26
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html49
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html45
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html72
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html60
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml68
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_getAttributes.html49
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html57
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_overlay.html60
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html37
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html53
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html50
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html52
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html60
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_setAttributes.html79
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_translateElements.html47
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html48
-rw-r--r--dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html56
-rw-r--r--dom/l10n/tests/mochitest/l10n_mutations/test_append_content_post_dcl.html30
-rw-r--r--dom/l10n/tests/mochitest/l10n_mutations/test_append_content_pre_dcl.html28
-rw-r--r--dom/l10n/tests/mochitest/l10n_mutations/test_append_fragment_post_dcl.html39
-rw-r--r--dom/l10n/tests/mochitest/l10n_mutations/test_disconnectedRoot_webcomponent.html148
-rw-r--r--dom/l10n/tests/mochitest/l10n_mutations/test_pause_observing.html44
-rw-r--r--dom/l10n/tests/mochitest/l10n_mutations/test_remove_element.html68
-rw-r--r--dom/l10n/tests/mochitest/l10n_mutations/test_remove_fragment.html67
-rw-r--r--dom/l10n/tests/mochitest/l10n_mutations/test_set_attributes.html37
-rw-r--r--dom/l10n/tests/mochitest/l10n_mutations/test_template.html37
-rw-r--r--dom/l10n/tests/mochitest/l10n_overlays/test_attributes.html86
-rw-r--r--dom/l10n/tests/mochitest/l10n_overlays/test_extra_text_markup.html136
-rw-r--r--dom/l10n/tests/mochitest/l10n_overlays/test_functional_children.html344
-rw-r--r--dom/l10n/tests/mochitest/l10n_overlays/test_l10n_overlays.xhtml87
-rw-r--r--dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html57
-rw-r--r--dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html57
-rw-r--r--dom/l10n/tests/mochitest/l10n_overlays/test_text_children.html74
-rw-r--r--dom/l10n/tests/mochitest/l10n_overlays/test_title.html60
-rw-r--r--dom/l10n/tests/mochitest/mochitest.ini1
52 files changed, 3160 insertions, 0 deletions
diff --git a/dom/l10n/tests/mochitest/browser.ini b/dom/l10n/tests/mochitest/browser.ini
new file mode 100644
index 0000000000..966be56661
--- /dev/null
+++ b/dom/l10n/tests/mochitest/browser.ini
@@ -0,0 +1,6 @@
+[document_l10n/non-system-principal/browser_resource_uri.js]
+support-files =
+ document_l10n/non-system-principal/test.html
+ document_l10n/non-system-principal/localization/test.ftl
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
diff --git a/dom/l10n/tests/mochitest/chrome.ini b/dom/l10n/tests/mochitest/chrome.ini
new file mode 100644
index 0000000000..68f3d89d40
--- /dev/null
+++ b/dom/l10n/tests/mochitest/chrome.ini
@@ -0,0 +1,47 @@
+[l10n_overlays/test_attributes.html]
+[l10n_overlays/test_functional_children.html]
+[l10n_overlays/test_text_children.html]
+[l10n_overlays/test_extra_text_markup.html]
+[l10n_overlays/test_l10n_overlays.xhtml]
+[l10n_overlays/test_same_id.html]
+[l10n_overlays/test_same_id_args.html]
+[l10n_overlays/test_title.html]
+
+[l10n_mutations/test_append_content_post_dcl.html]
+[l10n_mutations/test_append_content_pre_dcl.html]
+[l10n_mutations/test_append_fragment_post_dcl.html]
+[l10n_mutations/test_disconnectedRoot_webcomponent.html]
+[l10n_mutations/test_set_attributes.html]
+[l10n_mutations/test_pause_observing.html]
+[l10n_mutations/test_template.html]
+[l10n_mutations/test_remove_element.html]
+[l10n_mutations/test_remove_fragment.html]
+
+[dom_localization/test_attr_sanitized.html]
+[dom_localization/test_getAttributes.html]
+[dom_localization/test_setAttributes.html]
+[dom_localization/test_translateElements.html]
+[dom_localization/test_translateFragment.html]
+[dom_localization/test_connectRoot.html]
+[dom_localization/test_connectRoot_webcomponent.html]
+[dom_localization/test_disconnectRoot.html]
+[dom_localization/test_repeated_l10nid.html]
+[dom_localization/test_translateRoots.html]
+[dom_localization/test_l10n_mutations.html]
+[dom_localization/test_overlay.html]
+[dom_localization/test_overlay_repeated.html]
+[dom_localization/test_overlay_missing_all.html]
+[dom_localization/test_overlay_missing_children.html]
+[dom_localization/test_overlay_sanitized.html]
+[dom_localization/test_domloc.xhtml]
+
+
+[document_l10n/test_docl10n.xhtml]
+[document_l10n/test_docl10n.html]
+[document_l10n/test_docl10n_sync.html]
+[document_l10n/test_docl10n_ready_rejected.html]
+[document_l10n/test_docl10n_removeResourceIds.html]
+[document_l10n/test_docl10n_lazy.html]
+[document_l10n/test_connectRoot_webcomponent.html]
+[document_l10n/test_connectRoot_webcomponent_lazy.html]
+[document_l10n/test_telemetry.html]
diff --git a/dom/l10n/tests/mochitest/document_l10n/README.txt b/dom/l10n/tests/mochitest/document_l10n/README.txt
new file mode 100644
index 0000000000..b798a5039a
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/README.txt
@@ -0,0 +1,3 @@
+Tests in this directory cover support for DocumentL10n
+WebIDL API across different use cases such as
+processes, principals and so on.
diff --git a/dom/l10n/tests/mochitest/document_l10n/non-system-principal/README.txt b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/README.txt
new file mode 100644
index 0000000000..d0cc074166
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/README.txt
@@ -0,0 +1,3 @@
+Tests in this directory cover the functionality
+of DocumentL10n WebIDL API in non-system-principal
+scenario.
diff --git a/dom/l10n/tests/mochitest/document_l10n/non-system-principal/browser_resource_uri.js b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/browser_resource_uri.js
new file mode 100644
index 0000000000..a658e88bec
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/browser_resource_uri.js
@@ -0,0 +1,109 @@
+let uri =
+ "chrome://mochitests/content/browser/dom/l10n/tests/mochitest//document_l10n/non-system-principal/";
+let protocol = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+
+protocol.setSubstitution("l10n-test", Services.io.newURI(uri));
+
+// Since we want the mock source to work with all locales, we're going
+// to register it for currently used locales, and we'll put the path that
+// doesn't use the `{locale}` component to make it work irrelevant of
+// what locale the mochitest is running in.
+//
+// Notice: we're using a `chrome://` protocol here only for convenience reasons.
+// Real sources should use `resource://` protocol.
+let locales = Services.locale.appLocalesAsBCP47;
+
+// This source is actually using a real `FileSource` instead of a mocked one,
+// because we want to test that fetching real I/O out of the `uri` works in non-system-principal.
+let source = new L10nFileSource("test", "app", locales, `${uri}localization/`);
+L10nRegistry.getInstance().registerSources([source]);
+
+registerCleanupFunction(() => {
+ protocol.setSubstitution("l10n-test", null);
+ L10nRegistry.getInstance().removeSources(["test"]);
+ SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processPrelaunch.enabled", true]],
+ });
+});
+
+const kChildPage = getRootDirectory(gTestPath) + "test.html";
+
+const kAboutPagesRegistered = Promise.all([
+ BrowserTestUtils.registerAboutPage(
+ registerCleanupFunction,
+ "test-about-l10n-child",
+ kChildPage,
+ Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD |
+ Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ Ci.nsIAboutModule.ALLOW_SCRIPT
+ ),
+]);
+
+add_task(async () => {
+ // Bug 1640333 - windows fails (sometimes) to ever get document.l10n.ready
+ // if e10s process caching is enabled
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processPrelaunch.enabled", false],
+ ["dom.security.skip_about_page_has_csp_assert", true],
+ ],
+ });
+ await kAboutPagesRegistered;
+ await BrowserTestUtils.withNewTab(
+ "about:test-about-l10n-child",
+ async browser => {
+ await SpecialPowers.spawn(browser, [], async function () {
+ let document = content.document;
+ let window = document.defaultView;
+
+ await document.testsReadyPromise;
+
+ let principal = SpecialPowers.wrap(document).nodePrincipal;
+ is(
+ principal.spec,
+ "about:test-about-l10n-child",
+ "correct content principal"
+ );
+
+ let desc = document.getElementById("main-desc");
+
+ // We can test here for a particular value because we're
+ // using a mock file source which is locale independent.
+ //
+ // If you're writing a test that verifies that a UI
+ // widget got real localization, you should not rely on
+ // the particular value, but rather on the content not
+ // being empty (to keep the test pass in non-en-US locales).
+ is(desc.textContent, "This is a mock page title");
+
+ // Test for l10n.getAttributes
+ let label = document.getElementById("label1");
+ let l10nArgs = document.l10n.getAttributes(label);
+ is(l10nArgs.id, "subtitle");
+ is(l10nArgs.args.name, "Firefox");
+
+ // Test for manual value formatting
+ let customMsg = document.getElementById("customMessage").textContent;
+ is(customMsg, "This is a custom message formatted from JS.");
+
+ // Since we applied the `data-l10n-id` attribute
+ // on `label` in this microtask, we have to wait for
+ // the next paint to verify that the MutationObserver
+ // applied the translation.
+ await new Promise(resolve => {
+ let verifyL10n = () => {
+ if (!label.textContent.includes("Firefox")) {
+ window.requestAnimationFrame(verifyL10n);
+ } else {
+ resolve();
+ }
+ };
+
+ window.requestAnimationFrame(verifyL10n);
+ });
+ });
+ }
+ );
+});
diff --git a/dom/l10n/tests/mochitest/document_l10n/non-system-principal/localization/test.ftl b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/localization/test.ftl
new file mode 100644
index 0000000000..a5da5a8f00
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/localization/test.ftl
@@ -0,0 +1,4 @@
+page-title = This is a mock page title
+subtitle = This is a label for { $name }
+
+custom-message = This is a custom message formatted from JS.
diff --git a/dom/l10n/tests/mochitest/document_l10n/non-system-principal/test.html b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/test.html
new file mode 100644
index 0000000000..5d91f3da46
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/test.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DocumentL10n in HTML environment</title>
+ <link rel="localization" href="test.ftl"/>
+ <script type="text/javascript">
+ document.testsReadyPromise = new Promise((resolve) => {
+ // The test is in this file to ensure that we're testing
+ // the behavior in a non-system principal.
+ document.addEventListener("DOMContentLoaded", async () => {
+ await document.l10n.ready;
+
+ // Assign the localization from JS
+ let label = document.getElementById("label1");
+ document.l10n.setAttributes(
+ label,
+ "subtitle",
+ {
+ name: "Firefox",
+ }
+ );
+
+ const customMsg = await document.l10n.formatValue("custom-message");
+ document.getElementById("customMessage").textContent = customMsg;
+ resolve();
+ }, {once: true});
+ });
+ </script>
+</head>
+<body>
+ <h1 id="main-desc" data-l10n-id="page-title"></h1>
+
+ <p id="label1"></p>
+ <p id="customMessage"></p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent.html b/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent.html
new file mode 100644
index 0000000000..3f2def3547
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Web Component connecting into Document's l10n</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="browser/preferences/preferences.ftl"></link>
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ // In this test we are introducing two widgets. The only difference between them
+ // is that the first one is using `connectRoot` with the `aTranslate` argument set to `true`,
+ // and the other one to `false`.
+ //
+ // In this test, we will inject both of them into the DOM for parsing.
+ // For a test that verifies the behavior when they're injected lazily, see
+ // `test_connectRoot_webcomponent_lazy.html` test.
+ //
+ // Since both widgets get injected into DOM during parsing, we expect both of them
+ // to get translated before `document.l10n.ready` is resolved.
+
+ let passedTests = 0;
+
+ class FluentWidget extends HTMLElement {
+ constructor() {
+ super();
+ const shadowRoot = this.attachShadow({mode: "open"});
+ const t = document.querySelector("#fluent-widget-template");
+ const instance = t.content.cloneNode(true);
+ shadowRoot.appendChild(instance);
+ }
+ async connectedCallback() {
+ MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
+
+ document.l10n.connectRoot(this.shadowRoot, true);
+
+ let label = this.shadowRoot.getElementById("label");
+
+ await document.l10n.ready;
+ is(label.textContent, "Learn more", "localization content applied to element");
+ passedTests++;
+ if (passedTests == 2) {
+ SimpleTest.finish();
+ }
+ }
+ }
+
+ class FluentWidget2 extends HTMLElement {
+ constructor() {
+ super();
+ const shadowRoot = this.attachShadow({mode: "open"});
+ const t = document.querySelector("#fluent-widget-template");
+ const instance = t.content.cloneNode(true);
+ shadowRoot.appendChild(instance);
+ }
+ async connectedCallback() {
+ MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
+
+ document.l10n.connectRoot(this.shadowRoot, false);
+
+ let label = this.shadowRoot.getElementById("label");
+
+ await document.l10n.ready;
+ is(label.textContent, "Learn more", "localization content applied to element");
+ passedTests++;
+ if (passedTests == 2) {
+ SimpleTest.finish();
+ }
+ }
+ }
+
+ customElements.define("fluent-widget", FluentWidget);
+ customElements.define("fluent-widget2", FluentWidget2);
+ </script>
+</head>
+<body>
+ <template id="fluent-widget-template">
+ <div>
+ <button id="label" data-l10n-id="do-not-track-learn-more"></button>
+ </div>
+ </template>
+ <fluent-widget></fluent-widget>
+ <fluent-widget2></fluent-widget2>
+ <script>
+ // This trick makes sure that we connect the widgets before parsing is completed.
+ document.write("");
+ </script>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent_lazy.html b/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent_lazy.html
new file mode 100644
index 0000000000..b74bbc00c8
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent_lazy.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Web Component connecting into Document's l10n</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ // In this test we are introducing two widgets. The only difference between them
+ // is that the first one is using `connectRoot` with the `aTranslate` argument set to `true`,
+ // and the other one to `false`.
+ //
+ // In this test, we will inject both of them lazily, after initial parsing is completed.
+ // For a test that verifies the behavior when they're injected during parsing, see
+ // `test_connectRoot_webcomponent.html` test.
+ //
+ // The expected difference is that when both get lazily injected into the DOM, the first one
+ // will get translated, while the other will not.
+ // The latter behavior will be used by widgets that will want to translate the initial DOM on their
+ // own before connecting the root.
+
+ let firstWidgetTranslated = false;
+
+ class FluentWidget extends HTMLElement {
+ constructor() {
+ super();
+ const shadowRoot = this.attachShadow({mode: "open"});
+ const t = document.querySelector("#fluent-widget-template");
+ const instance = t.content.cloneNode(true);
+ shadowRoot.appendChild(instance);
+ }
+ connectedCallback() {
+ MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
+
+ document.l10n.connectRoot(this.shadowRoot, true);
+
+ let label = this.shadowRoot.getElementById("label");
+
+ let verifyL10n = () => {
+ if (label.textContent.length) {
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ is(label.textContent, "Learn more", "localization content applied to element");
+ firstWidgetTranslated = true;
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+ }
+ }
+
+ class FluentWidget2 extends HTMLElement {
+ constructor() {
+ super();
+ const shadowRoot = this.attachShadow({mode: "open"});
+ const t = document.querySelector("#fluent-widget-template");
+ const instance = t.content.cloneNode(true);
+ shadowRoot.appendChild(instance);
+ }
+ connectedCallback() {
+ MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
+
+ document.l10n.connectRoot(this.shadowRoot, false);
+
+ let label = this.shadowRoot.getElementById("label");
+
+ let verifyL10n = () => {
+ if (firstWidgetTranslated) {
+ is(label.textContent.length, 0, "This widget should remain untranslated.");
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+ }
+ }
+
+ customElements.define("fluent-widget", FluentWidget);
+ customElements.define("fluent-widget2", FluentWidget2);
+
+ window.addEventListener("load", () => {
+ window.requestIdleCallback(async () => {
+ let widget = document.createElement("fluent-widget");
+ document.body.appendChild(widget);
+ let widget2 = document.createElement("fluent-widget2");
+ document.body.appendChild(widget2);
+ });
+ }, { once: true });
+ </script>
+</head>
+<body>
+ <template id="fluent-widget-template">
+ <div>
+ <button id="label" data-l10n-id="do-not-track-learn-more"></button>
+ </div>
+ </template>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n.html
new file mode 100644
index 0000000000..12ff623f5c
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DocumentL10n in HTML environment</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+ <script>
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ is(document.l10n.ready && document.l10n.ready.then !== undefined, true,
+ "document.l10n.ready exists and is a Promise");
+
+ (async function() {
+ await document.l10n.ready;
+
+ const desc = document.getElementById("main-desc");
+ is(!!desc.textContent.length, true, "initial localization is applied");
+
+ const msg = await document.l10n.formatValue("id-heading");
+ is(!!msg.length, true, "value is formatted manually");
+
+ const label = document.getElementById("label1");
+ let l10nArgs = document.l10n.getAttributes(label);
+ is(l10nArgs.id, null, "id is null if not set");
+
+ SimpleTest.doesThrow(
+ () => {
+ const bad = {};
+ bad.bad = bad;
+ document.l10n.setAttributes(label, "date-crashed-heading", bad);
+ },
+ "an error is thrown for invalid args",
+ );
+
+ l10nArgs = document.l10n.getAttributes(label);
+ is(l10nArgs.id, null, "id is not set if args are invalid");
+
+ document.l10n.setAttributes(
+ label,
+ "date-crashed-heading",
+ {
+ name: "John",
+ }
+ );
+ ok(document.hasPendingL10nMutations, "Should have pending mutations");
+ l10nArgs = document.l10n.getAttributes(label);
+ is(l10nArgs.id, "date-crashed-heading", "id is set by setAttributes");
+ is(l10nArgs.args.name, "John", "args are set by setAttributes");
+ // Test for mutations applied.
+ document.addEventListener("L10nMutationsFinished", function() {
+ ok(!!label.textContent.length, "Should've applied translation");
+ ok(!document.hasPendingL10nMutations, "Should have no more pending mutations");
+ SimpleTest.finish();
+ }, { once: true });
+ })();
+ </script>
+</head>
+<body>
+ <h1 id="main-desc" data-l10n-id="crash-reports-title"></h1>
+
+ <p id="label1"></p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n.xhtml b/dom/l10n/tests/mochitest/document_l10n/test_docl10n.xhtml
new file mode 100644
index 0000000000..2d51d8689e
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n.xhtml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta charset="utf-8"></meta>
+ <title>Test DocumentL10n in HTML environment</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"></link>
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("DOMContentLoaded", async function() {
+ await document.l10n.ready;
+
+ // Test for initial localization applied.
+ let desc = document.getElementById("main-desc");
+ is(!!desc.textContent.length, true);
+
+ // Test for manual value formatting
+ let msg = await document.l10n.formatValue("id-heading");
+ is(!!msg.length, true);
+
+ // Test for mutations applied.
+ let verifyL10n = () => {
+ if (label.textContent.length) {
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+
+ let label = document.getElementById("label1");
+ document.l10n.setAttributes(
+ label,
+ "date-crashed-heading",
+ {
+ name: "John",
+ }
+ );
+
+ // Test for l10n.getAttributes
+ let l10nArgs = document.l10n.getAttributes(label);
+ is(l10nArgs.id, "date-crashed-heading");
+ is(l10nArgs.args.name, "John");
+ }, { once: true});
+ </script>
+</head>
+<body>
+ <h1 id="main-desc" data-l10n-id="crash-reports-title"></h1>
+
+ <p id="label1" />
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n_lazy.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_lazy.html
new file mode 100644
index 0000000000..6c3ddb73ed
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_lazy.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Lazy DocumentL10n in HTML environment</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ is(document.l10n, null, "document.l10n is null.");
+
+ window.addEventListener("load", async () => {
+ is(document.l10n, null, "document.l10n is null after load.");
+
+ let desc = document.getElementById("main-desc");
+ is(desc.textContent.length, 0, "main-desc is not translated");
+
+ let link = document.createElement("link");
+ link.setAttribute("rel", "localization");
+ link.setAttribute("href", "crashreporter/aboutcrashes.ftl");
+ document.head.appendChild(link);
+
+ // Verify now that `l10n.ready` exists and is fulfilled.
+ await document.l10n.ready;
+
+ // Lazy initialized localization should translate the document.
+ is(!!desc.textContent.length, true, "main-desc is translated");
+
+ document.head.removeChild(link);
+
+ is(document.l10n, null, "document.l10n is null");
+
+ SimpleTest.finish();
+ }, { once: true});
+ </script>
+</head>
+<body>
+ <h1 id="main-desc" data-l10n-id="crash-reports-title"></h1>
+
+ <p id="label1"></p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n_ready_rejected.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_ready_rejected.html
new file mode 100644
index 0000000000..63e18f802c
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_ready_rejected.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test mozIDOMLocalization.ready rejected state</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="path/to_non_existing.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("DOMContentLoaded", async function() {
+ /**
+ * Even when we fail to localize all elements, we will
+ * still resolve the `ready` promise to communicate that
+ * the initial translation phase is now completed.
+ */
+ document.l10n.ready.then(() => {
+ is(1, 1, "the ready should resolve");
+ SimpleTest.finish();
+ });
+ });
+ </script>
+</head>
+<body>
+ <h1 data-l10n-id="non-existing-id"></h1>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n_removeResourceIds.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_removeResourceIds.html
new file mode 100644
index 0000000000..8ccaa04614
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_removeResourceIds.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DocumentL10n::RemoveResourceIds</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="toolkit/about/aboutAddons.ftl"/>
+ <link rel="localization" href="toolkit/about/aboutSupport.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ /* eslint-disable mozilla/prefer-formatValues */
+
+ SimpleTest.waitForExplicitFinish();
+
+ window.onload = async function() {
+ {
+ // 1. An example message from aboutAddons should be available.
+ let value = await document.l10n.formatValue("shortcuts-browserAction2");
+ is(!!value.length, true, "localized value retrieved");
+ }
+
+ {
+ // 2. Remove aboutAddons.ftl
+ let link = document.head.querySelector("link[href*=aboutAddons]");
+ document.head.removeChild(link);
+ }
+
+ {
+ // 3. An example message from aboutSupport should still be available.
+ let value = await document.l10n.formatValue("features-version");
+ is(!!value.length, true, "localized value retrieved");
+
+ // 4. An example message from aboutAddons should not be available.
+ await document.l10n.formatValue("shortcuts-browserAction").then(
+ () => {
+ ok(false, "localization should not be available");
+ },
+ () => {
+ ok(true, "localization should not be available");
+ });
+ }
+
+ {
+ // 5. Remove aboutSupport.ftl
+ let link = document.head.querySelector("link[href*=aboutSupport]");
+ document.head.removeChild(link);
+
+ // 6. document.l10n should be null.
+ is(document.l10n, null, "document.l10n should be null");
+
+ SimpleTest.finish();
+ }
+ };
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html
new file mode 100644
index 0000000000..ea44d1afe1
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html data-l10n-sync>
+<head>
+ <meta charset="utf-8">
+ <title>Test DocumentL10n in HTML environment</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("DOMContentLoaded", async function() {
+ await document.l10n.ready;
+
+ // Test for initial localization applied.
+ let desc = document.getElementById("main-desc");
+ is(!!desc.textContent.length, true);
+
+ // Test for manual value formatting.
+ let msg = await document.l10n.formatValue("id-heading");
+ is(!!msg.length, true);
+
+ // Test for mutations applied.
+ let verifyL10n = () => {
+ if (label.textContent.length) {
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+
+ let label = document.getElementById("label1");
+ document.l10n.setAttributes(
+ label,
+ "date-crashed-heading",
+ {
+ name: "John",
+ }
+ );
+
+ // Test for l10n.getAttributes
+ let l10nArgs = document.l10n.getAttributes(label);
+ is(l10nArgs.id, "date-crashed-heading");
+ is(l10nArgs.args.name, "John");
+ }, { once: true});
+ </script>
+</head>
+<body>
+ <h1 id="main-desc" data-l10n-id="crash-reports-title"></h1>
+
+ <p id="label1"></p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/document_l10n/test_telemetry.html b/dom/l10n/tests/mochitest/document_l10n/test_telemetry.html
new file mode 100644
index 0000000000..b528ebc7ea
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_telemetry.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DocumentL10n Telemetry</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const { BrowserTestUtils } = ChromeUtils.import(
+ "resource://testing-common/BrowserTestUtils.jsm"
+ );
+
+ SimpleTest.waitForExplicitFinish();
+
+ function countValues(snapshot, key) {
+ if (!snapshot.hasOwnProperty(key)) {
+ return 0;
+ }
+ let values = snapshot[key].values;
+ return Object.values(values).reduce((sum, n) => sum + n, 0);
+ }
+
+ (async function() {
+ let histogram = Services.telemetry
+ .getKeyedHistogramById("L10N_DOCUMENT_INITIAL_TRANSLATION_TIME_US");
+ let snapshot = histogram.snapshot();
+
+ // In some cases the test runs before first window is localized.
+ // We just want to ensure that we didn't register more than 1
+ // first window telemetry.
+ let firstWindowCount = countValues(snapshot, "browser_first_window");
+ is(firstWindowCount < 2, true);
+
+ histogram.clear();
+
+ // Open a new window
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ waitForTabURL: "about:blank",
+ });
+
+ // Telemetry in testing is flaky and when landing this test
+ // we saw cases where the snapshot did not contain the new
+ // window telemetry at this moment.
+ //
+ // We're going to use `waitForCondition` to test for
+ // the telemetry to be eventually recorded.
+ BrowserTestUtils.waitForCondition(() => {
+ snapshot = histogram.snapshot();
+
+ // We want to make sure that since we cleared histogram
+ // just one new window of either type has been opened.
+ let browserWindowsCount =
+ countValues(snapshot, "browser_new_window") +
+ countValues(snapshot, "browser_first_window");
+ return browserWindowsCount == 1;
+ });
+
+ // Open preferences in a new tab
+ let tab = BrowserTestUtils.addTab(
+ win.gBrowser,
+ "about:preferences"
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Similarly to the above, we've seen cases where the telemetry
+ // was not present right after the tab was opened, so
+ // we'll use `waitForCondition` here.
+ BrowserTestUtils.waitForCondition(() => {
+ snapshot = histogram.snapshot();
+
+ return countValues(snapshot, "about:preferences") == 1;
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+
+ SimpleTest.finish();
+ })();
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/document_l10n/test_unpriv_iframe.html b/dom/l10n/tests/mochitest/document_l10n/test_unpriv_iframe.html
new file mode 100644
index 0000000000..4f4b29c500
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_unpriv_iframe.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure unprivilaged document cannot access document.l10n in an iframe</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function() {
+ let frame = document.getElementById("frame");
+ let frame2 = document.getElementById("frame2");
+
+ is("l10n" in frame.contentDocument, false);
+ is("l10n" in frame2.contentDocument, false);
+ });
+ addLoadEvent(SimpleTest.finish);
+ </script>
+</head>
+<body>
+ <iframe id="frame" src="about:blank"></iframe>
+ <iframe id="frame2" src="about:crashes"></iframe>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html b/dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html
new file mode 100644
index 0000000000..a9244c9891
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization's attr sanitization functionality</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+key1 = Value for Key 1
+
+key2 = Value for <a>Key 2<a/>.
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(async () => {
+ const domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ await domLoc.translateFragment(document.body);
+
+ const elem1 = document.querySelector("#elem1");
+ const elem2 = document.querySelector("#elem2");
+
+ ok(elem1.textContent.includes("Value for"));
+ ok(!elem1.hasAttribute("title"));
+
+ ok(elem2.textContent.includes("Value for"));
+ ok(!elem2.hasAttribute("title"));
+
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+ <p id="elem1" title="Old Translation" data-l10n-id="key1"></p>
+ <p id="elem2" title="Old Translation" data-l10n-id="key2"></p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html b/dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html
new file mode 100644
index 0000000000..83b83757f1
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization.prototype.connectRoot</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+key1 = Value for Key 1
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const p1 = document.getElementById("p1");
+
+ const domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ await domLoc.translateRoots();
+ is(!p1.textContent.length, true);
+ const body = document.body;
+ domLoc.connectRoot(body);
+ await domLoc.translateRoots();
+ is(!!p1.textContent.length, true);
+
+ SimpleTest.finish();
+ };
+ </script>
+</head>
+<body>
+ <p id="p1" data-l10n-id="key1"></p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html b/dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html
new file mode 100644
index 0000000000..1254bd814f
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization.prototype.connectRoot with Web Components</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ class FluentWidget extends HTMLElement {
+ constructor() {
+ super();
+ const shadowRoot = this.attachShadow({mode: "open"});
+ const t = document.querySelector("#fluent-widget-template");
+ const instance = t.content.cloneNode(true);
+ shadowRoot.appendChild(instance);
+ }
+ connectedCallback() {
+ document.domLoc.connectRoot(this.shadowRoot);
+ ok(true);
+
+ let label = this.shadowRoot.getElementById("label");
+
+ // Test for mutations applied.
+ let verifyL10n = () => {
+ if (label.textContent.length) {
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ // Notice: In normal tests we do not want to test against any particular
+ // value as per https://firefox-source-docs.mozilla.org/l10n/fluent/tutorial.html#testing
+ // But in this particular test, since we do not rely on actual real
+ // localization, but instead we mock it in the test, we can test
+ // against the actual value safely.
+ is(label.textContent, "Value for Key 1", "localization content applied to element");
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+
+ document.domLoc.setAttributes(label, "key1");
+ }
+ }
+ customElements.define("fluent-widget", FluentWidget);
+ </script>
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+key1 = Value for Key 1
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ document.domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+ </script>
+</head>
+<body>
+ <template id="fluent-widget-template">
+ <div>
+ <p id="label"></p>
+ </div>
+ </template>
+ <fluent-widget id="widget1"></fluent-widget>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html b/dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html
new file mode 100644
index 0000000000..fd7344c88e
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization.prototype.disconnectRoot</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+key1 = Value for Key 1
+key2 = Value for Key 2
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const p1 = document.getElementById("p1");
+
+ const domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ await domLoc.translateRoots();
+ is(!p1.textContent.length, true);
+
+ const body = document.body;
+
+ domLoc.connectRoot(body);
+ await domLoc.translateRoots();
+ is(p1.textContent.includes("Key 1"), true);
+ is(p1.textContent.includes("Key 2"), false);
+
+ domLoc.disconnectRoot(body);
+ domLoc.setAttributes(p1, "key2");
+ await domLoc.translateRoots();
+ is(p1.textContent.includes("Key 1"), true);
+ is(p1.textContent.includes("Key 2"), false);
+
+ domLoc.connectRoot(body);
+ await domLoc.translateRoots();
+ is(p1.textContent.includes("Key 1"), false);
+ is(p1.textContent.includes("Key 2"), true);
+
+ SimpleTest.finish();
+ };
+ </script>
+</head>
+<body>
+ <p id="p1" data-l10n-id="key1"></p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml b/dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml
new file mode 100644
index 0000000000..a4d4fcc506
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml
@@ -0,0 +1,68 @@
+<?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 xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Testing DOMLocalization in XUL environment">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript">
+ <![CDATA[
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+file-menu =
+ .label = File
+ .accesskey = F
+new-tab =
+ .label = New Tab
+ .accesskey = N
+container = Some text with an <image data-l10n-name="foo"> inside it.
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ SimpleTest.waitForExplicitFinish();
+
+
+ const domLoc = new DOMLocalization(
+ [],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ async function foo() {
+ domLoc.addResourceIds(["/mock.ftl"]);
+ domLoc.connectRoot(document.documentElement);
+ await domLoc.translateRoots();
+
+ is(document.getElementById('file-menu').getAttribute('label'), 'File');
+ is(document.getElementById('file-menu').getAttribute('accesskey'), 'F');
+
+ is(document.getElementById('new-tab').getAttribute('label'), 'New Tab');
+ is(document.getElementById('new-tab').getAttribute('accesskey'), 'N');
+
+ ok(document.querySelector("image"),
+ "Image should still be present after localization.");
+ SimpleTest.finish();
+ }
+
+ window.onload = foo;
+
+ ]]>
+ </script>
+ <description data-l10n-id="container"><image data-l10n-name="foo"/></description>
+
+ <menubar id="main-menubar">
+ <menu id="file-menu" data-l10n-id="file-menu">
+ <menupopup id="menu_FilePopup">
+ <menuitem id="new-tab" data-l10n-id="new-tab">
+ </menuitem>
+ </menupopup>
+ </menu>
+ </menubar>
+</window>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_getAttributes.html b/dom/l10n/tests/mochitest/dom_localization/test_getAttributes.html
new file mode 100644
index 0000000000..11996d132a
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_getAttributes.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization.prototype.getAttributes</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const domLoc = new DOMLocalization(
+ [],
+ false,
+ );
+
+ const p1 = document.querySelectorAll("p")[0];
+ const p2 = document.querySelectorAll("p")[1];
+ const p3 = document.querySelectorAll("p")[2];
+ const attrs1 = domLoc.getAttributes(p1);
+ const attrs2 = domLoc.getAttributes(p2);
+ const attrs3 = domLoc.getAttributes(p3);
+ isDeeply(attrs1, {
+ id: null,
+ args: null,
+ });
+ isDeeply(attrs2, {
+ id: "id1",
+ args: null,
+ });
+ isDeeply(attrs3, {
+ id: "id2",
+ args: {
+ userName: "John",
+ },
+ });
+
+ SimpleTest.finish();
+ };
+ </script>
+</head>
+<body>
+ <p />
+ <p data-l10n-id="id1" />
+ <p data-l10n-id="id2" data-l10n-args='{"userName": "John"}' />
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html b/dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html
new file mode 100644
index 0000000000..278baa15dd
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization's MutationObserver</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+title = Hello World
+title2 = Hello Another World
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const domLoc = new DOMLocalization(
+ [],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ const h1 = document.querySelectorAll("h1")[0];
+
+ domLoc.addResourceIds(["/mock.ftl"]);
+ domLoc.connectRoot(document.body);
+
+ await domLoc.translateRoots();
+
+ is(h1.textContent, "Hello World");
+
+
+ const mo = new MutationObserver(function onMutations(mutations) {
+ is(h1.textContent, "Hello Another World");
+ mo.disconnect();
+ SimpleTest.finish();
+ });
+
+ mo.observe(h1, { childList: true, characterData: true });
+
+ domLoc.setAttributes(h1, "title2");
+ };
+ </script>
+</head>
+<body>
+ <div>
+ <h1 data-l10n-id="title"></h1>
+ </div>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_overlay.html b/dom/l10n/tests/mochitest/dom_localization/test_overlay.html
new file mode 100644
index 0000000000..2c8c219bb2
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization's DOMOverlay functionality</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+title = <strong>Hello</strong> World
+title2 = This is <a data-l10n-name="link">a link</a>!
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ const p1 = document.querySelectorAll("p")[0];
+ const p2 = document.querySelectorAll("p")[1];
+ const a = p2.querySelector("a");
+ // We want to test that the event listener persists after
+ // translateFragment().
+ a.addEventListener("click", function(e) {
+ SimpleTest.finish();
+ // We cannot connect to non-local connections on automation, so prevent
+ // the navigation.
+ e.preventDefault();
+ });
+
+ await domLoc.translateFragment(document.body);
+
+
+ is(p1.querySelector("strong").textContent, "Hello");
+
+ is(p2.querySelector("a").getAttribute("href"), "http://www.mozilla.org");
+ is(p2.querySelector("a").textContent, "a link");
+
+ a.click();
+ };
+ </script>
+</head>
+<body>
+ <p data-l10n-id="title" />
+ <p data-l10n-id="title2">
+ <a href="http://www.mozilla.org" data-l10n-name="link"></a>
+ </p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html b/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html
new file mode 100644
index 0000000000..c6f285aa38
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization's DOMOverlay functionality</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(async () => {
+ const domLoc = new DOMLocalization(
+ [],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ await domLoc.translateFragment(document.body).then(() => {
+ ok(false, "Expected translateFragment to throw on missing l10n-id");
+ }, () => {
+ ok(true, "Expected translateFragment to throw on missing l10n-id");
+ });
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+ <p data-l10n-id="title">
+ <a href="http://www.mozilla.org"></a>
+ <a href="http://www.firefox.com"></a>
+ <a href="http://www.w3.org"></a>
+ </p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html b/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html
new file mode 100644
index 0000000000..9e4fec1ce2
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization's DOMOverlay functionality</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ await domLoc.translateFragment(document.body);
+
+ const p1 = document.querySelectorAll("p")[0];
+ const linkList = p1.querySelectorAll("a");
+
+
+ is(linkList[0].getAttribute("href"), "http://www.mozilla.org");
+ is(linkList[0].textContent, "Mozilla");
+ is(linkList[1].getAttribute("href"), "http://www.firefox.com");
+ is(linkList[1].textContent, "Firefox");
+
+ is(linkList.length, 2, "There should be exactly two links in the result.");
+
+ SimpleTest.finish();
+ };
+ </script>
+</head>
+<body>
+ <p data-l10n-id="title">
+ <a href="http://www.mozilla.org" data-l10n-name="mozilla-link"></a>
+ <a href="http://www.firefox.com" data-l10n-name="firefox-link"></a>
+ <a href="http://www.w3.org" data-l10n-name="w3-link"></a>
+ </p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html b/dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html
new file mode 100644
index 0000000000..a169c27fa0
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization's DOMOverlay functionality</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ await domLoc.translateFragment(document.body);
+
+ const p1 = document.querySelectorAll("p")[0];
+ const linkList = p1.querySelectorAll("a");
+
+
+ is(linkList[0].getAttribute("href"), "http://www.mozilla.org");
+ is(linkList[0].textContent, "Mozilla");
+ is(linkList[1].getAttribute("href"), "http://www.firefox.com");
+ is(linkList[1].textContent, "Firefox");
+
+ SimpleTest.finish();
+ };
+ </script>
+</head>
+<body>
+ <p data-l10n-id="title">
+ <a href="http://www.mozilla.org" data-l10n-name="mozilla-link"></a>
+ <a href="http://www.firefox.com" data-l10n-name="firefox-link"></a>
+ </p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html b/dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html
new file mode 100644
index 0000000000..fcc201edb6
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization's DOMOverlay functionality</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+key1 =
+ .href = https://www.hacked.com
+
+key2 =
+ .href = https://pl.wikipedia.org
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ async function test() {
+ const domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ await domLoc.translateFragment(document.body);
+
+ const key1Elem = document.querySelector("[data-l10n-id=key1]");
+ const key2Elem = document.querySelector("[data-l10n-id=key2]");
+
+
+ is(key1Elem.hasAttribute("href"), false, "href translation should not be allowed");
+ is(key2Elem.getAttribute("href"), "https://pl.wikipedia.org",
+ "href translation should be allowed");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(test);
+ </script>
+</head>
+<body>
+ <a data-l10n-id="key1"></a>
+ <a data-l10n-id="key2" data-l10n-attrs="href"></a>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html b/dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html
new file mode 100644
index 0000000000..64d585f0a0
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization's matching l10nIds functionality</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+key1 = Translation For Key 1
+
+key2 = Visit <a data-l10n-name="link">this link<a/>.
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(async () => {
+ const domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ await domLoc.translateFragment(document.body);
+
+ ok(document.querySelector("#elem1").textContent.includes("Key 1"));
+ ok(document.querySelector("#elem2").textContent.includes("Key 1"));
+
+ const elem3 = document.querySelector("#elem3");
+ const elem4 = document.querySelector("#elem4");
+
+ ok(elem3.textContent.includes("Visit"));
+ is(elem3.querySelector("a").getAttribute("href"), "http://www.mozilla.org");
+
+ ok(elem4.textContent.includes("Visit"));
+ is(elem4.querySelector("a").getAttribute("href"), "http://www.firefox.com");
+
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+ <h1 id="elem1" data-l10n-id="key1"></h1>
+ <h2 id="elem2" data-l10n-id="key1"></h2>
+
+ <p id="elem3" data-l10n-id="key2">
+ <a href="http://www.mozilla.org" data-l10n-name="link"></a>
+ </p>
+
+ <p id="elem4" data-l10n-id="key2">
+ <a href="http://www.firefox.com" data-l10n-name="link"></a>
+ </p>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_setAttributes.html b/dom/l10n/tests/mochitest/dom_localization/test_setAttributes.html
new file mode 100644
index 0000000000..508a15dcd3
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_setAttributes.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization.prototype.setAttributes and DOMLocalization.prototype.setArgs</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const domLoc = new DOMLocalization(
+ [],
+ false,
+ l10nReg,
+ );
+
+ const p1 = document.querySelectorAll("p")[0];
+
+ domLoc.setAttributes(p1, "title");
+ is(
+ p1.getAttribute("data-l10n-id"),
+ "title",
+ "The data-l10n-id can be set by setAttributes."
+ );
+ is(
+ p1.getAttribute("data-l10n-args"),
+ null,
+ "The data-l10n-args is unset."
+ );
+
+
+ domLoc.setAttributes(p1, "title2", {userName: "John"});
+ is(
+ p1.getAttribute("data-l10n-id"),
+ "title2",
+ "The data-l10n-id can be set by setAttributes."
+ );
+ is(
+ p1.getAttribute("data-l10n-args"),
+ JSON.stringify({userName: "John"}),
+ "The data-l10n-args can be set by setAttributes."
+ );
+
+ domLoc.setArgs(p1, {userName: "Jane"});
+ is(
+ p1.getAttribute("data-l10n-id"),
+ "title2",
+ "The data-l10n-id is unchanged by setArgs."
+ );
+ is(
+ p1.getAttribute("data-l10n-args"),
+ JSON.stringify({userName: "Jane"}),
+ "The data-l10n-args can by set by setArgs."
+ );
+
+ domLoc.setArgs(p1);
+ is(
+ p1.getAttribute("data-l10n-id"),
+ "title2",
+ "The data-l10n-id is unchanged by setArgs."
+ );
+ is(
+ p1.getAttribute("data-l10n-args"),
+ null,
+ "The data-l10n-args be unset by setArgs."
+ );
+
+ SimpleTest.finish();
+ };
+ </script>
+</head>
+<body>
+ <p />
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_translateElements.html b/dom/l10n/tests/mochitest/dom_localization/test_translateElements.html
new file mode 100644
index 0000000000..7893309ce4
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_translateElements.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization.prototype.translateElements</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+title = Hello World
+link =
+ .title = Click me
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ const p1 = document.querySelectorAll("p")[0];
+ const link1 = document.querySelectorAll("a")[0];
+
+ await domLoc.translateElements([p1, link1]);
+
+ is(p1.textContent, "Hello World");
+ is(link1.getAttribute("title"), "Click me");
+
+ SimpleTest.finish();
+ };
+ </script>
+</head>
+<body>
+ <p data-l10n-id="title" />
+ <a data-l10n-id="link" />
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html b/dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html
new file mode 100644
index 0000000000..c5ccfd996a
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization.prototype.translateFragment</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+title = Hello World
+subtitle = Welcome to FluentBundle
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ const frag = document.querySelectorAll("div")[0];
+ const h1 = document.querySelectorAll("h1")[0];
+ const p1 = document.querySelectorAll("p")[0];
+
+ await domLoc.translateFragment(frag);
+ is(h1.textContent, "Hello World");
+ is(p1.textContent, "Welcome to FluentBundle");
+
+ SimpleTest.finish();
+ };
+ </script>
+</head>
+<body>
+ <div>
+ <h1 data-l10n-id="title" />
+ <p data-l10n-id="subtitle" />
+ </div>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html b/dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html
new file mode 100644
index 0000000000..58bfb044b0
--- /dev/null
+++ b/dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization.prototype.translateRoots</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+title = Hello World
+title2 = Hello Another World
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ window.onload = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ const domLoc = new DOMLocalization(
+ [],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+
+ const frag1 = document.querySelectorAll("div")[0];
+ const frag2 = document.querySelectorAll("div")[1];
+ const h1 = document.querySelectorAll("h1")[0];
+ const h2 = document.querySelectorAll("h2")[0];
+
+ domLoc.addResourceIds(["/mock.ftl"]);
+ domLoc.connectRoot(frag1);
+ domLoc.connectRoot(frag2);
+
+ await domLoc.translateRoots();
+
+ is(h1.textContent, "Hello World");
+ is(h2.textContent, "Hello Another World");
+
+ SimpleTest.finish();
+ };
+ </script>
+</head>
+<body>
+ <div>
+ <h1 data-l10n-id="title"></h1>
+ </div>
+ <div>
+ <h2 data-l10n-id="title2"></h2>
+ </div>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_post_dcl.html b/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_post_dcl.html
new file mode 100644
index 0000000000..e3a9819728
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_post_dcl.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10n Mutations for ContentAppended after DOMContentLoaded</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("DOMContentLoaded", function() {
+ let elem = document.createElement("div");
+ document.l10n.setAttributes(elem, "crash-reports-title");
+ is(elem.textContent.length, 0);
+ let verifyL10n = () => {
+ if (elem.textContent.length) {
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+ document.body.appendChild(elem);
+ });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_pre_dcl.html b/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_pre_dcl.html
new file mode 100644
index 0000000000..825464e7b8
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_pre_dcl.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10n Mutations for ContentAppended before DOMContentLoaded</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+</head>
+<body>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ let elem = document.createElement("div");
+ document.l10n.setAttributes(elem, "crash-reports-title");
+ is(elem.textContent.length, 0);
+ let verifyL10n = () => {
+ if (elem.textContent.length) {
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+ document.body.appendChild(elem);
+ </script>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_append_fragment_post_dcl.html b/dom/l10n/tests/mochitest/l10n_mutations/test_append_fragment_post_dcl.html
new file mode 100644
index 0000000000..8efb3203c6
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_mutations/test_append_fragment_post_dcl.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10n Mutations for appending a fragment after DOMContentLoaded</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("DOMContentLoaded", function() {
+ let frag = document.createDocumentFragment();
+ let elem = document.createElement("div");
+ document.l10n.setAttributes(elem, "crash-reports-title");
+ frag.appendChild(elem);
+
+ let elem2 = document.createElement("div");
+ document.l10n.setAttributes(elem2, "crash-reports-title");
+ frag.appendChild(elem2);
+
+ is(elem.textContent.length, 0);
+ is(elem2.textContent.length, 0);
+
+ let verifyL10n = () => {
+ if (elem.textContent.length && elem2.textContent.length) {
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+ document.body.appendChild(frag);
+ });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_disconnectedRoot_webcomponent.html b/dom/l10n/tests/mochitest/l10n_mutations/test_disconnectedRoot_webcomponent.html
new file mode 100644
index 0000000000..bb9d9fc24d
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_mutations/test_disconnectedRoot_webcomponent.html
@@ -0,0 +1,148 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test DOMLocalization.prototype.connectRoot with Web Components</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/mock.ftl", source: `
+key1 = Key 1
+key2 = Key 2
+key3 = Key 3
+key4 = Key 4
+` },
+ ];
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ document.domLoc = new DOMLocalization(
+ ["/mock.ftl"],
+ false,
+ l10nReg,
+ ["en-US"],
+ );
+ document.domLoc.connectRoot(document.documentElement);
+ </script>
+ <script type="application/javascript">
+ // In this test we're going to use two elements - `shadowLabel` and `lightLabel`.
+ // We create a new `DOMLocalization` and connect it to the document's root first.
+ //
+ // Then, we connect and disconnect it on root and element within the shadow DOM and
+ // apply new `data-l10n-id` onto both labels.
+ // Once the `lightLabel` get a new translation, we check what happened to the `shadowLabel`
+ // to ensure that depending on the status of connection between the shadow DOM and the `DOMLocalization`
+ // the `shadowLabel` either gets translated or not.
+
+ SimpleTest.waitForExplicitFinish();
+
+ class FluentWidget extends HTMLElement {
+ constructor() {
+ super();
+ const shadowRoot = this.attachShadow({mode: "open"});
+ const t = document.querySelector("#fluent-widget-template");
+ const instance = t.content.cloneNode(true);
+ shadowRoot.appendChild(instance);
+ }
+ connectedCallback() {
+ this.runTests();
+ }
+ runTests() {
+ // First, let's verify that the mutation will not be applied since
+ // the shadow DOM is not connected to the `DOMLocalization`.
+ let shadowLabel = this.shadowRoot.getElementById("shadowLabel");
+ let lightLabel = document.getElementById("lightLabel");
+
+ let verifyL10n = () => {
+ if (lightLabel.textContent == "Key 1") {
+ is(shadowLabel.textContent, "", "document.l10n content not applied to an element in the shadow DOM");
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ this.testPart2();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+
+ document.domLoc.setAttributes(shadowLabel, "key1");
+ document.domLoc.setAttributes(lightLabel, "key1");
+ }
+ testPart2() {
+ // Next, we connect the shadow root to DOMLocalization and the next attribute
+ // change should result in a translation being applied.
+ document.domLoc.connectRoot(this.shadowRoot);
+
+ let shadowLabel = this.shadowRoot.getElementById("shadowLabel");
+ let lightLabel = document.getElementById("lightLabel");
+
+ // Test that mutation was applied.
+ let verifyL10n = () => {
+ if (lightLabel.textContent == "Key 2") {
+ is(shadowLabel.textContent, "Key 2", "document.l10n content applied to an element in the shadow DOM");
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ this.testPart3();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+
+ document.domLoc.setAttributes(shadowLabel, "key2");
+ document.domLoc.setAttributes(lightLabel, "key2");
+ }
+ testPart3() {
+ // After we disconnect the shadow root, the mutations should
+ // not be applied onto the `shadowLabel`.
+ document.domLoc.disconnectRoot(this.shadowRoot);
+
+ let shadowLabel = this.shadowRoot.getElementById("shadowLabel");
+ let lightLabel = document.getElementById("lightLabel");
+
+ let verifyL10n = () => {
+ if (lightLabel.textContent == "Key 3") {
+ is(shadowLabel.textContent, "Key 2", "document.l10n content not applied to an element in the shadow DOM");
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ this.testPart4();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+
+ document.domLoc.setAttributes(shadowLabel, "key3");
+ document.domLoc.setAttributes(lightLabel, "key3");
+ }
+ testPart4() {
+ // Finally, we'll connect it back, but this time, we'll connect
+ // not the shadow root, but an element within it.
+ // This should still result in the `shadowLabel` receiving a new translation.
+ document.domLoc.connectRoot(this.shadowRoot.getElementById("shadowDiv"));
+
+ let shadowLabel = this.shadowRoot.getElementById("shadowLabel");
+ let lightLabel = document.getElementById("lightLabel");
+
+ // Test that mutation was applied.
+ let verifyL10n = () => {
+ if (lightLabel.textContent == "Key 4") {
+ is(shadowLabel.textContent, "Key 4", "document.l10n content applied to an element in the shadow DOM");
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+
+ document.domLoc.setAttributes(shadowLabel, "key4");
+ document.domLoc.setAttributes(lightLabel, "key4");
+ }
+ }
+ customElements.define("fluent-widget", FluentWidget);
+ </script>
+</head>
+<body>
+ <p id="lightLabel"></p>
+
+ <template id="fluent-widget-template">
+ <div id="shadowDiv">
+ <p id="shadowLabel"></p>
+ </div>
+ </template>
+ <fluent-widget id="widget1"></fluent-widget>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_pause_observing.html b/dom/l10n/tests/mochitest/l10n_mutations/test_pause_observing.html
new file mode 100644
index 0000000000..d225153418
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_mutations/test_pause_observing.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10n Mutations for Pause/Resume Observing</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("DOMContentLoaded", function() {
+ let elem1 = document.createElement("div");
+ let elem2 = document.createElement("div");
+ let elem3 = document.createElement("div");
+ is(elem1.textContent.length, 0);
+ is(elem2.textContent.length, 0);
+ is(elem3.textContent.length, 0);
+
+ document.l10n.setAttributes(elem1, "crash-reports-title");
+ document.l10n.setAttributes(elem2, "crash-reports-title");
+ document.l10n.setAttributes(elem3, "crash-reports-title");
+
+ let verifyL10n = () => {
+ if (elem1.textContent.length &&
+ !elem2.textContent.length &&
+ elem3.textContent.length) {
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+ document.body.appendChild(elem1);
+ document.l10n.pauseObserving();
+ document.body.appendChild(elem2);
+ document.l10n.resumeObserving();
+ document.body.appendChild(elem3);
+ });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_remove_element.html b/dom/l10n/tests/mochitest/l10n_mutations/test_remove_element.html
new file mode 100644
index 0000000000..347e858d52
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_mutations/test_remove_element.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10n Mutations for removing element</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("DOMContentLoaded", async function() {
+ // This element will be added to DOM and expected to be localized.
+ let elem = document.createElement("div");
+
+ // This element will be added to DOM, then immediatelly removed and
+ // expected *not* to be localized.
+ let elem2 = document.createElement("div");
+
+ // This element will be added to DOM, then immediatelly removed and
+ // and then immediatelly re-added, and expected to be localized.
+ let elem3 = document.createElement("div");
+
+ // This element will be added to DOM, then immediatelly removed and
+ // and then re-added later, and expected to be localized.
+ let elem4 = document.createElement("div");
+
+ document.l10n.setAttributes(elem, "crash-reports-title");
+ document.l10n.setAttributes(elem2, "crash-reports-title");
+ document.l10n.setAttributes(elem3, "crash-reports-title");
+ document.l10n.setAttributes(elem4, "crash-reports-title");
+
+ document.body.appendChild(elem);
+ document.body.appendChild(elem2);
+ document.body.appendChild(elem3);
+ document.body.appendChild(elem4);
+
+ is(elem.textContent.length, 0);
+ is(elem2.textContent.length, 0);
+ is(elem3.textContent.length, 0);
+ is(elem4.textContent.length, 0);
+
+ document.body.removeChild(elem2);
+ document.body.removeChild(elem3);
+ document.body.removeChild(elem4);
+
+ document.body.appendChild(elem3);
+
+ // 1. `elem` should be localized since it is in DOM.
+ await SimpleTest.waitForCondition(() => elem.textContent.length);
+ // 2. `elem2` was removed before l10n frame, so it should remain not localized.
+ is(elem2.textContent.length, 0);
+ // 3. `elem3` was added/removed/re-added so it should become localized.
+ await SimpleTest.waitForCondition(() => elem3.textContent.length);
+ // 4. `elem4` was not re-added, so it shouldn't be localized.
+ is(elem4.textContent.length, 0);
+
+ document.body.appendChild(elem4);
+ // 5. Now we re-added `elem4` to DOM so it should get localized.
+ await SimpleTest.waitForCondition(() => elem4.textContent.length);
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_remove_fragment.html b/dom/l10n/tests/mochitest/l10n_mutations/test_remove_fragment.html
new file mode 100644
index 0000000000..4f4d0fe1d8
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_mutations/test_remove_fragment.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10n Mutations for removing fragment</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("DOMContentLoaded", async function() {
+ let div = document.createElement("div");
+ let div2 = document.createElement("div");
+
+ let elem = document.createElement("p");
+ let elem2 = document.createElement("p");
+ let elem3 = document.createElement("p");
+ let elem4 = document.createElement("p");
+
+ document.l10n.setAttributes(elem, "crash-reports-title");
+ document.l10n.setAttributes(elem2, "crash-reports-title");
+ document.l10n.setAttributes(elem3, "crash-reports-title");
+ document.l10n.setAttributes(elem4, "crash-reports-title");
+
+ div.appendChild(elem);
+ div.appendChild(elem2);
+ div.appendChild(elem3);
+ div.appendChild(elem4);
+
+ document.body.appendChild(div);
+
+ is(elem.textContent.length, 0);
+ is(elem2.textContent.length, 0);
+ is(elem3.textContent.length, 0);
+ is(elem4.textContent.length, 0);
+
+ document.body.removeChild(div);
+
+ div2.appendChild(elem);
+ div2.appendChild(elem3);
+
+ document.body.appendChild(div2);
+
+ // 1. `elem` should be localized since it is in DOM.
+ await SimpleTest.waitForCondition(() => !!elem.textContent.length);
+
+ // 2. `elem2` was removed before l10n frame, so it should remain not localized.
+ is(elem2.textContent.length, 0);
+
+ // 3. `elem3` was added/removed/re-added so it should become localized.
+ await SimpleTest.waitForCondition(() => !!elem3.textContent.length);
+
+ // 4. `elem4` was not re-added, so it shouldn't be localized.
+ is(elem4.textContent.length, 0);
+
+ document.body.appendChild(div);
+ // 5. Now we re-added `elem4` to DOM so it should get localized.
+ await SimpleTest.waitForCondition(() => !!elem4.textContent.length);
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_set_attributes.html b/dom/l10n/tests/mochitest/l10n_mutations/test_set_attributes.html
new file mode 100644
index 0000000000..762353a06c
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_mutations/test_set_attributes.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10n Mutations for AttributeChange after DOMContentLoaded</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+ <link rel="localization" href="toolkit/about/aboutCompat.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("DOMContentLoaded", async function() {
+ await document.l10n.ready;
+ let elem = document.getElementById("elem1");
+ let elem2 = document.getElementById("elem2");
+ is(elem.textContent.length, 0);
+ is(elem2.textContent.includes("Initial string"), true);
+ document.l10n.setAttributes(elem, "crash-reports-title");
+ elem2.setAttribute("data-l10n-args", JSON.stringify({bug: "New string"}));
+
+ let verifyL10n = () => {
+ if (elem.textContent.length && elem2.textContent.includes("New string")) {
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+ });
+ </script>
+</head>
+<body>
+ <div id="elem1"></div>
+ <div id="elem2" data-l10n-id="label-more-information" data-l10n-args='{"bug":"Initial string"}'></div>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_template.html b/dom/l10n/tests/mochitest/l10n_mutations/test_template.html
new file mode 100644
index 0000000000..d02ecfcd4d
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_mutations/test_template.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10n Mutations in Template elements</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("DOMContentLoaded", async () => {
+ await document.l10n.ready;
+ let template = document.getElementById("template");
+ let clone = document.importNode(template.content, true);
+ let span = clone.querySelector("span");
+ is(span.textContent.length, 0,
+ "Element has not been translated while in template");
+ document.body.appendChild(clone);
+
+ let verifyL10n = () => {
+ if (span.textContent.length) {
+ window.removeEventListener("MozAfterPaint", verifyL10n);
+ SimpleTest.finish();
+ }
+ };
+ window.addEventListener("MozAfterPaint", verifyL10n);
+ });
+ </script>
+</head>
+<body>
+ <template id="template">
+ <span data-l10n-id="crash-reports-title"></span>
+ </template>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_attributes.html b/dom/l10n/tests/mochitest/l10n_overlays/test_attributes.html
new file mode 100644
index 0000000000..3d1f6048b2
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_overlays/test_attributes.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10nOverlays Top-level attributes</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ /* global L10nOverlays */
+ "use strict";
+
+ function elem(name) {
+ return function(str) {
+ const element = document.createElement(name);
+ // eslint-disable-next-line no-unsanitized/property
+ element.innerHTML = str;
+ return element;
+ };
+ }
+
+ const { translateElement } = L10nOverlays;
+
+ {
+ // Allowed attribute
+ const element = elem("div")``;
+ const translation = {
+ value: null,
+ attributes: [
+ {name: "title", value: "FOO"},
+ ],
+ };
+ translateElement(element, translation);
+ is(element.outerHTML, '<div title="FOO"></div>');
+ }
+
+ {
+ // Forbidden attribute
+ const element = elem("input")``;
+ const translation = {
+ value: null,
+ attributes: [
+ {name: "disabled", value: "DISABLED"},
+ ],
+ };
+ translateElement(element, translation);
+ is(element.outerHTML, "<input>");
+ }
+
+ {
+ // Attributes do not leak on first translation
+ const element = elem("div")`Foo`;
+ element.setAttribute("title", "Title");
+
+ const translation = {
+ value: "FOO",
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.outerHTML, "<div>FOO</div>");
+ }
+
+ {
+ // Attributes do not leak on retranslation
+ const element = elem("div")`Foo`;
+
+ const translationA = {
+ value: "FOO A",
+ attributes: [
+ {name: "title", value: "TITLE A"},
+ ],
+ };
+
+ const translationB = {
+ value: "FOO B",
+ attributes: null,
+ };
+ translateElement(element, translationA);
+ is(element.outerHTML, '<div title="TITLE A">FOO A</div>');
+ translateElement(element, translationB);
+ is(element.outerHTML, "<div>FOO B</div>");
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_extra_text_markup.html b/dom/l10n/tests/mochitest/l10n_overlays/test_extra_text_markup.html
new file mode 100644
index 0000000000..5f60599dfc
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_overlays/test_extra_text_markup.html
@@ -0,0 +1,136 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10nOverlays Localized text markup</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ /* global L10nOverlays */
+ "use strict";
+
+ function elem(name) {
+ return function(str) {
+ const element = document.createElement(name);
+ // eslint-disable-next-line no-unsanitized/property
+ element.innerHTML = str;
+ return element;
+ };
+ }
+
+ const { translateElement } = L10nOverlays;
+
+ // Localized text markup
+ {
+ // allowed element
+ const element = elem("div")`Foo`;
+ const translation = {
+ value: "FOO <em>BAR</em> BAZ",
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML, "FOO <em>BAR</em> BAZ");
+ }
+
+ {
+ // forbidden element
+ const element = elem("div")`Foo`;
+ const translation = {
+ value: 'FOO <img src="img.png" />',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML, "FOO ");
+ }
+
+ {
+ // forbiden element with text
+ const element = elem("div")`Foo`;
+ const translation = {
+ value: "FOO <a>a</a>",
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML, "FOO a");
+ }
+
+ {
+ // nested HTML is forbidden
+ const element = elem("div")`Foo`;
+ const translation = {
+ value: "FOO <em><strong>BAR</strong></em> BAZ",
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML, "FOO <em>BAR</em> BAZ");
+ }
+
+ // Attributes of localized text markup
+ {
+ // allowed attribute
+ const element = elem("div")`Foo Bar`;
+ const translation = {
+ value: 'FOO <em title="BAR">BAR</em>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ 'FOO <em title="BAR">BAR</em>');
+ }
+
+ {
+ // forbidden attribute
+ const element = elem("div")`Foo Bar`;
+ const translation = {
+ value: 'FOO <em class="BAR" title="BAR">BAR</em>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ 'FOO <em title="BAR">BAR</em>');
+ }
+
+ {
+ // attributes do not leak on first translation
+ const element = elem("div")`
+ <em title="Foo">Foo</a>`;
+ const translation = {
+ value: "<em>FOO</em>",
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ "<em>FOO</em>");
+ }
+
+ {
+ // attributes do not leak on retranslation
+ const element = elem("div")``;
+ const translationA = {
+ value: '<em title="FOO A">FOO A</em>',
+ attributes: null,
+ };
+ const translationB = {
+ value: "<em>FOO B</em>",
+ attributes: null,
+ };
+
+ translateElement(element, translationA);
+ is(element.innerHTML,
+ '<em title="FOO A">FOO A</em>');
+ translateElement(element, translationB);
+ is(element.innerHTML,
+ "<em>FOO B</em>");
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_functional_children.html b/dom/l10n/tests/mochitest/l10n_overlays/test_functional_children.html
new file mode 100644
index 0000000000..dba5b6e633
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_overlays/test_functional_children.html
@@ -0,0 +1,344 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10nOverlays functional children test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ /* global L10nOverlays */
+ "use strict";
+
+ function elem(name) {
+ return function(str) {
+ const element = document.createElement(name);
+ // eslint-disable-next-line no-unsanitized/property
+ element.innerHTML = str;
+ return element;
+ };
+ }
+
+ const { translateElement } = L10nOverlays;
+
+ // Child without name
+ {
+ // in source
+ const element = elem("div")`
+ <a>Foo</a>`;
+ const translation = {
+ value: "FOO",
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, "FOO");
+ }
+
+ {
+ // in translation
+ const element = elem("div")`Foo`;
+ const translation = {
+ value: "<a>FOO</a>",
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, "FOO");
+ }
+
+ {
+ // in both
+ const element = elem("div")`
+ <a>Foo</a>`;
+ const translation = {
+ value: "<a>FOO</a>",
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, "FOO");
+ }
+
+ // Child with name
+ {
+ // in source
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>`;
+ const translation = {
+ value: "<a>FOO</a>",
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, "FOO");
+ }
+
+ {
+ // in translation
+ const element = elem("div")`
+ <a>Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo">FOO</a>',
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, "FOO");
+ }
+
+ {
+ // in both
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo">FOO</a>',
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, '<a data-l10n-name="foo">FOO</a>');
+ }
+
+ {
+ // translation without text content
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo"></a>',
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, '<a data-l10n-name="foo"></a>');
+ }
+
+ {
+ // different names
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="bar">BAR</a>',
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, "BAR");
+ }
+
+ {
+ // of different types
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>`;
+ const translation = {
+ value: '<div data-l10n-name="foo">FOO</div>',
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, "FOO");
+ }
+
+ {
+ // used twice
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo">FOO 1</a> <a data-l10n-name="foo">FOO 2</a>',
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, '<a data-l10n-name="foo">FOO 1</a> FOO 2');
+ }
+
+ // Two named children
+ {
+ // in order
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>
+ <a data-l10n-name="bar">Bar</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo">FOO</a><a data-l10n-name="bar">BAR</a>',
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, '<a data-l10n-name="foo">FOO</a><a data-l10n-name="bar">BAR</a>');
+ }
+
+ {
+ // out of order
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>
+ <a data-l10n-name="bar">Bar</a>`;
+ const translation = {
+ value: '<a data-l10n-name="bar">BAR</a><a data-l10n-name="foo">FOO</a>',
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, '<a data-l10n-name="bar">BAR</a><a data-l10n-name="foo">FOO</a>');
+ }
+
+ {
+ // nested in source
+ const element = elem("div")`
+ <a data-l10n-name="foo">
+ Foo 1
+ <a data-l10n-name="bar">Bar</a>
+ Foo 2
+ </a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo">FOO</a><a data-l10n-name="bar">BAR</a>',
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(
+ element.innerHTML,
+ '<a data-l10n-name="foo">FOO</a><a data-l10n-name="bar">BAR</a>'
+ );
+ }
+
+ {
+ // nested in translation
+ const element = elem("div")`
+ <div data-l10n-name="foo">Foo</div>
+ <div data-l10n-name="bar">Bar</div>`;
+ const translation = {
+ value: '<div data-l10n-name="foo">FOO 1 <div data-l10n-name="bar">BAR</div> FOO 2</div>',
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(
+ element.innerHTML,
+ '<div data-l10n-name="foo">FOO 1 BAR FOO 2</div>'
+ );
+ }
+
+ // Child attributes
+ {
+ // functional attribute in source
+ const element = elem("div")`
+ <a data-l10n-name="foo" class="foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo">FOO</a>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ '<a data-l10n-name="foo" class="foo">FOO</a>');
+ }
+
+ {
+ // functional attribute in translation
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo" class="bar">FOO</a>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ '<a data-l10n-name="foo">FOO</a>');
+ }
+
+ {
+ // functional attribute in both
+ const element = elem("div")`
+ <a data-l10n-name="foo" class="foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo" class="bar">FOO</a>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ '<a data-l10n-name="foo" class="foo">FOO</a>');
+ }
+
+ {
+ // localizable attribute in source
+ const element = elem("div")`
+ <a data-l10n-name="foo" title="Foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo">FOO</a>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ '<a data-l10n-name="foo">FOO</a>');
+ }
+
+ {
+ // localizable attribute in translation
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo" title="FOO">FOO</a>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ '<a data-l10n-name="foo" title="FOO">FOO</a>');
+ }
+
+ {
+ // localizable attribute in both
+ const element = elem("div")`
+ <a data-l10n-name="foo" title="Foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo" title="BAR">FOO</a>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ '<a data-l10n-name="foo" title="BAR">FOO</a>');
+ }
+
+ {
+ // localizable attribute does not leak on retranslation
+ const element = elem("div")`
+ <a data-l10n-name="foo">Foo</a>`;
+ const translationA = {
+ value: '<a data-l10n-name="foo" title="FOO A">FOO A</a>',
+ attributes: null,
+ };
+ const translationB = {
+ value: '<a data-l10n-name="foo">FOO B</a>',
+ attributes: null,
+ };
+
+ translateElement(element, translationA);
+ is(element.innerHTML,
+ '<a data-l10n-name="foo" title="FOO A">FOO A</a>');
+ translateElement(element, translationB);
+ is(element.innerHTML,
+ '<a data-l10n-name="foo">FOO B</a>');
+ }
+
+ // Child attributes overrides
+ {
+ // the source can override child's attributes
+ const element = elem("div")`
+ <a data-l10n-name="foo" data-l10n-attrs="class" class="foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo" class="FOO">FOO</a>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ '<a data-l10n-name="foo" data-l10n-attrs="class" class="FOO">FOO</a>');
+ }
+
+ {
+ // the translation cannot override child's attributes
+ const element = elem("div")`
+ <a data-l10n-name="foo" class="foo">Foo</a>`;
+ const translation = {
+ value: '<a data-l10n-name="foo" data-l10n-attrs="class" class="FOO">FOO</a>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(element.innerHTML,
+ '<a data-l10n-name="foo" class="foo">FOO</a>');
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_l10n_overlays.xhtml b/dom/l10n/tests/mochitest/l10n_overlays/test_l10n_overlays.xhtml
new file mode 100644
index 0000000000..494958c573
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_overlays/test_l10n_overlays.xhtml
@@ -0,0 +1,87 @@
+<?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 xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Testing DocumentL10n in XUL environment">
+
+ <linkset>
+ <html:link rel="localization" href="toolkit/about/aboutAddons.ftl"/>
+ </linkset>
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript">
+ <![CDATA[
+ /* global L10nOverlays */
+
+ function elem(name) {
+ return function(str) {
+ const element = document.createXULElement(name);
+ // eslint-disable-next-line no-unsanitized/property
+ element.innerHTML = str;
+ return element;
+ };
+ }
+
+ const { translateElement } = L10nOverlays;
+
+ SimpleTest.waitForExplicitFinish();
+
+ {
+ // Allowed attribute
+ const element = elem("description")``;
+ const translation = {
+ value: null,
+ attributes: [
+ {name: "title", value: "FOO"},
+ ],
+ };
+ translateElement(element, translation);
+ is(element.outerHTML, '<description xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" title="FOO"/>');
+ }
+
+ document.addEventListener("DOMContentLoaded", () => {
+ {
+ // Handle HTML translation
+ const element = document.getElementById("test2");
+ const translation = {
+ value: "This is <a data-l10n-name=\"link\">a link</a>.",
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, 'This is <html:a xmlns:html="http://www.w3.org/1999/xhtml" data-l10n-name=\"link\" href="https://www.mozilla.org\">a link</html:a>.');
+ }
+
+ {
+ // Don't handle XUL translation
+ //
+ // Current iteration of L10nOverlays will replace
+ // XUL elements from translation with text.
+ //
+ // See bug 1545704 for details.
+ const element = document.getElementById("test3");
+ const translation = {
+ value: "This is <description data-l10n-name=\"desc\">a desc</description>.",
+ attributes: null,
+ };
+ translateElement(element, translation);
+ is(element.innerHTML, 'This is a desc.');
+ }
+ SimpleTest.finish();
+ }, {once: true});
+
+ ]]>
+ </script>
+
+ <description id="test2">
+ <html:a data-l10n-name="link" href="https://www.mozilla.org"/>
+ </description>
+
+ <box id="test3">
+ <description data-l10n-name="desc"/>
+ </box>
+</window>
diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html
new file mode 100644
index 0000000000..ecaefbd68d
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Amount of mutations generated from DOM Overlays</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="toolkit/about/aboutTelemetry.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ let config = {
+ attributes: true,
+ attributeOldValue: true,
+ characterData: true,
+ characterDataOldValue: true,
+ childList: true,
+ subtree: true,
+ };
+ let allMutations = [];
+
+ document.addEventListener("DOMContentLoaded", async function() {
+ await document.l10n.ready;
+
+ let inputElem = document.getElementById("search-input");
+
+ // Test for initial localization applied.
+ is(!!inputElem.getAttribute("placeholder").length, true);
+
+ let observer = new MutationObserver((mutations) => {
+ for (let mutation of mutations) {
+ allMutations.push(mutation);
+ }
+ });
+ observer.observe(inputElem, config);
+
+ document.l10n.setAttributes(inputElem, "about-telemetry-filter-all-placeholder");
+
+ // Due to the async iteractions between nsINode.localize
+ // and DOMLocalization, we'll need to wait two frames
+ // to verify that no mutations happened.
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ // Since the l10n-id is the same as the previous one
+ // no mutation should happen in result.
+ is(allMutations.length, 0);
+ SimpleTest.finish();
+ });
+ });
+ }, { once: true});
+ </script>
+</head>
+<body>
+ <input id="search-input" data-l10n-id="about-telemetry-filter-all-placeholder"></input>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html
new file mode 100644
index 0000000000..e43c394970
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Amount of mutations generated from DOM Overlays</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="localization" href="toolkit/about/aboutTelemetry.ftl"/>
+ <script type="application/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+
+ let config = {
+ attributes: true,
+ attributeOldValue: true,
+ characterData: true,
+ characterDataOldValue: true,
+ childList: true,
+ subtree: true,
+ };
+ let allMutations = [];
+
+ document.addEventListener("DOMContentLoaded", async function() {
+ await document.l10n.ready;
+
+ let inputElem = document.getElementById("search-input");
+
+ // Test for initial localization applied.
+ is(!!inputElem.getAttribute("placeholder").length, true);
+
+ let observer = new MutationObserver((mutations) => {
+ for (let mutation of mutations) {
+ allMutations.push(mutation);
+ }
+ });
+ observer.observe(inputElem, config);
+
+ document.l10n.setAttributes(inputElem, "about-telemetry-filter-placeholder", {selectedTitle: "Test"});
+
+ // Due to the async iteractions between nsINode.localize
+ // and DOMLocalization, we'll need to wait two frames
+ // to verify that no mutations happened.
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ // Since the l10n-id is the same as the previous one
+ // no mutation should happen in result.
+ is(allMutations.length, 0);
+ SimpleTest.finish();
+ });
+ });
+ }, { once: true});
+ </script>
+</head>
+<body>
+ <input id="search-input" data-l10n-id="about-telemetry-filter-placeholder" data-l10n-args='{"selectedTitle":"Test"}'></input>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_text_children.html b/dom/l10n/tests/mochitest/l10n_overlays/test_text_children.html
new file mode 100644
index 0000000000..1c2fab7ade
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_overlays/test_text_children.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10nOverlays Text-semantic argument elements</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ /* global L10nOverlays */
+ "use strict";
+
+ function elem(name) {
+ return function(str) {
+ const element = document.createElement(name);
+ // eslint-disable-next-line no-unsanitized/property
+ element.innerHTML = str;
+ return element;
+ };
+ }
+
+ const { translateElement } = L10nOverlays;
+
+ {
+ // without data-l10n-name
+ const element = elem("div")`
+ <em class="bar"></em>`;
+ const translation = {
+ value: '<em title="FOO">FOO</em>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(
+ element.innerHTML,
+ '<em title="FOO">FOO</em>'
+ );
+ }
+
+ {
+ // mismatched types
+ const element = elem("div")`
+ <button data-l10n-name="foo"></button>`;
+ const translation = {
+ value: '<em data-l10n-name="foo" title="FOO">FOO</em>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(
+ element.innerHTML,
+ "FOO"
+ );
+ }
+
+ {
+ // types and names mismatch
+ const element = elem("div")`
+ <em data-l10n-name="foo" class="foo"></em>`;
+ const translation = {
+ value: '<em data-l10n-name="foo" title="FOO">FOO</em>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(
+ element.innerHTML,
+ '<em data-l10n-name="foo" class="foo" title="FOO">FOO</em>'
+ );
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_title.html b/dom/l10n/tests/mochitest/l10n_overlays/test_title.html
new file mode 100644
index 0000000000..4571589b8e
--- /dev/null
+++ b/dom/l10n/tests/mochitest/l10n_overlays/test_title.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test L10nOverlays Special treatment of the title element</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ /* global L10nOverlays */
+ "use strict";
+
+ function elem(name) {
+ return function(str) {
+ const element = document.createElement(name);
+ // eslint-disable-next-line no-unsanitized/property
+ element.innerHTML = str;
+ return element;
+ };
+ }
+
+ const { translateElement } = L10nOverlays;
+
+ {
+ // Text is fine.
+ const element = elem("title")``;
+ const translation = {
+ value: 'Text',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(
+ element.innerHTML,
+ 'Text'
+ );
+ }
+
+ {
+ // Markup is ignored.
+ const element = elem("title")``;
+ const translation = {
+ value: '<em>Markup</em>',
+ attributes: null,
+ };
+
+ translateElement(element, translation);
+ is(
+ element.textContent,
+ '<em>Markup</em>'
+ );
+ is(
+ element.innerHTML,
+ '&lt;em&gt;Markup&lt;/em&gt;'
+ );
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/l10n/tests/mochitest/mochitest.ini b/dom/l10n/tests/mochitest/mochitest.ini
new file mode 100644
index 0000000000..82af57c47c
--- /dev/null
+++ b/dom/l10n/tests/mochitest/mochitest.ini
@@ -0,0 +1 @@
+[document_l10n/test_unpriv_iframe.html]