Subject: Re: Acceptance Tests against a browser (WIP)

From: Atira Odhner <[email-masked]>
Date: Tue, 17 Jan 2017 11:09:48 -0500

Thanks for your feedback, Dave!

We can put the tests under the regression directory. I think that makes
sense.
I'm not picturing these tests being module specific, but we may want to
enable running it as a separate suite of tests.

Thanks for the callout about the port and title. We'll make sure those are
pulled from config or that the pgAdmin server is spun up by the test with
specific values.

I have a couple ideas about why the test might not have been running for
you. I think the patch we attached didn't spin up its own pgAdmin yet and
it definitely doesn't fill in username/password if your app is running that
way. That's part of the WIP-ness :-P

-Tira

Hi

On Thu, Jan 12, 2017 at 10:41 PM, George Gelashvili
<ggelashvili(at)pivotal(dot)io> wrote:
> here's the patch we forgot to attach. Also, you can see work on our branch
> at:
> https://github.com/pivotalsoftware/pgadmin4/tree/pivotal/acceptance-tests
>
> On Thu, Jan 12, 2017 at 5:26 PM, George Gelashvili <ggelashvili(at)pivotal(dot)io>
> wrote:
>>
>> Hi there,
>>
>> We are working on browser-automation-based acceptance tests that exercise
>> pgAdmin4 the way a user might.

Nice!

>> The first "connect to database" test works, but at the moment depends on
>> Chrome and chromedriver. We would appreciate feedback on any possible
>> license or code style issues at this point, as well as any thoughts on
>> adding this sort of test to the codebase.

A few thoughts:

- If these tests are to run as part of the regression suite, the
framework for them should live under that directory.

- Are any of the tests likely to be module-specific? If so, they
should really be part of the relevant module as the regression tests
are. If they're more general/less tightly coupled, then I don't see a
problem with them residing where they are.

- Please take care not to include changes to .gitgnore files that
aren't relevant to the rest of us.

- The port number is hard-coded in the test.

- You've hard-coded the string "pgAdmin 4". We've tried to keep that
title as a config option in config.py, so you should pull the string
from there rather than hard-coding it.

- The connect test fails for me (Mac, Python 2.7). I have a suspicion
that this may be because when the test starts chromedriver, OS X
prompts the user about whether a listening port should be opened, but
the tests don't wait (though, I tested with 3 servers configured and
it failed with the same error on the second and third as well, long
after I clicked OK on the prompt):

Traceback (most recent call last):
  File "/Users/dpage/git/pgadmin4/web/acceptance/test_connects_to_database.py",
line 32, in runTest
    self.assertEqual("pgAdmin 4", self.driver.title)
AssertionError: 'pgAdmin 4' != u'localhost'

- Please keep tests in the pgadmin. namespace (pgadmin.acceptance.??).

- It looks like running a single test won't work yet (because of
TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' %
arguments['pkg']))

Thanks!

-- 
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake

EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

---

From: George Gelashvili <[email-masked]>
Date: Thu, 19 Jan 2017 17:07:44 -0500

Here is an updated patch which starts the server up when the test starts
and uses the values from config.py for server name etc. It still requires
installing chromedriver before running. Should we add something to the
readme about that?

> TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' %
diff --git a/requirements_py2.txt b/requirements_py2.txt
index 51170a45..de167121 100644
--- a/requirements_py2.txt
+++ b/requirements_py2.txt
@@ -36,6 +36,7 @@ testscenarios==0.5.0
 testtools==2.0.0
 traceback2==1.4.0
 unittest2==1.1.0
+selenium==3.0.2
 Werkzeug==0.9.6
 WTForms==2.0.2
 sqlparse==0.1.19
diff --git a/requirements_py3.txt b/requirements_py3.txt
index f68db7a8..9565a6e4 100644
--- a/requirements_py3.txt
+++ b/requirements_py3.txt
@@ -35,6 +35,7 @@ testscenarios==0.5.0
 testtools==2.0.0
 traceback2==1.4.0
 unittest2==1.1.0
+selenium==3.0.2
 Werkzeug==0.9.6
 WTForms==2.0.2
 sqlparse==0.1.19
diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/web/pgadmin/acceptance/tests/test_connects_to_database.py b/web/pgadmin/acceptance/tests/test_connects_to_database.py
new file mode 100644
index 00000000..ccbcf9f5
--- /dev/null
+++ b/web/pgadmin/acceptance/tests/test_connects_to_database.py
@@ -0,0 +1,108 @@
+#############################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2017, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##############################################################
+
+import time
+
+from selenium import webdriver
+from selenium.common.exceptions import NoSuchElementException
+from selenium.webdriver import ActionChains
+
+from pgadmin.utils.route import BaseTestGenerator
+
+import subprocess
+import os
+import signal
+import config
+
+
+class ConnectsToDatabase(BaseTestGenerator):
+    """
+    Tests that a database connection can be created from the UI
+    """
+
+    def setUp(self):
+        if (config.SERVER_MODE):
+            self.skipTest("Currently, config is set to start pgadmin in server mode. "
+                          "This test doesn't know username and password so doesn't work in server mode")
+
+        self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid)
+
+        print("waiting for server to start")
+        time.sleep(10)
+
+        print("opening browser")
+        self.driver = webdriver.Chrome()
+        self.server_config = self.server
+        print(config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT))
+        self.driver.get("http://" + config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT))
+
+    def runTest(self):
+        self.assertEqual(config.APP_NAME, self.driver.title)
+        self._wait_for_spinner_to_disappear()
+
+        self._find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click()
+        self.driver.find_element_by_link_text("Object").click()
+        ActionChains(self.driver) \
+            .move_to_element(self.driver.find_element_by_link_text("Create")) \
+            .perform()
+        self._find_by_partial_link_text("Server...").click()
+
+        self._fill_input_by_xpath("name", self.server_config['name'])
+        self._find_by_partial_link_text("Connection").click()
+        self._fill_input_by_xpath("host", self.server_config['host'])
+        self._fill_input_by_xpath("port", self.server_config['port'])
+        self._fill_input_by_xpath("username", self.server_config['username'])
+        self._fill_input_by_xpath("password", self.server_config['db_password'])
+        self._find_by_xpath("//button[contains(.,'Save')]").click()
+
+        self._find_by_xpath("//*[@id='tree']//*[.='" + self.server_config['name'] + "']")
+
+    def tearDown(self):
+        self.driver.close()
+        os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM)
+
+    def failureException(self, *args, **kwargs):
+        self.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png')
+        return AssertionError(*args, **kwargs)
+
+    def _find_by_xpath(self, xpath):
+        return self._wait_for_element(lambda: self.driver.find_element_by_xpath(xpath))
+
+    def _find_by_partial_link_text(self, link_text):
+        return self._wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text))
+
+    def _fill_input_by_xpath(self, field_name, field_content):
+        self._find_by_xpath("//input[@name='" + field_name + "']").clear()
+        self._find_by_xpath("//input[@name='" + field_name + "']").send_keys(
+            field_content)
+
+    def _wait_for_element(self, find_method_with_args):
+        timeout = 5
+        time_waited = 0
+        sleep_time = 0.01
+
+        while time_waited < timeout:
+            try:
+                element = find_method_with_args()
+                if element.is_displayed() & element.is_enabled():
+                    return element
+            except NoSuchElementException:
+                time_waited += sleep_time
+                time.sleep(sleep_time)
+                pass
+
+        self.fail("Timed out waiting for element")
+
+    def _wait_for_spinner_to_disappear(self):
+        try:
+            while True:
+                self.driver.find_element_by_id("pg-spinner")
+                time.sleep(0.1)
+        except NoSuchElementException:
+            pass
diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py
index f18d2c18..fed26a0f 100644
--- a/web/pgadmin/utils/route.py
+++ b/web/pgadmin/utils/route.py
@@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta):
         ABCMeta.__init__(cls, name, bases, d)
 
     @classmethod
-    def load_generators(cls, pkg):
+    def load_generators(cls, *pkgs):
 
         cls.registry = dict()
 
+        all_modules = []
+
+        for pkg in pkgs:
+            all_modules += find_modules(pkg, False, True)
+
+
         # Check for SERVER mode
-        if config.SERVER_MODE:
-            for module_name in find_modules(pkg, False, True):
+        for module_name in all_modules:
+            if config.SERVER_MODE:
                 try:
                     if "tests." in str(module_name):
                         import_module(module_name)
                 except ImportError:
                     traceback.print_exc(file=sys.stderr)
-        else:
-            for module_name in find_modules(pkg, False, True):
+            else:
                 try:
                     # Exclude the test cases in browser node if SERVER_MODE
                     # is False
diff --git a/web/regression/.gitignore b/web/regression/.gitignore
index 0581810b..723fce7e 100644
--- a/web/regression/.gitignore
+++ b/web/regression/.gitignore
@@ -1,4 +1,5 @@
 parent_id.pkl
 regression.log
+test_greenplum_config.json
 test_advanced_config.json
 test_config.json

-- 
Sent via pgadmin-hackers mailing list ([email-masked])
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgadmin-hackers

