Skip to content

Commit

Permalink
[fix] Fixed unsaved changes alert displaying wihtout changes openwisp…
Browse files Browse the repository at this point in the history
…#481

Bug:
Configuration variables for templates were not being fetched
because "owcInitialValuesLoaded" was getting triggered before
event listener was registering.

Fix:
Check is "owcInitialValuesLoaded" was already fired. If so, fetch
the configuration variables for templates. Otherwise, register
the event listener.

Added also browser based tests for this bug.

Closes openwisp#481
  • Loading branch information
pandafy committed Jun 8, 2021
1 parent fd0c883 commit f16e74d
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 3 deletions.
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,16 @@ jobs:
- name: Install system packages
run: |
sudo apt update
sudo apt-get -qq -y install sqlite3 gdal-bin libproj-dev libgeos-dev libspatialite-dev spatialite-bin libsqlite3-mod-spatialite
sudo apt-get -qq -y install sqlite3 gdal-bin libproj-dev \
libgeos-dev libspatialite-dev spatialite-bin \
libsqlite3-mod-spatialite
- name: Install Chromium for Selenium testing
run: |
sudo snap install chromium
wget https://chromedriver.storage.googleapis.com/91.0.4472.19/chromedriver_linux64.zip
sudo unzip chromedriver_linux64.zip -d /usr/local/bin/
sudo chmod +x /usr/local/bin/chromedriver
- name: Install npm dependencies
run: sudo npm install -g jshint stylelint
Expand All @@ -70,6 +79,8 @@ jobs:
coverage run --source=openwisp_controller runtests.py
# SAMPLE tests do not influence coverage, so we can speed up tests with --parallel
SAMPLE_APP=1 ./runtests.py --keepdb
env:
SELENIUM_HEADLESS: 1

- name: Upload Coverage
run: coveralls --service=github
Expand Down
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ Install the system dependencies:
sudo apt install -y sqlite3 libsqlite3-dev openssl libssl-dev
sudo apt install -y gdal-bin libproj-dev libgeos-dev libspatialite-dev libsqlite3-mod-spatialite
sudo snap install -y chromium
Fork and clone the forked repository:

Expand Down Expand Up @@ -959,6 +960,9 @@ Install development dependencies:
pip install -r requirements-test.txt
npm install -g jslint stylelint
Install WebDriver for Chromium for your browser version from `<https://chromedriver.chromium.org/home>`_
and Extract ``chromedriver`` to one of directories from your ``$PATH`` (example: ``~/.local/bin/``).

Create database:

.. code-block:: shell
Expand Down
10 changes: 8 additions & 2 deletions openwisp_controller/config/static/config/js/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,9 +401,15 @@
// otherwise load immediately
bindLoadUi();
// fill device context field with default values of selected templates.
$(document).one('owcInitialValuesLoaded', function () {
// If unsaved_changes have already mapped values, then fetch defaultValues,
// otherwise wait for event to be triggered.
if (django._owcInitialValues !== undefined){
getDefaultValues(true);
});
} else {
$(document).one('owcInitialValuesLoaded', function () {
getDefaultValues(true);
});
}
$('.sortedm2m-items').on('change', function() {
getDefaultValues();
});
Expand Down
253 changes: 253 additions & 0 deletions openwisp_controller/config/tests/test_selenium.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
from django.conf import settings
from django.urls.base import reverse
from selenium import webdriver
from selenium.common.exceptions import (
StaleElementReferenceException,
TimeoutException,
UnexpectedAlertPresentException,
)
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select, WebDriverWait

from openwisp_users.tests.utils import TestOrganizationMixin

from .utils import CreateConfigTemplateMixin, SeleniumTestCase


class TestDeviceAdmin(
TestOrganizationMixin, CreateConfigTemplateMixin, SeleniumTestCase
):
admin_username = 'admin'
admin_password = 'password'

@classmethod
def setUpClass(cls):
super().setUpClass()
chrome_options = webdriver.ChromeOptions()
if getattr(settings, 'SELENIUM_HEADLESS', True):
chrome_options.add_argument('--headless')
chrome_options.add_argument('--window-size=1366,768')
chrome_options.add_argument('--ignore-certificate-errors')
chrome_options.add_argument('--remote-debugging-port=9222')
capabilities = DesiredCapabilities.CHROME
capabilities['goog:loggingPrefs'] = {'browser': 'ALL'}
cls.web_driver = webdriver.Chrome(
options=chrome_options, desired_capabilities=capabilities
)

