Clean up visible test handling (including GTK tests).

This will be used for backgrounding (e.g., for runin).

BUG=None
TEST=Manual

Change-Id: Id5e7988f73285fc62407e2d36f15619421983250
Reviewed-on: https://gerrit.chromium.org/gerrit/26801
Tested-by: Jon Salz <jsalz@chromium.org>
Reviewed-by: Chinyue Chen <chinyue@chromium.org>
Reviewed-by: Jon Salz <jsalz@chromium.org>
Commit-Ready: Jon Salz <jsalz@chromium.org>
diff --git a/py/goofy/goofy.py b/py/goofy/goofy.py
index cebd415..a73d982 100755
--- a/py/goofy/goofy.py
+++ b/py/goofy/goofy.py
@@ -203,6 +203,9 @@
         lambda event: self.update_factory(),
       Event.Type.STOP:
         lambda event: self.stop(),
+      Event.Type.SET_VISIBLE_TEST:
+        lambda event: self.set_visible_test(
+          self.test_list.lookup_path(event.path)),
     }
 
     self.exceptions = []
@@ -1030,8 +1033,7 @@
       # Already running: just bring to the front if it
       # has a UI.
       logging.info('Setting visible test to %s', test.path)
-      self.event_client.post_event(
-        Event(Event.Type.SET_VISIBLE_TEST, path=test.path))
+      self.set_visible_test(test)
       return
 
     self.abort_active_tests()
diff --git a/py/goofy/js/goofy.js b/py/goofy/js/goofy.js
index 39fe697..3d9fe9e 100644
--- a/py/goofy/js/goofy.js
+++ b/py/goofy/js/goofy.js
@@ -229,6 +229,10 @@
      * @type HTMLIFrameElement
      */
     this.iframe = goog.dom.iframe.createBlank(new goog.dom.DomHelper(document));
+    goog.dom.classes.add(this.iframe, 'goofy-test-iframe');
+    goog.dom.classes.enable(this.iframe, 'goofy-test-visible',
+                            /** @type boolean */(
+                                goofy.pathTestMap[path].state.visible));
     document.getElementById('goofy-main').appendChild(this.iframe);
     this.iframe.contentWindow.test = this.test;
 };
@@ -269,10 +273,22 @@
     this.uuid = null;
 
     /**
-     * Whether the context menu is currently visible.
-     * @type boolean
+     * The currently visible context menu, if any.
+     * @type goog.ui.PopupMenu
      */