---

From: George Gelashvili <[email-masked]>
Date: Thu, 19 Jan 2017 18:15:04 -0500

Here's an updated patch which polls to wait for the app to start instead of
sleeping for 10 seconds.

On Thu, Jan 19, 2017 at 5:07 PM, George Gelashvili <[email-masked]>
wrote:

>> Thanks for the callout about the port and title. We'll make sure those
>> are pulled from config or that the pgAdmin server is spun up by the test
>> with specific values.
>>
index 00000000..2c4f85c4
@@ -0,0 +1,123 @@
+        self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid, stderr=open(os.devnull, 'w'))
+        self._wait_for_app()
+        def element_if_it_exists():
+                return False
+        return self.__wait_for("element to exist", element_if_it_exists)
+        def spinner_has_disappeared():
+                return False
+                return True
+        self.__wait_for("spinner to disappear", spinner_has_disappeared)
+    def _wait_for_app(self):
+        def page_shows_app():
+            self.driver.refresh()
+            return self.driver.title == config.APP_NAME
+        self.__wait_for("app to start", page_shows_app)
+    def __wait_for(self, waiting_for_message, condition_met_function):
+            result = condition_met_function()
+            if(result):
+                return result
+        self.fail("Timed out waiting for " + waiting_for_message)
 
     @classmethod
 
         cls.registry = dict()
 
+        all_modules = []
+
+        for pkg in pkgs:

---

From: Dave Page <[email-masked]>
Date: Fri, 20 Jan 2017 15:38:21 +0000

Hi

> Here's an updated patch which polls to wait for the app to start instead of

I see the browser opening now, and it immediately loads pgAdmin. Then,
it prompts me for a reload whenever the test is run, which fails no
matter how quickly I hit the prompt from what I can see:

======================================================================
ERROR: runTest (pgadmin.acceptance.tests.test_connects_to_database.ConnectsToDatabase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/test_connects_to_database.py",
line 41, in setUp
    self._wait_for_app()
  File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/test_connects_to_database.py",
line 109, in _wait_for_app
    self.__wait_for("app to start", page_shows_app)
  File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/test_connects_to_database.py",
line 117, in __wait_for
    result = condition_met_function()
  File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/test_connects_to_database.py",
line 107, in page_shows_app
    return self.driver.title == config.APP_NAME
  File "/Users/dpage/.virtualenvs/pgadmin4/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py",
line 257, in title
    resp = self.execute(Command.GET_TITLE)
  File "/Users/dpage/.virtualenvs/pgadmin4/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py",
line 236, in execute
    self.error_handler.check_response(response)
  File "/Users/dpage/.virtualenvs/pgadmin4/lib/python2.7/site-packages/selenium/webdriver/remote/errorhandler.py",
line 192, in check_response
    raise exception_class(message, screen, stacktrace)
UnexpectedAlertPresentException: Alert Text: None
Message: unexpected alert open: {Alert text : Are you sure you wish to
close the pgAdmin 4 browser?}
  (Session info: chrome=55.0.2883.95)
  (Driver info: chromedriver=2.27.440174
(e97a722caafc2d3a8b807ee115bfb307f7d2cfd9),platform=Mac OS X 10.12.1
x86_64)

---

From: Dave Page <[email-masked]>
Date: Fri, 20 Jan 2017 15:38:55 +0000

On Thu, Jan 19, 2017 at 10:07 PM, George Gelashvili
<[email-masked]> wrote:
>
> Here is an updated patch which starts the server up when the test starts and
> uses the values from config.py for server name etc. It still requires

Yes, we definitely should (including download site URL)

>> you. I think the patch we attached didn't spin up its own pgAdmin yet and it
>> definitely doesn't fill in username/password if your app is running that
>>   File
>> "/Users/dpage/git/pgadmin4/web/acceptance/test_connects_to_database.py",
>> line 32, in runTest

---

From: George Gelashvili <[email-masked]>
Date: Fri, 20 Jan 2017 12:33:09 -0500

Thanks for bringing that to our attention! Here's the latest patch

On Fri, Jan 20, 2017 at 10:38 AM, Dave Page <[email-masked]> wrote:

> and
> are
index 00000000..c35cd0fd
@@ -0,0 +1,127 @@
+        self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid,
+                                                stderr=open(os.devnull, 'w'))
+            if self.driver.title == config.APP_NAME:
+            if result:
 
     @classmethod
 
         cls.registry = dict()
 
+        all_modules = []
+
+        for pkg in pkgs:
+            all_modules += find_modules(pkg, False, True)
+
+
         # Check for SERVER mode
-        if config.SERVER_MODE:
-            for module_name in find_modules(pkg, False, True):
+        for module_name in all_modules:
+            if config.SERVER_MODE:
                 try:
                     if "tests." in str(module_name):
                         import_module(module_name)
                 except ImportError:
                     traceback.print_exc(file=sys.stderr)
-        else:
-            for module_name in find_modules(pkg, False, True):
+            else:
                 try:
                     # Exclude the test cases in browser node if SERVER_MODE
                     # is False
diff --git a/web/regression/.gitignore b/web/regression/.gitignore
index 0581810b..723fce7e 100644
--- a/web/regression/.gitignore
+++ b/web/regression/.gitignore
@@ -1,4 +1,5 @@
 parent_id.pkl
 regression.log
+test_greenplum_config.json
 test_advanced_config.json
 test_config.json
diff --git a/web/regression/README b/web/regression/README
index 8cc29987..ae5d268d 100644
--- a/web/regression/README
+++ b/web/regression/README
@@ -103,6 +103,10 @@ Test Data Details
 Execution:
 -----------
 
+- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed:
+  get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager
+  and make sure it is on the PATH
+
 - The test framework is modular and pluggable and dynamically locates tests
   for modules which are discovered at runtime. All test cases are found
   and registered automatically by its module name in
diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py
index a2d2f5cd..1f9f0522 100644
--- a/web/regression/test_utils.py
+++ b/web/regression/test_utils.py
@@ -69,7 +69,7 @@ def get_config_data():
     """This function reads the server data from config_data"""
     server_data = []
     for srv in test_setup.config_data['server_credentials']:
-        if (not srv.has_key('enabled')) or srv['enabled'] == True:
+        if (not 'enabled' in srv) or srv['enabled']:
             data = {"name": srv['name'],
                     "comment": srv['comment'],
                     "host": srv['host'],

---

From: Dave Page <[email-masked]>
Date: Tue, 24 Jan 2017 09:43:17 +0000

> Thanks for bringing that to our attention! Here's the latest patch

piranha:pgadmin4 dpage$ git apply
~/Downloads/acceptance-tests-with-server-start-and-polling.diff
error: patch failed: web/regression/test_utils.py:69
error: web/regression/test_utils.py: patch does not apply

:-(

>> >> >> The first "connect to database" test works, but at the moment
>> >> >> depends
>> >> >> on

---

From: George Gelashvili <[email-masked]>
Date: Wed, 25 Jan 2017 18:31:06 -0500

ah
That diff was generated before the python 3 patch was applied. This should
work against master

Cheers,
George

On Tue, Jan 24, 2017 at 4:43 AM, Dave Page <[email-masked]> wrote:

> >> >> >> license or code style issues at this point, as well as any
> thoughts
> >> >> >> on
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py
index f501d3d0..99fa00c7 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py
@@ -1673,8 +1673,8 @@ class TableView(PGChildNodeView, DataTypeReader, VacuumSettings):
 
             return make_json_response(
                 success=1,
-                info=gettext("Trigger(s) have been enabled") if is_enable
-                else gettext("Trigger(s) have been disabled"),
+                info=gettext("Trigger(s) has been enabled") if is_enable
+                else gettext("Trigger(s) has been disabled"),
                 data={
                     'id': tid,
                     'scid': scid
@@ -1706,7 +1706,7 @@ class TableView(PGChildNodeView, DataTypeReader, VacuumSettings):
 
             return make_json_response(
                 success=1,
-                info=gettext("Table statistics have been reset"),
+                info=gettext("Table statistics has been reset"),
                 data={
                     'id': tid,
                     'scid': scid
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py
index fe7d9349..6b7f52b0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py
@@ -816,7 +816,7 @@ class ExclusionConstraintView(PGChildNodeView):
             sql = render_template("/".join([self.template_path, 'create.sql']),
                                   data=data, conn=self.conn)
 
-        return sql, data['name']
+        return sql, data['name'] if 'name' in data else old_data['name']
 
     @check_precondition
     def sql(self, gid, sid, did, scid, tid, exid=None):
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js
index e7bd905c..cfe15195 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js
@@ -7,11 +7,10 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) {
     defaults: {
       column: undefined,
       oper_class: undefined,
-      order: false,
-      nulls_order: false,
+      order: undefined,
+      nulls_order: undefined,
       operator:undefined,
-      col_type:undefined,
-      is_sort_nulls_applicable: true
+      col_type:undefined
     },
     toJSON: function () {
       var d = pgBrowser.Node.Model.prototype.toJSON.apply(this, arguments);
@@ -24,9 +23,26 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) {
       },{
         id: 'oper_class', label:'{{ _('Operator class') }}', type:'text',
         node: 'table', url: 'get_oper_class', first_empty: true,
-        editable: true,
+        editable: function(m) {
+          if (m instanceof Backbone.Collection) {
+            return true;
+          }
+          if ((_.has(m.collection, 'handler') &&
+                !_.isUndefined(m.collection.handler) &&
+                !_.isUndefined(m.collection.handler.get('oid')))) {
+            return false;
+          }
+
+          if (m.collection) {
+            var indexType = m.collection.handler.get('amname')
+            return (indexType == 'btree' || _.isUndefined(indexType) ||
+              _.isNull(indexType) || indexType == '');
+          } else {
+            return true;
+          }
+        },
         select2: {
-          allowClear: true, width: 'style', tags: true,
+          allowClear: true, width: 'style',
           placeholder: '{{ _("Select the operator class") }}'
         }, cell: Backgrid.Extension.Select2Cell.extend({
           initialize: function () {
@@ -39,12 +55,6 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) {
 
             if (url && (indextype == 'btree' || _.isUndefined(indextype) ||
                 _.isNull(indextype) || indextype == '')) {
-              // Set sort_order and nulls to true if access method is btree
-              setTimeout(function() {
-                m.set('order', true);
-                m.set('nulls_order', true);
-              }, 10);
-
               var node = this.column.get('schema_node'),
                   eventHandler = m.top || m,
                   node_info = this.column.get('node_info'),
@@ -98,14 +108,6 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) {
           if (m instanceof Backbone.Collection) {
             return true;
           }
-          else {
-            if (m.top.get('amname') === 'btree') {
-              m.set('is_sort_nulls_applicable', true);
-              return true;
-            }
-            m.set('is_sort_nulls_applicable', false);
-            return false;
-          }
           if ((_.has(m.collection, 'handler') &&
                 !_.isUndefined(m.collection.handler) &&
                 !_.isUndefined(m.collection.handler.get('oid')))) {
@@ -122,15 +124,6 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) {
           if (m instanceof Backbone.Collection) {
             return true;
           }
-          else {
-            if (m.top.get('amname') === 'btree') {
-              m.set('is_sort_nulls_applicable', true);
-              return true;
-            }
-            m.set('is_sort_nulls_applicable', false);
-            return false;
-          }
-
           if ((_.has(m.collection, 'handler') &&
                 !_.isUndefined(m.collection.handler) &&
                 !_.isUndefined(m.collection.handler.get('oid')))) {
@@ -905,15 +898,8 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) {
         }],
         validate: function() {
           this.errorModel.clear();
-          var columns = this.get('columns'),
-              name = this.get('name');
-
-          if ((_.isUndefined(name) || _.isNull(name) || name.length < 1)) {
-            var msg = '{{ _('Please specify name for exclusion constraint.') }}';
-            this.errorModel.set('name', msg);
-            return msg;
-          }
-          else  if ((_.isUndefined(columns) || _.isNull(columns) || columns.length < 1)) {
+          var columns = this.get('columns');
+          if ((_.isUndefined(columns) || _.isNull(columns) || columns.length < 1)) {
             var msg = '{{ _('Please specify columns for exclusion constraint.') }}';
             this.errorModel.set('columns', msg);
             return msg;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js
index 6f462652..6531ba5e 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js
@@ -51,8 +51,7 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) {
           collspcname: undefined,
           op_class: undefined,
           sort_order: false,
-          nulls: false,
-          is_sort_nulls_applicable: true
+          nulls: false
         },
         schema: [
           {
@@ -78,7 +77,7 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) {
             control: 'node-ajax-options', url: 'get_collations', node: 'index'
           },{
             id: 'op_class', label:'{{ _('Operator class') }}',
-            cell: NodeAjaxOptionsDepsCell, tags: true,
+            cell: NodeAjaxOptionsDepsCell,
             type: 'text', disabled: 'checkAccessMethod',
             editable: function(m) {
                 // Header cell then skip
@@ -109,19 +108,13 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) {
           },{
             id: 'sort_order', label:'{{ _('Sort order') }}',
             cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch',
+            disabled: 'checkAccessMethod',
             editable: function(m) {
-              // Header cell then skip
-              if (m instanceof Backbone.Collection) {
-                  return false;
-              }
-              else {
-                if (m.top.get('amname') === 'btree') {
-                  m.set('is_sort_nulls_applicable', true);
-                  return true;
+                // Header cell then skip
+                if (m instanceof Backbone.Collection) {
+                    return false;
                 }
-                m.set('is_sort_nulls_applicable', false);
-                return false;
-              }
+                return !(m.checkAccessMethod.apply(this, arguments));
             },
             deps: ['amname'],
             options: {
@@ -132,18 +125,13 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) {
           },{
             id: 'nulls', label:'{{ _('NULLs') }}',
             cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch',
+            disabled: 'checkAccessMethod',
             editable: function(m) {
-              // Header cell then skip
-              if (m instanceof Backbone.Collection) {
-                  return true;
-              } else {
-                  if (m.top.get('amname') === 'btree') {
-                    m.set('is_sort_nulls_applicable', true);
+                // Header cell then skip
+                if (m instanceof Backbone.Collection) {
                     return true;
-                  }
-                  m.set('is_sort_nulls_applicable', false);
-                  return false;
-              }
+                }
+                return !(m.checkAccessMethod.apply(this, arguments));
             },
             deps: ['amname', 'sort_order'],
             options: {
@@ -196,11 +184,9 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) {
             if(m.get('sort_order') == true && m.previous('sort_order') ==  false) {
                setTimeout(function() { m.set('nulls', true) }, 10);
             }
+            return false;
           }
-          else {
-            m.set('is_sort_nulls_applicable', false);
-          }
-          return false;
+          return true;
         },
     });
 
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql
index 6d0bd1be..db29048b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql
@@ -1,7 +1,7 @@
 ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }}
     ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} (
     {% for col in data.columns %}{% if loop.index != 1 %},
-    {% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %}
+    {% endif %}{{ conn|qtIdent(col.column)}} {% if col.oper_class and col.oper_class != '' %}{{col.oper_class}} {% endif%}{% if col.order %}ASC{% else %}DESC{% endif %} NULLS {% if col.nulls_order %}FIRST{% else %}LAST{% endif %} WITH {{col.operator}}{% endfor %}){% if data.fillfactor %}
     WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %}
 
     USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql
index 6d0bd1be..db29048b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql
@@ -1,7 +1,7 @@
 ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }}
     ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} (
     {% for col in data.columns %}{% if loop.index != 1 %},
-    {% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %}
+    {% endif %}{{ conn|qtIdent(col.column)}} {% if col.oper_class and col.oper_class != '' %}{{col.oper_class}} {% endif%}{% if col.order %}ASC{% else %}DESC{% endif %} NULLS {% if col.nulls_order %}FIRST{% else %}LAST{% endif %} WITH {{col.operator}}{% endfor %}){% if data.fillfactor %}
     WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %}
 
     USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql
index 6d0bd1be..db29048b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql
@@ -1,7 +1,7 @@
 ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }}
     ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} (
     {% for col in data.columns %}{% if loop.index != 1 %},
-    {% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %}
+    {% endif %}{{ conn|qtIdent(col.column)}} {% if col.oper_class and col.oper_class != '' %}{{col.oper_class}} {% endif%}{% if col.order %}ASC{% else %}DESC{% endif %} NULLS {% if col.nulls_order %}FIRST{% else %}LAST{% endif %} WITH {{col.operator}}{% endfor %}){% if data.fillfactor %}
     WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %}
 
     USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql
index b7bfa527..33af1973 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql
@@ -3,7 +3,7 @@ CREATE {% if data.indisunique %}UNIQUE {% endif %}INDEX {% if data.isconcurrent
 
 {% if mode == 'create' %}
     ({% for c in data.columns %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(c.colname)}}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.op_class %}
- {{c.op_class}}{% endif %}{% if data.amname is defined %}{% if c.sort_order is defined and c.is_sort_nulls_applicable %}{% if c.sort_order %} DESC{% else %} ASC{% endif %}{% endif %}{% if c.nulls is defined and c.is_sort_nulls_applicable %} NULLS {% if c.nulls %}
+ {{c.op_class}}{% endif %}{% if data.amname is defined and data.amname not in ['gist', 'gin'] %}{% if c.sort_order is defined %}{% if c.sort_order %} DESC{% else %} ASC{% endif %}{% endif %}{% if c.nulls is defined %} NULLS {% if c.nulls %}
 FIRST{% else %}LAST{% endif %}{% endif %}{% endif %}{% endfor %})
 {% else %}
 {## We will get indented data from postgres for column ##}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js
index c87a2930..2d006090 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js
@@ -198,54 +198,51 @@ function($, _, S, pgAdmin, pgBrowser, alertify) {
          });
        },
        reset_table_stats: function(args) {
-          var input = args || {},
-            obj = this,
-            t = pgBrowser.tree,
-            i = input.item || t.selected(),
-            d = i && i.length == 1 ? t.itemData(i) : undefined;
+          var input = args || {};
+          obj = this,
+          t = pgBrowser.tree,
+          i = input.item || t.selected(),
+          d = i && i.length == 1 ? t.itemData(i) : undefined;
 
           if (!d)
             return false;
 
           alertify.confirm(
-            '{{ _('Reset statistics') }}',
-            S('{{ _('Are you sure you want to reset the statistics for table %%s?') }}').sprintf(d._label).value(),
+            S('{{ _('Are you sure you want to reset table statistics for %s?') }}').sprintf(d.label).value(),
             function (e) {
-              if (e) {
-                var data = d;
-                $.ajax({
-                  url: obj.generate_url(i, 'reset' , d, true),
-                  type:'DELETE',
-                  success: function(res) {
-                    if (res.success == 1) {
-                      alertify.success("{{ _('" + res.info + "') }}");
-                      t.removeIcon(i);
-                      data.icon = 'icon-table';
-                      t.addIcon(i, {icon: data.icon});
-                      t.unload(i);
-                      t.setInode(i);
-                      t.deselect(i);
-                      // Fetch updated data from server
-                      setTimeout(function() {
-                        t.select(i);
-                      }, 10);
-                    }
-                  },
-                  error: function(xhr, status, error) {
-                    try {
-                      var err = $.parseJSON(xhr.responseText);
-                      if (err.success == 0) {
-                        msg = S('{{ _(' + err.errormsg + ')}}').value();
-                        alertify.error("{{ _('" + err.errormsg + "') }}");
-                      }
-                    } catch (e) {}
+            if (e) {
+              var data = d;
+              $.ajax({
+                url: obj.generate_url(i, 'reset' , d, true),
+                type:'DELETE',
+                success: function(res) {
+                  if (res.success == 1) {
+                    alertify.success("{{ _('" + res.info + "') }}");
+                    t.removeIcon(i);
+                    data.icon = 'icon-table';
+                    t.addIcon(i, {icon: data.icon});
                     t.unload(i);
+                    t.setInode(i);
+                    t.deselect(i);
+                    // Fetch updated data from server
+                    setTimeout(function() {
+                      t.select(i);
+                    }, 10);
                   }
-                });
-              }
-            },
-            function() {}
-          );
+                },
+                error: function(xhr, status, error) {
+                  try {
+                    var err = $.parseJSON(xhr.responseText);
+                    if (err.success == 0) {
+                      msg = S('{{ _(' + err.errormsg + ')}}').value();
+                      alertify.error("{{ _('" + err.errormsg + "') }}");
+                    }
+                  } catch (e) {}
+                  t.unload(i);
+                }
+              });
+            }
+         });
        }
       },
       model: pgBrowser.Node.Model.extend({
diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py
index f18d2c18..fed26a0f 100644
--- a/web/pgadmin/utils/route.py
+++ b/web/pgadmin/utils/route.py
@@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta):
         ABCMeta.__init__(cls, name, bases, d)
 
     @classmethod
 
         cls.registry = dict()
 
+        all_modules = []
+
+        for pkg in pkgs:

---

From: George Gelashvili <[email-masked]>
Date: Thu, 26 Jan 2017 17:40:26 -0500

instead of that patch, please use this no-zombies version that kills the
started process group instead of pid-only.

On Wed, Jan 25, 2017 at 6:31 PM, George Gelashvili <[email-masked]>
wrote:

>> >> >> you. I think the patch we attached didn't spin up its own pgAdmin
>> yet
>> >> >> and it
>> >> >> > here's the patch we forgot to attach. Also, you can see work on
>> our
>> >> >> > branch
>> >> >> > at:
>> >> >> >
>> >> >> >
diff --git a/requirements_py2.txt b/requirements_py2.txt
index 51170a45..de167121 100644
--- a/requirements_py2.txt
+++ b/requirements_py2.txt
@@ -36,6 +36,7 @@ testscenarios==0.5.0
 testtools==2.0.0
 traceback2==1.4.0
 unittest2==1.1.0
+selenium==3.0.2
 Werkzeug==0.9.6
 WTForms==2.0.2
 sqlparse==0.1.19
diff --git a/requirements_py3.txt b/requirements_py3.txt
index f68db7a8..9565a6e4 100644
--- a/requirements_py3.txt
+++ b/requirements_py3.txt
@@ -35,6 +35,7 @@ testscenarios==0.5.0
 testtools==2.0.0
 traceback2==1.4.0
 unittest2==1.1.0
+selenium==3.0.2
 Werkzeug==0.9.6
 WTForms==2.0.2
 sqlparse==0.1.19
diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/web/pgadmin/acceptance/tests/connect_to_database_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_database_feature_test.py
new file mode 100644
index 00000000..a3baa45c
--- /dev/null
+++ b/web/pgadmin/acceptance/tests/connect_to_database_feature_test.py
@@ -0,0 +1,78 @@
+#############################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2017, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##############################################################
+
+import time
+
+from selenium import webdriver
+from selenium.common.exceptions import NoSuchElementException
+from selenium.webdriver import ActionChains
+
+from pgadmin.utils.route import BaseTestGenerator
+
+import subprocess
+import os
+import signal
+import config as app_config
+from regression import test_utils
+from regression.utils.app_starter import AppStarter
+from regression.utils.pgadmin_page import PgadminPage
+
+
+class ConnectsToDatabaseFeatureTest(BaseTestGenerator):
+    """
+    Tests that a database connection can be created from the UI
+    """
+
+    def setUp(self):
+        if app_config.SERVER_MODE:
+            self.skipTest("Currently, config is set to start pgadmin in server mode. "
+                          "This test doesn't know username and password so doesn't work in server mode")
+
+        self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid,
+                                                stderr=open(os.devnull, 'w'))
+
+        driver = webdriver.Chrome()
+        self.app_starter = AppStarter(driver, app_config)
+        self.page = PgadminPage(driver, app_config)
+
+        test_utils.create_database(self.server, "acceptance_test_db")
+
+        self.app_starter.start_app()
+        self.page.wait_for_app()
+
+
+    def runTest(self):
+        self.assertEqual(app_config.APP_NAME, self.page.driver.title)
+        self.page.wait_for_spinner_to_disappear()
+
+        self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click()
+        self.page.driver.find_element_by_link_text("Object").click()
+        ActionChains(self.page.driver) \
+            .move_to_element(self.page.driver.find_element_by_link_text("Create")) \
+            .perform()
+        self.page.find_by_partial_link_text("Server...").click()
+
+        server_config = self.server
+        self.page.fill_input_by_field_name("name", server_config['name'])
+        self.page.find_by_partial_link_text("Connection").click()
+        self.page.fill_input_by_field_name("host", server_config['host'])
+        self.page.fill_input_by_field_name("port", server_config['port'])
+        self.page.fill_input_by_field_name("username", server_config['username'])
+        self.page.fill_input_by_field_name("password", server_config['db_password'])
+        self.page.find_by_xpath("//button[contains(.,'Save')]").click()
+
+        self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']")
+
+    def tearDown(self):
+        self.page.driver.close()
+        os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM)
+
+    def failureException(self, *args, **kwargs):
+        self.page.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png')
+        return AssertionError(*args, **kwargs)
diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py
index f18d2c18..fed26a0f 100644
--- a/web/pgadmin/utils/route.py
+++ b/web/pgadmin/utils/route.py
@@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta):
         ABCMeta.__init__(cls, name, bases, d)
 
     @classmethod
 
         cls.registry = dict()
 