@classmethod
def tearDownClass(cls):
cls.web_driver.quit()
super().tearDownClass()

def setUp(self):
self.admin = self._create_admin(
username=self.admin_username, password=self.admin_password
)

def tearDown(self):
# Accept unsaved changes alert to allow other tests to run
try:
self.web_driver.refresh()
except UnexpectedAlertPresentException:
self.web_driver.switch_to_alert().accept()
else:
try:
WebDriverWait(self.web_driver, 1).until(EC.alert_is_present())
except TimeoutException:
pass
else:
self.web_driver.switch_to_alert().accept()
self.web_driver.refresh()
WebDriverWait(self.web_driver, 2).until(
EC.visibility_of_element_located((By.XPATH, '//*[@id="site-name"]'))
)

def test_create_new_device(self):
required_template = self._create_template(name='Required', required=True)
default_template = self._create_template(name='Default', default=True)
org = self._get_org()
self.login()
self.open(reverse('admin:config_device_add'))
self.web_driver.find_element_by_name('name').send_keys('11:22:33:44:55:66')
Select(self.web_driver.find_element_by_name('organization')).select_by_value(
str(org.id)
)
self.web_driver.find_element_by_name('mac_address').send_keys(
'11:22:33:44:55:66'
)
self.web_driver.find_element_by_xpath(
'//*[@id="config-group"]/fieldset/div[2]/a'
).click()

try:
WebDriverWait(self.web_driver, 2).until(
EC.element_to_be_clickable(
(By.XPATH, f'//*[@value="{default_template.id}"]')
)
)
except TimeoutException:
self.fail('Default template clickable timed out')

required_template_element = self.web_driver.find_element_by_xpath(
f'//*[@value="{required_template.id}"]'
)
default_template_element = self.web_driver.find_element_by_xpath(
f'//*[@value="{default_template.id}"]'
)
self.assertEqual(required_template_element.is_enabled(), False)
self.assertEqual(required_template_element.is_selected(), True)
self.assertEqual(default_template_element.is_enabled(), True)
self.assertEqual(default_template_element.is_selected(), True)

self.web_driver.find_element_by_name('_save').click()
self.assertEqual(
self.web_driver.find_elements_by_class_name('success')[0].text,
'The Device “11:22:33:44:55:66” was added successfully.',
)

def test_unsaved_changes(self):
self.login()
device = self._create_config(organization=self._get_org()).device
self.open(reverse('admin:config_device_change', args=[device.id]))
with self.subTest('Alert should not be displayed without any change'):
self.web_driver.refresh()
try:
WebDriverWait(self.web_driver, 1).until(EC.alert_is_present())
except TimeoutException:
pass
else:
self.fail('Unsaved changes alert displayed without any change')

with self.subTest('Alert should be displayed after making changes'):
self.web_driver.find_element_by_name('name').send_keys('new.device.name')
self.web_driver.refresh()
try:
WebDriverWait(self.web_driver, 1).until(EC.alert_is_present())
except TimeoutException:
self.fail('Timed out wating for unsaved changes alert')
else:
self.web_driver.switch_to_alert().accept()

def test_multiple_organization_templates(self):
shared_required_template = self._create_template(
name='shared required', organization=None
)

org1 = self._create_org(name='org1', slug='org1')
org1_required_template = self._create_template(
name='org1 required', organization=org1, required=True
)
org1_default_template = self._create_template(
name='org1 default', organization=org1, default=True
)

org2 = self._create_org(name='org2', slug='org2')
org2_required_template = self._create_template(
name='org2 required', organization=org2, required=True
)
org2_default_template = self._create_template(
name='org2 default', organization=org2, default=True
)

org1_device = self._create_config(
device=self._create_device(organization=org1)
).device

self.login()
self.open(
reverse('admin:config_device_change', args=[org1_device.id])
+ '#config-group'
)
wait = WebDriverWait(self.web_driver, 2)
# org2 templates should not be visible
try:
wait.until(
EC.invisibility_of_element_located(
(By.XPATH, f'//*[@value="{org2_required_template.id}"]')
)
)
wait.until(
EC.invisibility_of_element_located(
(By.XPATH, f'//*[@value="{org2_default_template.id}"]')
)
)
except (TimeoutException, StaleElementReferenceException):
self.fail('Template belonging to other organization found')

