Bug 1501411 - Make sure that copying preference values from the new "about:config" page preserves whitespace. r=bgrins
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Tue, 15 Jan 2019 14:21:07 +0000
changeset 514020 062ab37367fb624f974b115a5153661a87983e60
parent 514019 cd0401ea98ff98343f1f207fd74071de60ce4622
child 514021 963484091d53cc5b77ce0530cea042270c184a85
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1501411
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1501411 - Make sure that copying preference values from the new "about:config" page preserves whitespace. r=bgrins Differential Revision: https://phabricator.services.mozilla.com/D16056
browser/components/aboutconfig/content/aboutconfig.css
browser/components/aboutconfig/content/aboutconfig.js
browser/components/aboutconfig/test/browser/browser.ini
browser/components/aboutconfig/test/browser/browser_clipboard.js
browser/components/aboutconfig/test/browser/head.js
testing/mochitest/tests/SimpleTest/SimpleTest.js
--- a/browser/components/aboutconfig/content/aboutconfig.css
+++ b/browser/components/aboutconfig/content/aboutconfig.css
@@ -64,16 +64,17 @@
 }
 
 #prefs > tr.deleted > td.cell-name {
   font-weight: bold;
   opacity: 0.4;
 }
 
 .cell-value {
+  white-space: pre-wrap;
   word-break: break-all;
 }
 
 td.cell-value > form > input[type="text"],
 td.cell-value > form > input[type="number"] {
   -moz-appearance: textfield;
   width: 100%;
   box-sizing: border-box;
--- a/browser/components/aboutconfig/content/aboutconfig.js
+++ b/browser/components/aboutconfig/content/aboutconfig.js
@@ -112,17 +112,22 @@ class PrefRow {
     this.refreshElement();
   }
 
   refreshElement() {
     this.element.classList.toggle("has-user-value", !!this.hasUserValue);
     this.element.classList.toggle("locked", !!this.isLocked);
     this.element.classList.toggle("deleted", !this.exists);
     if (this.exists && !this.editing) {
-      this.valueCell.textContent = this.value;
+      // We need to place the text inside a "span" element to ensure that the
+      // text copied to the clipboard includes all whitespace.
+      let span = document.createElement("span");
+      span.textContent = this.value;
+      this.valueCell.textContent = "";
+      this.valueCell.append(span);
       if (this.type == "Boolean") {
         document.l10n.setAttributes(this.editButton, "about-config-pref-toggle");
         this.editButton.className = "button-toggle";
       } else {
         document.l10n.setAttributes(this.editButton, "about-config-pref-edit");
         this.editButton.className = "button-edit";
       }
       this.editButton.removeAttribute("form");
--- a/browser/components/aboutconfig/test/browser/browser.ini
+++ b/browser/components/aboutconfig/test/browser/browser.ini
@@ -1,13 +1,16 @@
 [DEFAULT]
 support-files =
   head.js
 
 [browser_basic.js]
+[browser_clipboard.js]
+skip-if = debug # Bug 1507747
+subsuite = clipboard
 [browser_edit.js]
 skip-if = debug # Bug 1507747
 [browser_locked.js]
 [browser_observe.js]
 skip-if = debug # Bug 1507747
 [browser_search.js]
 skip-if = debug # Bug 1507747
 [browser_warning.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutconfig/test/browser/browser_clipboard.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["test.aboutconfig.copy.false", false],
+      ["test.aboutconfig.copy.number", 10],
+      ["test.aboutconfig.copy.spaces.1", " "],
+      ["test.aboutconfig.copy.spaces.2", "  "],
+      ["test.aboutconfig.copy.spaces.3", "   "],
+      ["test.aboutconfig.copy.string", "010.5"],
+    ],
+  });
+});
+
+add_task(async function test_copy() {
+  await AboutConfigTest.withNewTab(async function() {
+    for (let [name, expectedString] of [
+      [PREF_BOOLEAN_DEFAULT_TRUE, "true"],
+      [PREF_BOOLEAN_USERVALUE_TRUE, "true"],
+      [PREF_STRING_DEFAULT_EMPTY, ""],
+      ["test.aboutconfig.copy.false", "false"],
+      ["test.aboutconfig.copy.number", "10"],
+      ["test.aboutconfig.copy.spaces.1", " "],
+      ["test.aboutconfig.copy.spaces.2", "  "],
+      ["test.aboutconfig.copy.spaces.3", "   "],
+      ["test.aboutconfig.copy.string", "010.5"],
+    ]) {
+      // Limit the number of preferences shown so all the rows are visible.
+      this.search(name);
+      let row = this.getRow(name);
+
+      // Triple click at any location in the name cell should select the name.
+      await BrowserTestUtils.synthesizeMouseAtCenter(row.nameCell,
+                                                     { clickCount: 3 },
+                                                     this.browser);
+      Assert.ok(row.nameCell.contains(this.window.getSelection().anchorNode));
+      await SimpleTest.promiseClipboardChange(name, async () => {
+        await BrowserTestUtils.synthesizeKey("c", { accelKey: true },
+                                             this.browser);
+      });
+
+      // Triple click at any location in the value cell should select the value.
+      await BrowserTestUtils.synthesizeMouseAtCenter(row.valueCell,
+                                                     { clickCount: 3 },
+                                                     this.browser);
+      let selection = this.window.getSelection();
+      Assert.ok(row.valueCell.contains(selection.anchorNode));
+
+      // The selection is never collapsed because of the <span> element, and
+      // this makes sure that an empty string can be copied.
+      Assert.ok(!selection.isCollapsed);
+      await SimpleTest.promiseClipboardChange(expectedString, async () => {
+        await BrowserTestUtils.synthesizeKey("c", { accelKey: true },
+                                             this.browser);
+      });
+    }
+  });
+});
+
+add_task(async function test_copy_multiple() {
+  await AboutConfigTest.withNewTab(async function() {
+    // Lines are separated by a single LF character on all platforms.
+    let expectedString = "test.aboutconfig.copy.false\tfalse\t\n" +
+                         "test.aboutconfig.copy.number\t10\t\n" +
+                         "test.aboutconfig.copy.spaces.1\t \t\n" +
+                         "test.aboutconfig.copy.spaces.2\t  \t\n" +
+                         "test.aboutconfig.copy.spaces.3\t   \t\n" +
+                         "test.aboutconfig.copy.string\t010.5";
+
+    this.search("test.aboutconfig.copy.");
+    let startRow = this.getRow("test.aboutconfig.copy.false");
+    let endRow = this.getRow("test.aboutconfig.copy.string");
+    let { width, height } = endRow.valueCell.getBoundingClientRect();
+
+    // Drag from the top left of the first row to the bottom right of the last.
+    await BrowserTestUtils.synthesizeMouse(startRow.nameCell, 1, 1,
+                                           { type: "mousedown" }, this.browser);
+    await BrowserTestUtils.synthesizeMouse(endRow.valueCell,
+                                           width - 1, height - 1,
+                                           { type: "mousemove" }, this.browser);
+    await BrowserTestUtils.synthesizeMouse(endRow.valueCell,
+                                           width - 1, height - 1,
+                                           { type: "mouseup" }, this.browser);
+
+    await SimpleTest.promiseClipboardChange(expectedString, async () => {
+      await BrowserTestUtils.synthesizeKey("c", { accelKey: true },
+                                           this.browser);
+    });
+  });
+});
--- a/browser/components/aboutconfig/test/browser/head.js
+++ b/browser/components/aboutconfig/test/browser/head.js
@@ -24,29 +24,37 @@ class AboutConfigRowTest {
   constructor(element) {
     this.element = element;
   }
 
   querySelector(selector) {
     return this.element.querySelector(selector);
   }
 
+  get nameCell() {
+    return this.querySelector("td");
+  }
+
   get name() {
-    return this.querySelector("td").textContent;
+    return this.nameCell.textContent;
+  }
+
+  get valueCell() {
+    return this.querySelector("td.cell-value");
   }
 
   get value() {
-    return this.querySelector("td.cell-value").textContent;
+    return this.valueCell.textContent;
   }
 
   /**
    * Text input field when the row is in edit mode.
    */
   get valueInput() {
-    return this.querySelector("td.cell-value input");
+    return this.valueCell.querySelector("input");
   }
 
   /**
    * This is normally "edit" or "toggle" based on the preference type, "save"
    * when the row is in edit mode, or "add" when the preference does not exist.
    */
   get editColumnButton() {
     return this.querySelector("td.cell-edit > button");
@@ -72,17 +80,19 @@ class AboutConfigTest {
     }, async browser => {
       let scope = new this(browser);
       await scope.setupNewTab(options);
       await testFn.call(scope);
     });
   }
 
   constructor(browser) {
+    this.browser = browser;
     this.document = browser.contentDocument;
+    this.window = browser.contentWindow;
   }
 
   async setupNewTab(options) {
     await this.document.l10n.ready;
     if (!options.dontBypassWarning) {
       this.document.querySelector("button").click();
     }
   }
--- a/testing/mochitest/tests/SimpleTest/SimpleTest.js
+++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js
@@ -985,17 +985,17 @@ SimpleTest.promiseClipboardChange = asyn
         inputValidatorFn = typeof(aExpectedStringOrValidatorFn) == "string"
             ? function(aData) { return aData == aExpectedStringOrValidatorFn; }
             : aExpectedStringOrValidatorFn;
     }
 
     let maxPolls = aTimeout ? aTimeout / 100 : 50;
 
     async function putAndVerify(operationFn, validatorFn, flavor) {
-        operationFn();
+        await operationFn();
 
         let data;
         for (let i = 0; i < maxPolls; i++) {
             data = SpecialPowers.getClipboardData(flavor);
             if (validatorFn(data)) {
                 // Don't show the success message when waiting for preExpectedVal
                 if (preExpectedVal) {
                     preExpectedVal = null;