+        all_modules = []
+
+        for pkg in pkgs:
 
index 1f9f0522..68f36cbc 100644
@@ -134,6 +134,24 @@ def create_database(server, db_name):

+def create_table(server, db_name, table_name):
+    try:
+        connection = get_db_connection(db_name,
+                                       server['username'],
+                                       server['db_password'],
+                                       server['host'],
+                                       server['port'])
+        old_isolation_level = connection.isolation_level
+        connection.set_isolation_level(0)
+        pg_cursor = connection.cursor()
+        pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name)
+        pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name)
+        connection.set_isolation_level(old_isolation_level)
+        connection.commit()
+
+    except Exception:
+        traceback.print_exc(file=sys.stderr)
+
 def drop_database(connection, database_name):
     """This function used to drop the database"""
     if database_name not in ["postgres", "template1", "template0"]:
diff --git a/web/regression/utils/__init__.py b/web/regression/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py
new file mode 100644
index 00000000..c093f046
--- /dev/null
+++ b/web/regression/utils/app_starter.py
@@ -0,0 +1,24 @@
+import os
+import subprocess
+
+import signal
+
+
+class AppStarter:
+
+    def __init__(self, driver, app_config):
+        self.driver = driver
+        self.app_config = app_config
+
+    def start_app(self):
+        self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"],
+                                                shell=False,
+                                                preexec_fn=os.setsid,
+                                                stderr=open(os.devnull, 'w'))
+
+        print("opening browser")
+        self.driver.get("http://" + self.app_config.DEFAULT_SERVER + ":" + str(self.app_config.DEFAULT_SERVER_PORT))
+
+    def stop_app(self):
+        self.driver.close()
+        os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM)
diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py
new file mode 100644
index 00000000..1c8857f2
--- /dev/null
+++ b/web/regression/utils/pgadmin_page.py
@@ -0,0 +1,103 @@
+import time
+from selenium.common.exceptions import NoSuchElementException
+from selenium.webdriver import ActionChains
+
+
+class PgadminPage:
+    def __init__(self, driver, app_config):
+        self.driver = driver
+        self.app_config = app_config
+
+    def add_server(self, server_config):
+        self.wait_for_spinner_to_disappear()
+
+        self.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click()
+        self.driver.find_element_by_link_text("Object").click()
+        ActionChains(self.driver) \
+            .move_to_element(self.driver.find_element_by_link_text("Create")) \
+            .perform()
+        self.find_by_partial_link_text("Server...").click()
+
+        self.fill_input_by_field_name("name", server_config['name'])
+        self.find_by_partial_link_text("Connection").click()
+        self.fill_input_by_field_name("host", server_config['host'])
+        self.fill_input_by_field_name("port", server_config['port'])
+        self.fill_input_by_field_name("username", server_config['username'])
+        self.fill_input_by_field_name("password", server_config['db_password'])
+        self.find_by_xpath("//button[contains(.,'Save')]").click()
+
+        self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']")
+
+    def remove_server(self, server_config):
+        self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']").click()
+        self.find_by_partial_link_text("Object").click()
+        self.find_by_partial_link_text("Delete/Drop").click()
+        time.sleep(0.5)
+        self.find_by_xpath("//button[contains(.,'OK')]").click()
+
+    def toggle_open_tree_item(self, tree_item_text):
+        self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click()
+
+    def find_by_xpath(self, xpath):
+        return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath))
+
+    def find_by_id(self, element_id):
+        return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id))
+
+    def find_by_partial_link_text(self, link_text):
+        return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text))
+
+    def fill_input_by_field_name(self, field_name, field_content):
+        self.find_by_xpath("//input[@name='" + field_name + "']").clear()
+        self.find_by_xpath("//input[@name='" + field_name + "']").send_keys(
+            field_content)
+
+    def fill_codemirror_area_with(self, field_content):
+        self.find_by_xpath(
+            "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click()
+        ActionChains(self.driver).send_keys(field_content).perform()
+
+    def wait_for_element(self, find_method_with_args):
+        def element_if_it_exists():
+            try:
+                element = find_method_with_args()
+                if element.is_displayed() & element.is_enabled():
+                    return element
+            except NoSuchElementException:
+                return False
+
+        return self._wait_for("element to exist", element_if_it_exists)
+
+    def wait_for_spinner_to_disappear(self):
+        def spinner_has_disappeared():
+            try:
+                self.driver.find_element_by_id("pg-spinner")
+                return False
+            except NoSuchElementException:
+                return True
+
+        self._wait_for("spinner to disappear", spinner_has_disappeared)
+
+    def wait_for_app(self):
+        def page_shows_app():
+            if self.driver.title == self.app_config.APP_NAME:
+                return True
+            else:
+                self.driver.refresh()
+                return False
+
+        self._wait_for("app to start", page_shows_app)
+
+    def _wait_for(self, waiting_for_message, condition_met_function):
+        timeout = 5
+        time_waited = 0
+        sleep_time = 0.01
+
+        while time_waited < timeout:
+            result = condition_met_function()
+            if result:
+                return result
+            time_waited += sleep_time
+            time.sleep(sleep_time)
+
+        raise RuntimeError("timed out waiting for " + waiting_for_message)

---

From: Dave Page <[email-masked]>
Date: Fri, 27 Jan 2017 16:11:12 +0000

> started process group instead of pid-only.

Very cool :-). The only minor annoyance for me is that my Mac pops up
a message asking me if I want pgAdmin to accept connections, but
there's nothing you can do about that of course.