# org1 and shared templates should be visible
wait.until(
EC.visibility_of_any_elements_located(
(By.XPATH, f'//*[@value="{org1_required_template.id}"]')
)
)
wait.until(
EC.visibility_of_any_elements_located(
(By.XPATH, f'//*[@value="{org1_default_template.id}"]')
)
)
wait.until(
EC.visibility_of_any_elements_located(
(By.XPATH, f'//*[@value="{shared_required_template.id}"]')
)
)

def test_change_config_backend(self):
device = self._create_config(organization=self._get_org()).device
template = self._create_template()

self.login()
self.open(
reverse('admin:config_device_change', args=[device.id]) + '#config-group'
)
self.web_driver.find_element_by_xpath(f'//*[@value="{template.id}"]')
# Change config backed to
config_backend_select = Select(
self.web_driver.find_element_by_name('config-0-backend')
)
config_backend_select.select_by_visible_text('OpenWISP Firmware 1.x')
try:
WebDriverWait(self.web_driver, 1).until(
EC.invisibility_of_element_located(
(By.XPATH, f'//*[@value="{template.id}"]')
)
)
except TimeoutException:
self.fail('Template for other config backend found')

def test_template_context_variables(self):
self._create_template(
name='Template1', default_values={'vni': '1'}, required=True
)
self._create_template(
name='Template2', default_values={'vni': '2'}, required=True
)
device = self._create_config(organization=self._get_org()).device
self.login()
self.open(
reverse('admin:config_device_change', args=[device.id]) + '#config-group'
)
try:
WebDriverWait(self.web_driver, 2).until(
EC.text_to_be_present_in_element_value(
(
By.XPATH,
'//*[@id="flat-json-config-0-context"]/div[2]/div/div/input[1]',
),
'vni',
)
)
except TimeoutException:
self.fail('Timed out wating for configuration variabled to get loaded')
self.web_driver.find_element_by_xpath(
'//*[@id="container"]/div[2]/a[3]'
).click()
try:
WebDriverWait(self.web_driver, 2).until(EC.alert_is_present())
except TimeoutException:
pass
else:
self.web_driver.switch_to_alert().accept()
self.fail('Unsaved changes alert displayed without any change')
39 changes: 39 additions & 0 deletions openwisp_controller/config/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from unittest import mock
from uuid import uuid4

from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from swapper import load_model

from ...pki.tests.utils import TestPkiMixin
Expand Down Expand Up @@ -144,3 +145,41 @@ def _create_config(self, **kwargs):
name='test-device', organization=kwargs.pop('organization')
)
return super()._create_config(**kwargs)


class SeleniumTestCase(StaticLiveServerTestCase):
"""
A base test case for Selenium, providing helped methods for generating
clients and logging in profiles.
"""

def open(self, url, driver=None):
"""
Opens a URL
Argument:
url: URL to open
driver: selenium driver (default: cls.base_driver)
"""
if not driver:
driver = self.web_driver
driver.get(f'{self.live_server_url}{url}')

def login(self, username=None, password=None, driver=None):
"""
Log in to the admin dashboard
Argument:
driver: selenium driver (default: cls.web_driver)
username: username to be used for login (default: cls.admin.username)
password: password to be used for login (default: cls.admin.password)
"""
if not driver:
driver = self.web_driver
if not username:
username = self.admin_username
if not password:
password = self.admin_password
driver.get(f'{self.live_server_url}/admin/login/')
if 'admin/login' in driver.current_url:
driver.find_element_by_name('username').send_keys(username)
driver.find_element_by_name('password').send_keys(password)
driver.find_element_by_xpath('//input[@type="submit"]').click()
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pytest-asyncio~=0.14.0
pytest-cov~=2.10.0
mock-ssh-server>=0.8.0,<0.9.0
responses~=0.12.1
selenium~=3.141.0
1 change: 1 addition & 0 deletions tests/openwisp2/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DEBUG = True
TESTING = sys.argv[1:2] == ['test']
SELENIUM_HEADLESS = True if os.environ.get('SELENIUM_HEADLESS', False) else False
SHELL = 'shell' in sys.argv or 'shell_plus' in sys.argv

ALLOWED_HOSTS = ['*']
Expand Down

0 comments on commit f16e74d

Please sign in to comment.