-    this.contextMenuVisible = false;
+    this.contextMenu = null;
+
+    /**
+     * The last test for which a context menu was displayed.
+     * @type {?string}
+     */
+    this.lastContextMenuPath = null;
+
+    /**
+     * The time at which the last context menu was hidden.
+     * @type {?number}
+     */
+    this.lastContextMenuHideTime = null;
 
     /**
      * All tooltips that we have created.
@@ -880,13 +896,30 @@
  */
 cros.factory.Goofy.prototype.showTestPopup = function(path, labelElement,
                                                       extraItems) {
-    this.contextMenuVisible = true;
+    var test = this.pathTestMap[path];
+
+    if (path == this.lastContextMenuPath &&
+        (goog.now() - this.lastContextMenuHideTime <
+         goog.ui.PopupBase.DEBOUNCE_DELAY_MS)) {
+        // We just hid it; don't reshow.
+        return false;
+    }
+
+    // If it's a leaf node, and it's the active but not the visible
+    // test, ask the backend to make it visible.
+    if (test.state.status == 'ACTIVE' &&
+        !/** @type boolean */(test.state.visible) &&
+        !test.subtests.length) {
+        this.sendEvent('goofy:set_visible_test', {'path': path});
+    }
+
     // Hide all tooltips so that they don't fight with the context menu.
     goog.array.forEach(this.tooltips, function(tooltip) {
             tooltip.setVisible(false);
         });
 
-    var menu = new goog.ui.PopupMenu();
+    var menu = this.contextMenu = new goog.ui.PopupMenu();
+    this.lastContextMenuPath = path;
 
     if (extraItems && extraItems.length) {
         goog.array.forEach(extraItems, function(item) {
@@ -897,7 +930,6 @@
 
     var numLeaves = 0;
     var numLeavesByStatus = {};
-    var test = this.pathTestMap[path];
     var allPaths = [];
     function countLeaves(test) {
         allPaths.push(test.path);
@@ -975,8 +1007,10 @@
     goog.events.listen(menu, goog.ui.Component.EventType.HIDE,
                        function(event) {
                            menu.dispose();
-                           this.contextMenuVisible = false;
+                           this.contextMenu = null;
+                           this.lastContextMenuHideTime = goog.now();
                        }, true, this);
+    return true;
 };
 
 /**
@@ -994,7 +1028,7 @@
     tooltip.setHtml('')
 
     var errorMsg = test.state['error_msg'];
-    if (test.state.status != 'FAILED' || this.contextMenuVisible || !errorMsg) {
+    if (test.state.status != 'FAILED' || this.contextMenu || !errorMsg) {
         // Don't bother showing it.
         event.preventDefault();
     } else {
@@ -1063,9 +1097,11 @@
         goog.events.listen(
             labelElement, goog.events.EventType.MOUSEDOWN,
             function(event) {
-                this.showTestPopup(path, labelElement);
-                event.stopPropagation();
-                event.preventDefault();
+                if (event.button == 0) {
+                    this.showTestPopup(path, labelElement);
+                    event.stopPropagation();
+                    event.preventDefault();
+                }
             }, true, this);
     }, this);
 
@@ -1081,6 +1117,12 @@
                 document.getElementById('goofy-title'),
                 eventType,
                 function(event) {
+                    if (eventType == goog.events.EventType.MOUSEDOWN &&
+                        event.button != 0) {
+                        // Only process primary button for MOUSEDOWN.
+                        return;
+                    }
+
                     var updateItem = new goog.ui.MenuItem(
                         cros.factory.Content('Update factory software',
                                              '更新工廠軟體'));
@@ -1137,6 +1179,15 @@
             }),
         'goofy-status-' + state.status.toLowerCase());
 
+    var visible = /** @type boolean */(state.visible);
+    goog.dom.classes.enable(elt, 'goofy-test-visible', visible);
+    goog.object.forEach(this.invocations, function(invoc, uuid) {
+            if (invoc.path == path) {
+                goog.dom.classes.enable(invoc.iframe,
+                                        'goofy-test-visible', visible);
+            }
+        }, this);
+
     if (state.status == 'ACTIVE') {
         // Automatically show the test if it is running.
         node.reveal();
diff --git a/py/goofy/static/goofy.css b/py/goofy/static/goofy.css
index f11a328..d7c9e97 100644
--- a/py/goofy/static/goofy.css
+++ b/py/goofy/static/goofy.css
@@ -76,16 +76,6 @@
 #goofy-console {
   padding: .5em;
 }
-#goofy-main {
-  left: 0; top: 0;
-  width: 100%; height: 100%;
-}
-#goofy-main iframe {
-  position: absolute;
-  left: 0; top: 0;
-  width: 100%; height: 100%;
-  padding: .5em;
-}
 
 /* Overrides for status bar. */
 #goofy-status-bar {
@@ -195,6 +185,9 @@
   vertical-align: -2px;
   width: 16px; height: 16px;
 }
+.goofy-test-visible .goog-tree-item-label {
+  background-color: red;
+}
 .goofy-status-passed > div > .goofy-test-icon {
   background-image: url(images/passed.gif);
 }
@@ -267,6 +260,21 @@
   font-style: italic;
 }
 
+/* Elements in the main pane. */
+#goofy-main {
+  left: 0; top: 0;
+  width: 100%; height: 100%;
+}
+.goofy-test-iframe {
+  position: absolute;
+  left: 0; top: 0;
+  width: 100%; height: 100%;
+  padding: .5em;
+  display: none;
+}
+.goofy-test-iframe.goofy-test-visible {
+  display: inline;
+}
 /* Elements in the console pane. */
 .goofy-internal-log {
   font-style: italic;
@@ -314,12 +322,17 @@
 }
 #goofy-control .goog-tree-item-label {
   margin-left: 2px;
+  cursor: pointer;
 }
 #goofy-control .goog-tree-expand-icon {
   vertical-align: -1px;
 }
-/* Use same color, whether or not it's focused. */
-#goofy-control .selected .goog-tree-item-label {
+/* No background for any tree items in the control pane. */
+#goofy-control .goog-tree-item-label {
+  background-color: transparent;
+}
+/* Add a background for the visible test. */
+#goofy-control .goofy-test-visible .goog-tree-item-label {
   background-color: #cddff0;
 }
 .modal-dialog-bg {
diff --git a/py/test/ui.py b/py/test/ui.py
index a3c4ef7..61d7f14 100755
--- a/py/test/ui.py
+++ b/py/test/ui.py
@@ -501,9 +501,11 @@
 
   def handle_event(event):
     if (event.type == Event.Type.STATE_CHANGE and
-      test_path and event.path == test_path and
-      event.state.visible):
-      show_window()
+      test_path and event.path == test_path):
+      if event.state.visible:
+        show_window()
+      else:
+        window.hide()
 
   event_client = EventClient(
       callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)