At this point I think there are a couple of things left to do;

- Add more tests!

- Add command line options to runtests.py to allow users to run either
the existing tests or the acceptance tests (or both, which should be
the default). Of course, it should still be possible to just run any
single test.

Thanks!

---

From: Dave Page <[email-masked]>
Date: Fri, 27 Jan 2017 16:28:41 +0000

> the default). Of course, it should still be possible to just run any

Please add:

- Proper cleanup. I just noticed the tests have left an
"acceptable_test_db" database behind.

Thanks.

---

From: George Gelashvili <[email-masked]>
Date: Mon, 30 Jan 2017 14:28:19 -0500

so, it sounds like you're saying our accaptable_test_db is unacceptable :-P

here's a patch that takes an "--exclude" flag (see README) and doesn't
create dbs that don't get cleaned up afterwards

diff --git a/requirements_py2.txt b/requirements_py2.txt
index 4fb05891..998cdabf 100644
--- a/requirements_py2.txt
+++ b/requirements_py2.txt
@@ -36,6 +36,7 @@ testscenarios==0.5.0
 testtools==2.0.0
 traceback2==1.4.0
 unittest2==1.1.0
+selenium==3.0.2
 Werkzeug==0.9.6
 WTForms==2.0.2
 sqlparse==0.1.19
diff --git a/requirements_py3.txt b/requirements_py3.txt
index c4490f52..2239de63 100644
--- a/requirements_py3.txt
+++ b/requirements_py3.txt
@@ -35,6 +35,7 @@ testscenarios==0.5.0
 testtools==2.0.0
 traceback2==1.4.0
 unittest2==1.1.0
+selenium==3.0.2
 Werkzeug==0.9.6
 WTForms==2.0.2
 sqlparse==0.1.19
diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py
new file mode 100644
index 00000000..47d077d5
--- /dev/null
+++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py
@@ -0,0 +1,72 @@
+#############################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2017, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##############################################################
+
+import time
+
+from selenium import webdriver
+from selenium.common.exceptions import NoSuchElementException
+from selenium.webdriver import ActionChains
+
+from pgadmin.utils.route import BaseTestGenerator
+
+import subprocess
+import os
+import signal
+import config as app_config
+from regression import test_utils
+from regression.utils.app_starter import AppStarter
+from regression.utils.pgadmin_page import PgadminPage
+
+
+class ConnectsToServerFeatureTest(BaseTestGenerator):
+    """
+    Tests that a database connection can be created from the UI
+    """
+
+    def setUp(self):
+        if app_config.SERVER_MODE:
+            self.skipTest("Currently, config is set to start pgadmin in server mode. "
+                          "This test doesn't know username and password so doesn't work in server mode")
+
+        driver = webdriver.Chrome()
+        self.app_starter = AppStarter(driver, app_config)
+        self.page = PgadminPage(driver, app_config)
+
+        self.app_starter.start_app()
+        self.page.wait_for_app()
+
+
+    def runTest(self):
+        self.assertEqual(app_config.APP_NAME, self.page.driver.title)
+        self.page.wait_for_spinner_to_disappear()
+
+        self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click()
+        self.page.driver.find_element_by_link_text("Object").click()
+        ActionChains(self.page.driver) \
+            .move_to_element(self.page.driver.find_element_by_link_text("Create")) \
+            .perform()
+        self.page.find_by_partial_link_text("Server...").click()
+
+        server_config = self.server
+        self.page.fill_input_by_field_name("name", server_config['name'])
+        self.page.find_by_partial_link_text("Connection").click()
+        self.page.fill_input_by_field_name("host", server_config['host'])
+        self.page.fill_input_by_field_name("port", server_config['port'])
+        self.page.fill_input_by_field_name("username", server_config['username'])
+        self.page.fill_input_by_field_name("password", server_config['db_password'])
+        self.page.find_by_xpath("//button[contains(.,'Save')]").click()
+
+        self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']")
+
+    def tearDown(self):
+        self.app_starter.stop_app()
+
+    def failureException(self, *args, **kwargs):
+        self.page.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png')
+        return AssertionError(*args, **kwargs)
diff --git a/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py
new file mode 100644
index 00000000..807dff5a
--- /dev/null
+++ b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py
@@ -0,0 +1,68 @@
+import time
+from selenium import webdriver
+from selenium.webdriver import ActionChains
+
+import config as app_config
+from pgadmin.utils.route import BaseTestGenerator
+from regression import test_utils
+from regression.utils.app_starter import AppStarter
+from regression.utils.pgadmin_page import PgadminPage
+
+
+class SQLTemplateSelectionByPostgresVersionWorksFeatureTest(BaseTestGenerator):
+    def setUp(self):
+        if app_config.SERVER_MODE:
+            self.skipTest("Currently, config is set to start pgadmin in server mode. "
+                          "This test doesn't know username and password so doesn't work in server mode")
+
+        driver = webdriver.Chrome()
+        self.app_starter = AppStarter(driver, app_config)
+        self.page = PgadminPage(driver, app_config)
+
+        test_utils.create_database(self.server, "acceptance_test_db")
+
+        self.app_starter.start_app()
+        self.page.wait_for_app()
+
+        self.page.add_server(self.server)
+
+    def runTest(self):
+        test_utils.create_table(self.server, "acceptance_test_db", "test_table")
+
+        self.page.toggle_open_tree_item(self.server['name'])
+        self.page.toggle_open_tree_item('Databases')
+        self.page.toggle_open_tree_item('acceptance_test_db')
+        self.page.toggle_open_tree_item('Schemas')
+        self.page.toggle_open_tree_item('public')
+        self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click()
+        self.page.find_by_partial_link_text("Object").click()
+        ActionChains(self.page.driver) \
+            .move_to_element(self.page.driver.find_element_by_link_text("Create")) \
+            .perform()
+        self.page.find_by_partial_link_text("Trigger function...").click()
+        self.page.fill_input_by_field_name("name", "test-trigger-function")
+        self.page.find_by_partial_link_text("Definition").click()
+        self.page.fill_codemirror_area_with(
+"""CREATE OR REPLACE FUNCTION log_last_name_changes()
+RETURNS TRIGGER AS
+$BODY$
+BEGIN
+
+END;
+$BODY$
+"""
+        )
+        self.page.find_by_partial_link_text("SQL").click()
+
+        self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]")
+
+    def tearDown(self):
+        self.page.find_by_xpath("//button[contains(.,'Cancel')]").click()
+        self.page.remove_server(self.server)
+        connection = test_utils.get_db_connection(self.server['db'],
+                                                  self.server['username'],
+                                                  self.server['db_password'],
+                                                  self.server['host'],
+                                                  self.server['port'])
+        test_utils.drop_database(connection, "acceptance_test_db")
+        self.app_starter.stop_app()
diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py
index f18d2c18..996892a6 100644
--- a/web/pgadmin/utils/route.py
+++ b/web/pgadmin/utils/route.py
@@ -54,27 +54,23 @@ class TestsGeneratorRegistry(ABCMeta):
         ABCMeta.__init__(cls, name, bases, d)
 
     @classmethod
-    def load_generators(cls, pkg):
+    def load_generators(cls, pkg_root, exclude_pkgs):
 
         cls.registry = dict()
 
+        all_modules = []
+
+        all_modules += find_modules(pkg_root, False, True)
+
         # Check for SERVER mode
-        if config.SERVER_MODE:
-            for module_name in find_modules(pkg, False, True):
-                try:
-                    if "tests." in str(module_name):
-                        import_module(module_name)
-                except ImportError:
-                    traceback.print_exc(file=sys.stderr)
-        else:
-            for module_name in find_modules(pkg, False, True):
-                try:
-                    # Exclude the test cases in browser node if SERVER_MODE
-                    # is False
-                    if "pgadmin.browser.tests" not in module_name:
-                        import_module(module_name)
-                except ImportError:
-                    traceback.print_exc(file=sys.stderr)
+        for module_name in all_modules:
+            try:
+                if "tests." in str(module_name) and not any(
+                        str(module_name).startswith('pgadmin.' + str(exclude_pkg)) for exclude_pkg in exclude_pkgs
+                ):
+                    import_module(module_name)
+            except ImportError:
+                traceback.print_exc(file=sys.stderr)

 import six
index 8cc29987..5b077d81 100644
 
@@ -122,3 +126,8 @@ Execution:
 
      Example 2) Run test framework for 'database' node
      run 'python runtests.py --pkg browser.server_groups.servers.databases'
+
+- Exclude a package and its subpackages when running tests:
+
+    Example: exclude acceptance tests but run all others:
+    run 'python runtests.py --exclude acceptance'
diff --git a/web/regression/runtests.py b/web/regression/runtests.py
index 8d2a886a..cd372b4e 100644
--- a/web/regression/runtests.py
+++ b/web/regression/runtests.py
@@ -133,12 +133,20 @@ def get_test_modules(arguments):
 
     from pgadmin.utils.route import TestsGeneratorRegistry
 
+    exclude_pkgs = []
+
+    if not config.SERVER_MODE:
+        exclude_pkgs.append("browser.tests")
+    if 'exclude' in arguments:
+        exclude_pkgs.append(arguments['exclude'])
+
     # Load the test modules which are in given package(i.e. in arguments.pkg)
     if arguments['pkg'] is None or arguments['pkg'] == "all":
-        TestsGeneratorRegistry.load_generators('pgadmin')
+        TestsGeneratorRegistry.load_generators('pgadmin', exclude_pkgs)
     else:
         TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' %
-                                               arguments['pkg'])
+                                               arguments['pkg'],
+                                               exclude_pkgs)
 
     # Sort module list so that test suite executes the test cases sequentially
     module_list = TestsGeneratorRegistry.registry.items()
@@ -159,6 +167,8 @@ def add_arguments():
     parser = argparse.ArgumentParser(description='Test suite for pgAdmin4')
     parser.add_argument('--pkg', help='Executes the test cases of particular'
                                       ' package')
+    parser.add_argument('--exclude', help='Skips execution of the test '
+                                          'cases of particular package')
     arg = parser.parse_args()
 
     return arg

index 00000000..fffd9526
@@ -0,0 +1,27 @@
+    Helper for starting the full pgadmin4 app and loading the page via selenium
index 00000000..dce257c5
@@ -0,0 +1,106 @@
+    Helper class for interacting with the page, given a selenium driver

---

From: Atira Odhner <[email-masked]>
Date: Mon, 30 Jan 2017 16:23:15 -0500

Here's the patch with one more fix -- cleaning up the connections that get
created in pgAdmin.

index 00000000..08e92154
@@ -0,0 +1,73 @@
index 00000000..ed6c0d6b
 
     @classmethod
 
         cls.registry = dict()
 
+        all_modules = []
+

 import six

 
 
+    exclude_pkgs = []
+

     return arg

index 00000000..4e334e80
+        self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click()
+        self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click()

---

From: Dave Page <[email-masked]>
Date: Tue, 31 Jan 2017 14:41:09 +0000

Hi

> Here's the patch with one more fix -- cleaning up the connections that get

Hmm, I had trouble with this one. I noticed a few issues:

- The tests started pgAdmin listening on the default port (5050),
however, I already had an instance running on there;
    a) It should have detected that something else was running on the port
    b) Shouldn't we just use a random, unused port?

- Errors were given because I already had an acceptance_test_db on a
number of servers, and that contained the test table. Obviously the
code now cleans up after itself, but I think we should use a random
database name as the main regression tests do (they append a random
number to the name iirc).

- Some of the tests just seemed to time out. I *think* this might be
because the test browser window opens quite narrowly, and it looks
like the tests are probably trying to do things with nodes that aren't
actually visible.

======================================================================
ERROR: runTest (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py",
line 69, in tearDown
    self.app_starter.stop_app()
  File "/Users/dpage/git/pgadmin4/web/regression/utils/app_starter.py",
line 27, in stop_app
    os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM)
OSError: [Errno 3] No such process

======================================================================
ERROR: runTest (pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py",
line 37, in runTest
    self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText'
and .='Trigger Functions']").click()
  File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py",
line 45, in find_by_xpath
    return self.wait_for_element(lambda:
self.driver.find_element_by_xpath(xpath))
  File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py",
line 72, in wait_for_element
    return self._wait_for("element to exist", element_if_it_exists)
  File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py",
line 106, in _wait_for
    raise RuntimeError("timed out waiting for " + waiting_for_message)
RuntimeError: timed out waiting for element to exist

======================================================================
ERROR: runTest (pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py",
line 60, in tearDown
    self.page.find_by_xpath("//button[contains(.,'Cancel')]").click()
  File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py",
line 45, in find_by_xpath
    return self.wait_for_element(lambda:
self.driver.find_element_by_xpath(xpath))
  File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py",
line 72, in wait_for_element
    return self._wait_for("element to exist", element_if_it_exists)
  File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py",
line 106, in _wait_for
    raise RuntimeError("timed out waiting for " + waiting_for_message)
RuntimeError: timed out waiting for element to exist

Thanks.

---

From: George Gelashvili <[email-masked]>
Date: Tue, 31 Jan 2017 09:54:27 -0500

Hi Dave,

We agree that a random port would be a nice addition. We think having
randomized test database names can lead to polluting with lots of extra
databases left around in the event that cleanup fails for whatever reason
(e.g. a test errors out).  We see this happen already with the randomized
test databases you mention. We agree that there should probably be one
strategy across the test suite. We could use randomized names and have a
more general cleanup step that removes all databases of the form "test_...".

Dave, are those errors you saw when you shut down your application on :5050
and did a fresh run of the tests? If not, could you please do a clean run?
It's possible that the second error could be related to viewport size as
you suggested, but the first error just looks like a problem with the test
not being able to spin up its own server.

Thanks,
George & Tira

On Tue, Jan 31, 2017 at 9:41 AM, Dave Page <[email-masked]> wrote:

> ERROR: runTest (pgadmin.acceptance.tests.sql_template_selection_by_
> postgres_version_works_feature_test.SQLTemplateSelectionByPostgres
> VersionWorksFeatureTest)
>   File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/
> sql_template_selection_by_postgres_version_works_feature_test.py",
> line 37, in runTest
> ERROR: runTest (pgadmin.acceptance.tests.sql_template_selection_by_
> postgres_version_works_feature_test.SQLTemplateSelectionByPostgres
> VersionWorksFeatureTest)
>   File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/
> sql_template_selection_by_postgres_version_works_feature_test.py",
> line 60, in tearDown

---

From: Dave Page <[email-masked]>
Date: Tue, 31 Jan 2017 15:10:04 +0000

Hi

> more general cleanup step that removes all databases of the form "test_...".

I'm very wary about doing things like that. We had an early version of
the suite that managed to delete all databases :-/. Maybe we could use
a patterned name, but only delete databases that also have a comment
with some text in it that we can verify?

> It's possible that the second error could be related to viewport size as you
> suggested, but the first error just looks like a problem with the test not
> being able to spin up its own server.

That was on a second run of the tests, yes. I just did a careful
cleanup of left-over test databases, double-checked my server wasn't
running and re-ran the tests - I got the same results.

>> ERROR: runTest
>> (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest)
>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py",
>> line 69, in tearDown
>> ERROR: runTest
>> (pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest)
>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py",
>> line 37, in runTest
>> ERROR: runTest
>> (pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest)
>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py",
>> line 60, in tearDown

---

From: Dave Page <[email-masked]>
Date: Tue, 31 Jan 2017 16:25:47 +0000

Hi George,

I just tried to do some debugging of pgAdmin, and found that I
couldn't start it. On further investigation, I found that I had an
instance running in the background on my system. I'm assuming this was
started by the acceptance tests, but not shutdown. I killed it off,
and re-ran the tests only to see failures because the database and
table used in the acceptance tests were still present. When the tests
completed, pgAdmin was again left running in the background.

I've just re-run the tests, having first killed the backgrounded
pgAdmin and then manually cleaned up the test objects. This time I do
indeed only get the two errors below when it tests the first of 3
servers I have configured. The second and third servers get three
errors each, and pgAdmin is left running in the background again.

So, you were right that I had another instance of pgAdmin running...
but it was tests that caused it :-p

---

From: Atira Odhner <[email-masked]>
Date: Fri, 3 Feb 2017 16:56:02 -0500

Hi Dave,

Here is a new patch which includes the following:
- randomized ports
- delete the acceptance_test_db database in setup in case a prior run failed
- fixed size browser window

Cheers,
Tira & George

On Tue, Jan 31, 2017 at 11:25 AM, Dave Page <[email-masked]> wrote:

> >>> > Here's the patch with one more fix -- cleaning up the connections
> that
> >>> > get
> >>> > created in pgAdmin.
> >>>
> >>> (pgadmin.acceptance.tests.sql_template_selection_by_
> >>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/
> >>> (pgadmin.acceptance.tests.sql_template_selection_by_
> >>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/
diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py
index 68848c00..95e675b7 100644
--- a/web/pgAdmin4.py
+++ b/web/pgAdmin4.py
@@ -59,6 +59,12 @@ if 'PGADMIN_PORT' in globals():
                      globals()['PGADMIN_PORT'])
     server_port = int(globals()['PGADMIN_PORT'])
     PGADMIN_RUNTIME = True
+elif 'PGADMIN_PORT' in os.environ:
+    port = os.environ['PGADMIN_PORT']
+    app.logger.debug(
+        'Not running under the desktop runtime, port: %s',
+        port)
+    server_port = int(port)
     app.logger.debug(
         'Not running under the desktop runtime, port: %s',
index 00000000..b54696f6
+        self.page.driver.save_screenshot('/tmp/pgadmin_connect_to_server_test_failure.png')
index 00000000..65b96eae
+        self.page.driver.save_screenshot('/tmp/pgadmin_sql_template_selection_failure.png')
 
     @classmethod
 
         cls.registry = dict()
 
+        all_modules = []
+

 import six

index 272e3802..7b1bf543 100644
@@ -138,12 +138,20 @@ def get_test_modules(arguments):

+    exclude_pkgs = []
+
 
@@ -164,6 +172,8 @@ def add_arguments():
 
     return arg

index 00000000..b297bd6d
@@ -0,0 +1,34 @@
+import random
+        random_server_port = str(random.randint(10000, 65535))
+        env = {"PGADMIN_PORT": random_server_port}
+        env.update(os.environ)
+        self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py", "magic-portal", random_server_port],
+                                                stderr=open(os.devnull, 'w'),
+                                                env=env)
+        self.driver.set_window_size(1024, 1024)
+        self.driver.get("http://" + self.app_config.DEFAULT_SERVER + ":" + random_server_port)

---

From: Dave Page <[email-masked]>
Date: Mon, 6 Feb 2017 10:32:18 +0000

Hi

> - fixed size browser window

Definitely getting there :-). A couple of thoughts/questions:

- Now there are 2 tests in there, it's clear that both the Python
server and browser session are restarted for each test. Can this be
avoided? It'll really slow down test execution as more and more are
added.

- We've got a new monster name:
pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest
(which on disk is
sql_template_select_by_postgres_version_works_feature_test.py). Names
like that really must be shortened to something more sane and
manageable.

- I'm a little confused by why the tests cannot be run in server mode.
The error says it's because the username/password is unknown -
however, both the pgAdmin and database server usernames and passwords
are in test_config.json.

Thanks!

---

From: Atira Odhner <[email-masked]>
Date: Mon, 6 Feb 2017 09:54:52 -0500

I agree that we should rename the test. We've renamed it to
"template_selection_feature_test".
Your other suggestions are captured in our backlog as future improvements.
We definitely can and should do those things but I think it would be
valuable to go ahead and get this suite in and give other devs a chance to
use and iterate on this work.

Thanks,

Tira & George

On Mon, Feb 6, 2017 at 5:32 AM, Dave Page <[email-masked]> wrote:

> pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_
> feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest
> (which on disk is
diff --git a/web/pgadmin/acceptance/tests/template_selection_feature_test.py b/web/pgadmin/acceptance/tests/template_selection_feature_test.py
index 00000000..b7405d56
+++ b/web/pgadmin/acceptance/tests/template_selection_feature_test.py
+class TemplateSelectionFeatureTest(BaseTestGenerator):
 
     @classmethod
 
         cls.registry = dict()
 
+        all_modules = []
+

 import six

 
 
+    exclude_pkgs = []
+

     return arg

---

From: Atira Odhner <[email-masked]>
Date: Wed, 8 Feb 2017 17:15:58 -0500

Hey Dave,

We re-used one of the test helpers for the 'fix-greenplum-show-tables.diff'
patch, so here is an updated patch which does not include adding that test
helper in case you apply the show-tables patch first. Also, we saw some
strange test behavior yesterday where form fields weren't being filled in
correctly so we changed the way that input fields get filled to be more
reliable.

In short these need to be applied in this order:

> git apply fix-greenplum-show-tables.diff

git apply acceptance-tests-minus-create-table-helper-with-fixed-inputs.diff

We also moved the --exclude flag changes out to a separate patch.

On our side we are still dealing with these as 20 separate commits. What is
the best way for us to send you these patches? Do you prefer having them
all squashed down to a single patch or to have smaller patches?

On Mon, Feb 6, 2017 at 9:54 AM, Atira Odhner <[email-masked]> wrote:

>> pgadmin.acceptance.tests.sql_template_selection_by_postgres_
>> version_works_feature_test.SQLTemplateSelectionByPostgresVer
>> sionWorksFeatureTest
>> (which on disk is
commit f340178c3f2d779ea5368f48a36f47112e2fa316
Author: George Gelashvili and Tira Odhner <[email-masked]>
Date:   Wed Feb 8 14:52:32 2017 -0500

    ACCEPTANCE TEST SQUASH
    
    Based on Work around input validation by sending backspace to clear

index 00000000..c3bee4e6
@@ -0,0 +1,92 @@
+        self._connects_to_server()
+        self._tables_node_expandable()
+    def _connects_to_server(self):
+    def _tables_node_expandable(self):
+        self.page.toggle_open_tree_item('Tables')
+        self.page.toggle_open_tree_item('test_table')
 
index 00000000..d6a5836c
@@ -0,0 +1,120 @@
+from selenium.webdriver.common.keys import Keys
+        field = self.find_by_xpath("//input[@name='" + field_name + "']")
+        backspaces = [Keys.BACKSPACE]*len(field.get_attribute('value'))
+        field.click()
+        field.send_keys(backspaces)
+        field.send_keys(str(field_content))
+        self.wait_for_input_field_content(field_name, field_content)
+    def wait_for_input_field_content(self, field_name, content):
+        def input_field_has_content():
+            element = self.driver.find_element_by_xpath(
+                "//input[@name='" + field_name + "']")
+            return str(content) == element.get_attribute('value')
+        return self._wait_for("field to contain '" + str(content) + "'", input_field_has_content)
+        timeout = 10

---

From: Dave Page <[email-masked]>
Date: Thu, 9 Feb 2017 12:47:17 +0000

Hi

I get the following crash when running with Python 3.4 or 3.5:

(pgadmin4-py34) piranha:pgadmin4 dpage$ python web/regression/runtests.py
pgAdmin 4 - Application Initialisation
======================================

The configuration database - '/Users/dpage/.pgadmin/test_pgadmin4.db'
does not exist.
Entering initial setup mode...
NOTE: Configuring authentication for DESKTOP mode.

The configuration database has been created at
/Users/dpage/.pgadmin/test_pgadmin4.db

=============Running the test cases for 'Regression - PG 9.4'=============
runTest (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest)
... Traceback (most recent call last):
  File "web/regression/runtests.py", line 276, in <module>
    verbosity=2).run(suite)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/runner.py",
line 168, in run
    test(result)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py",
line 84, in __call__
    return self.run(*args, **kwds)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py",
line 122, in run
    test(result)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py",
line 628, in __call__
    return self.run(*args, **kwds)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py",
line 588, in run
    self._feedErrorsToResult(result, outcome.errors)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py",
line 515, in _feedErrorsToResult
    if issubclass(exc_info[0], self.failureException):
TypeError: issubclass() arg 2 must be a class or tuple of classes

With Python 2.7, it initially opens Chrome with the URL "data:,"
(without the quotes), and then spits out:

======================================================================
ERROR: runTest (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py",
line 41, in setUp
    test_utils.create_table(self.server, "acceptance_test_db", "test_table")
AttributeError: 'module' object has no attribute 'create_table'

======================================================================
ERROR: runTest (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py",
line 36, in runTest
    test_utils.create_table(self.server, "acceptance_test_db", "test_table")
AttributeError: 'module' object has no attribute 'create_table'

======================================================================
ERROR: runTest (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py",
line 66, in tearDown
    self.page.find_by_xpath("//button[contains(.,'Cancel')]").click()
  File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py",
line 46, in find_by_xpath
    return self.wait_for_element(lambda:
self.driver.find_element_by_xpath(xpath))
  File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py",
line 86, in wait_for_element
    return self._wait_for("element to exist", element_if_it_exists)
  File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py",
line 120, in _wait_for
    raise RuntimeError("timed out waiting for " + waiting_for_message)
RuntimeError: timed out waiting for element to exist

Ran 149 tests in 59.258s

FAILED (errors=3, skipped=12)

On Wed, Feb 8, 2017 at 10:15 PM, Atira Odhner <[email-masked]> wrote:
>> git apply
>> acceptance-tests-minus-create-table-helper-with-fixed-inputs.diff
>
>
> the best way for us to send you these patches? Do you prefer having them all
> squashed down to a single patch or to have smaller patches?
>
>
>

---

From: Atira Odhner <[email-masked]>
Date: Thu, 09 Feb 2017 13:15:51 +0000

create_table is the change we pulled into the other patch which would need
to be applied first.

On Thu, Feb 9, 2017, 7:47 AM Dave Page <[email-masked]> wrote:

> runTest
> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/runner.py",
> line 168, in run
>     test(result)
>   File
> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py",
> line 84, in __call__
> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py",
> line 122, in run
>     test(result)
>   File
> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py",
> line 628, in __call__
> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py",
> line 588, in run
> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py",
> (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest)
> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py",
> line 36, in runTest
> (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest)
> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py",
> line 66, in tearDown
> all

---

From: Dave Page <[email-masked]>
Date: Thu, 9 Feb 2017 13:26:44 +0000

OK, well that one was sent back with feedback as well, so please
resubmit when the relevant updates have been made to either and
they've been retested. Given the amount of work you're doing at the
moment, it would be helpful if you could note when one patch is
dependent on another. It's hard to keep track when you're this
productive!

Thanks.

On Thu, Feb 9, 2017 at 1:15 PM, Atira Odhner <[email-masked]> wrote:
>> >>> > - delete the acceptance_test_db database in setup in case a prior
>> >>> > run
>> >>> > failed

---

From: Atira Odhner <[email-masked]>
Date: Thu, 09 Feb 2017 14:20:51 +0000

Certainly.  We did mention the dependency in the email. Would it be better
to mention it in the patch name? Is there a better way for us to manage
these changes? On other open source projects, I've seen github mirrors set
up so that changes can be pulled in like branches rather then as patch
applies. That would have avoided this situation since the parent commit
would be pulled in with the same SHA from either pull request branch and
git would not see it as a conflict.

I'm rather new to dealing with patch files like this so I would love some
tips.

Thanks,
Tira

On Thu, Feb 9, 2017, 8:27 AM Dave Page <[email-masked]> wrote:

> >> > On our side we are still dealing with these as 20 separate commits.
> What
> >> > is
> >> >> valuable to go ahead and get this suite in and give other devs a
> chance
> >> >> to

---

From: Dave Page <[email-masked]>
Date: Thu, 9 Feb 2017 14:28:43 +0000

Hi

On Thu, Feb 9, 2017 at 2:20 PM, Atira Odhner <[email-masked]> wrote:
> to mention it in the patch name?

I think the problem was that the way you phrased it, it sounded
optional ("an updated patch which does not include adding that test
helper in case you apply the show-tables patch first"). I think a
clear "This patch is dependent on patch Foo" would suffice.

> Is there a better way for us to manage
> would be pulled in with the same SHA from either pull request branch and git
> would not see it as a conflict.
>

The Postgres project in general is quite conservative and stuck in
it's ways about how things are done (which is usually a good thing
considering you trust your data to the resulting code). We're used to
dealing with larger patchsets via the mailing list - typically as long
as you're clear about any dependencies, it shouldn't be a problem.
Some of us use tools like PyCharms for handling patches and helping
with reviews etc. which I guess replaces most, if not all of the
GitHub functionality over plain git.

---

From: Atira Odhner <[email-masked]>
Date: Thu, 9 Feb 2017 11:17:40 -0500

Hi Dave,

 I think the problem was that the way you phrased it,

You're right, we totally messed that up. We were talking about making 3
patches and ended up making only 2 and forgot to reword that bit.
Sorry about that.

Here are the two patches for this change that resolves the AttributeError you
were seeing. The first patch is identical to the patch of the same name in
the other email thread.

We're used to
> as you're clear about any dependencies, it shouldn't be a problem.

Great! We'll try sending patchsets from now on and hopefully that resolves
some of the issues we were seeing.

Tira & George

---

From: Dave Page <[email-masked]>
Date: Mon, 13 Feb 2017 14:36:49 +0000

Hi,

I've been playing with this for the last couple of hours, and I just
can't get it to work reliably;

- A good percentage of the time the browser opens with a URL of
"data:," and does nothing more. This appears to happen if tests fail,
which still leaves server processes running in the background.

- The connect_to_server test usually seems to work.

- The template_selection_feature test usually does *not* work. I can't
see an obvious reason, but I suspect it's a race condition. What seems
to happen is that the function definition is entered, but not
registered by the UI, so the mSQL panel just ends up saying
"incomplete definition". Manually checking what was input proves that
everything is correct - and indeed, returning the SQL tab shows the
expected SQL.

Other issues I noted:

- The template_selection_feature test should just enter BEGIN/END.
What it currently enters is an entire function definition, when only
the body content is expected. E.g.

        self.page.fill_codemirror_area_with(
"""BEGIN

END;
"""
        )

- Screenshots are being taken of failed tests:
  1) I've never actually seen any get saved
  2) They should be saved to the same directory as the test log, not /tmp
  3) They should have guaranteed unique names, and be mentioned in the
test output so the user can reference the image to the failure.

The reason the last two items are important is that I've now got a
test server running the test suite with every supported version of
Python, for every supported database (well, almost, pending a couple
of fixes). I have separate workspaces for each Python version, and a
single test run might run every test 10 times, once for each database
server.

- Please wrap the README at < 80 chars.

On Thu, Feb 9, 2017 at 4:17 PM, Atira Odhner <[email-masked]> wrote:
> Here are the two patches for this change that resolves the AttributeError
> you were seeing. The first patch is identical to the patch of the same name
> in the other email thread.
>
>> We're used to

---

From: Atira Odhner <[email-masked]>
Date: Tue, 21 Feb 2017 17:12:46 -0500

Hi Dave,

We fixed the flakiness issues that we saw (hopefully they are the same ones
you were seeing.) by tearing down connections to the acceptance_test_db
before attempting to drop it at the beginning of the test. Once we have
access to the CI pipeline we can help out there to ensure the flakiness is
gone.

We wrapped the README at 80 characters, and removed the misleading function
definition from the test.

As far as the screenshots go, I'm more inclined to remove the
screenshotting than to work on improving it. It currently only works when
the failure is due to an AssertionError since that's what failureException
relies on.

We also renamed acceptance to feature_tests since 'acceptance' seemed
ambiguous/redundant with 'regression'.

Tira & Sara

---

From: Dave Page <[email-masked]>
Date: Wed, 22 Feb 2017 12:42:28 +0000

Thanks, patch applied!

On Tue, Feb 21, 2017 at 10:12 PM, Atira Odhner <[email-masked]> wrote:
> As far as the screenshots go, I'm more inclined to remove the screenshotting
> than to work on improving it. It currently only works when the failure is
> due to an AssertionError since that's what failureException relies on.
>
>> >> > these changes? On other open source projects, I've seen github
>> >> > mirrors
>> >> > set