Repository: openwisp/openwisp-utils (language: python)

Issue:
[test] Selenium test for Filters
Write selenium tests to ensure proper working of modified filters.
### Tasks

- [x] Log in to the admin site and ensure filters are visible on the change list page.
- [x] Test the filter dropdown is working properly.
- [x] Test filters are applied when the ``filter`` button is clicked.
- [ ] Test ``show all`` button works perfectly when clicked. **Not Req:** Removed this feature. 
- [x] Test ``clear all`` button working.

blocked by https://github.com/openwisp/openwisp-utils/issues/196.

[test] Selenium test for Filters
Write selenium tests to ensure proper working of modified filters.
### Tasks

- [x] Log in to the admin site and ensure filters are visible on the change list page.
- [x] Test the filter dropdown is working properly.
- [x] Test filters are applied when the ``filter`` button is clicked.
- [ ] Test ``show all`` button works perfectly when clicked. **Not Req:** Removed this feature. 
- [x] Test ``clear all`` button working.

blocked by https://github.com/openwisp/openwisp-utils/issues/196.



Discussion / hints:
> Also, create a checklist to show what is being tested and what is left ( In the PR description )

Where? I don't see it.
> > Also, create a checklist to show what is being tested and what is left ( In the PR description )
> 
> Where? I don't see it.

Thanks, I have created it.
> Also, create a checklist to show what is being tested and what is left ( In the PR description )

Where? I don't see it.
> > Also, create a checklist to show what is being tested and what is left ( In the PR description )
> 
> Where? I don't see it.

Thanks, I have created it.

Fix patch:
diff --git a/openwisp_utils/admin_theme/static/admin/css/ow-filters.css b/openwisp_utils/admin_theme/static/admin/css/ow-filters.css
index 3ad70ff4..8abd0841 100644
--- a/openwisp_utils/admin_theme/static/admin/css/ow-filters.css
+++ b/openwisp_utils/admin_theme/static/admin/css/ow-filters.css
@@ -81,11 +81,11 @@
   width: min-content;
   max-height: 18.75rem;
   min-width: 100%;
-  overflow-y: auto;
   transition: all 0.3s ease;
   box-shadow: 0px 1px 4px #00000040;
   pointer-events: none;
   display: none;
+  overflow-y: auto;
 }
 .filter-options ul{
   margin: 0px;
@@ -141,7 +141,7 @@
   letter-spacing: 0.5px;
   font-size: 14px;
 }
-.ow-filter.active .filter-title {
+.ow-filter.ow-active .filter-title {
   border-color: #df5d43;
   color: #df5d43;
 }
@@ -153,7 +153,7 @@
   display: block;
   box-sizing: border-box;
 }
-.ow-filter.active .filter-options {
+.ow-filter.ow-active .filter-options {
   pointer-events: all;
   opacity: 1;
   margin-top: 3px;
diff --git a/openwisp_utils/admin_theme/static/admin/js/ow-filter.js b/openwisp_utils/admin_theme/static/admin/js/ow-filter.js
index 333a749c..84bd4aaf 100644
--- a/openwisp_utils/admin_theme/static/admin/js/ow-filter.js
+++ b/openwisp_utils/admin_theme/static/admin/js/ow-filter.js
@@ -2,6 +2,8 @@
 var leftArrow, rightArrow, slider;
 const scrollDX = 200,
   btnAnimationTime = 100; //ms
+var gettext;
+
 (function () {
   document.addEventListener(
     'DOMContentLoaded',
@@ -16,6 +18,8 @@ const scrollDX = 200,
       if (slider) {
         setArrowButtonVisibility();
       }
+      gettext = window.gettext || function (v) { return v; };
+      add_clear_button();
     },
     false
   );
@@ -28,7 +32,7 @@ function showFilterOptions(filter, callback = null) {
   filter.querySelector('.filter-options').style.display = 'block';
   filter.querySelector('.filter-title').setAttribute('aria-expanded', 'true');
   setTimeout(function () {
-    filter.classList.add('active');
+    filter.classList.add('ow-active');
     if (callback) {
       callback(filter);
     }
@@ -39,15 +43,15 @@ function hideFilterOptions(filter){
   if(!filter){return;}
   filter.querySelector('.filter-options').style='';
   filter.querySelector('.filter-title').setAttribute('aria-expanded','false');
-  filter.classList.remove('active');
+  filter.classList.remove('ow-active');
 }
 
 function toggleFilter(filter, callback = null) {
-  var activeFilter = document.querySelector('.ow-filter.active');
+  var activeFilter = document.querySelector('.ow-filter.ow-active');
   if (activeFilter && activeFilter !== filter) {
     hideFilterOptions(activeFilter);
   }
-  if (filter.classList.contains('active')) {
+  if (filter.classList.contains('ow-active')) {
     return hideFilterOptions(filter);
   }
   showFilterOptions(filter, callback);
@@ -68,7 +72,6 @@ function initFilterDropdownHandler() {
       return;
     }
     toggler.addEventListener('click', function () {
-      // Close if any active filter
       toggleFilter(filter);
     });
     toggler.addEventListener('keypress', function (e) {
@@ -90,15 +93,15 @@ function initFilterDropdownHandler() {
 
   // Handle click outside of an active filter
   document.addEventListener('click', function (e) {
-    var activeFilter = document.querySelector('.ow-filter.active');
+    var activeFilter = document.querySelector('.ow-filter.ow-active');
     if (activeFilter && !activeFilter.contains(e.target)) {
-      activeFilter.classList.remove('active');
+      hideFilterOptions(activeFilter);
     }
   });
 
   // Handle focus shift from filter
   document.addEventListener('focusin', function (e) {
-    var activeFilter = document.querySelector('.ow-filter.active');
+    var activeFilter = document.querySelector('.ow-filter.ow-active');
     if (activeFilter && !activeFilter.contains(e.target)) {
       hideFilterOptions(activeFilter);
     }
@@ -112,12 +115,10 @@ function initFilterDropdownHandler() {
   filterValues.forEach(function (filterValue) {
     filterValue.addEventListener('click', function (e) {
       e.preventDefault();
-      let filter = document.querySelector('.ow-filter.active');
-      let selectedOption = filter.querySelector('.selected-option');
-      let selectedElement = filter.querySelector('.selected');
-      if (selectedElement) {
-        selectedElement.classList.remove('selected');
-      }
+      var filter = document.querySelector('.ow-filter.ow-active');
+      var selectedOption = filter.querySelector('.selected-option');
+      var selectedElement = filter.querySelector('.selected');
+      selectedElement.classList.remove('selected');
       filterValue.classList.add('selected');
       var text = filterValue.innerHTML;
       selectedOption.innerHTML = text;
@@ -295,3 +296,23 @@ function filterHandlers() {
     window.location.href = window.location.pathname + queryParams;
   });
 }
+
+function add_clear_button(){
+  /*
+  Some django versions do not support filter clear functionality
+  */ 
+  var path = window.location.href.split('?');
+  if (
+    path.length > 1 &&
+    path[1] != '' &&
+    !document.querySelector('#changelist-filter-clear')
+  ) {
+    var button = document.createElement('h3');
+    button.setAttribute('id', 'changelist-filter-clear');
+    var link = document.createElement('a');
+    link.setAttribute('href', path[0]);
+    link.innerText = gettext('Clear all filters');
+    button.appendChild(link);
+    document.querySelector('.filters-control').prepend(button);
+  }
+}
diff --git a/openwisp_utils/admin_theme/templates/admin/filter.html b/openwisp_utils/admin_theme/templates/admin/filter.html
index 281081f5..2614ef2f 100644
--- a/openwisp_utils/admin_theme/templates/admin/filter.html
+++ b/openwisp_utils/admin_theme/templates/admin/filter.html
@@ -1,24 +1,29 @@
 {% load i18n ow_tags %}
 
-<div class="ow-filter">
-  <div id="{{title|join_string}}" aria-label="by {{title}}"
-    tabindex="0" aria-controls="choices-{{filter_title}}"
-    role="button" aria-expanded="false" class="filter-title">
-    <h3>
-      {% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}
-    </h3>
-    <div class="selected-option" tabindex="0" title="{{ selected_choice | default_if_none:'------' }}">
-      {{ selected_choice | default_if_none:'------' }}
-    </div>
-  </div>
+<div class="ow-filter {{ title|join_string }}">
+  {% for choice in choices %}
+    {% if choice.selected %}
+      <div id="{{title|join_string}}" aria-label="by {{title}}"
+        tabindex="0" aria-controls="choices-{{filter_title}}"
+        role="button" aria-expanded="false" class="filter-title">
+        <h3>
+          {% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}
+        </h3>
+        <div class="selected-option" tabindex="0" title="{{ choice.display }}">
+          {{ choice.display }}
+        </div>
+      </div>
+    {% endif %}
+  {% endfor %}
   <div id="choices-{{title|join_string}}" role="region"
     aria-labelledby="{{title|join_string}}" class="filter-options">
     <ul>
     {% for choice in choices %}
       <li>
-        <a {% if choice.selected %} class="selected" {% endif %} title="{{ choice.display }}"
+        <a {% if choice.selected %} class="selected" {% endif %}
+          title="{{ choice.display | default_if_none:'------'}}"
           href="{{ choice.query_string|iriencode }}">
-          {{ choice.display }}
+          {{ choice.display | default_if_none:'------' }}
         </a>
       </li>
     {% endfor %}


Test patch:
diff --git a/tests/test_project/admin.py b/tests/test_project/admin.py
index f2c2ba48..aee82655 100644
--- a/tests/test_project/admin.py
+++ b/tests/test_project/admin.py
@@ -12,7 +12,7 @@
 )
 from openwisp_utils.admin_theme.filters import InputFilter, SimpleInputFilter
 
-from .models import Operator, Project, RadiusAccounting, Shelf
+from .models import Book, Operator, Project, RadiusAccounting, Shelf
 
 admin.site.unregister(User)
 
@@ -29,6 +29,11 @@ class UserAdmin(admin.ModelAdmin):
     ]
 
 
+@admin.register(Book)
+class BookAdmin(admin.ModelAdmin):
+    list_filter = ['name']
+
+
 @admin.register(Operator)
 class OperatorAdmin(admin.ModelAdmin):
     list_display = ['first_name', 'last_name']
@@ -80,7 +85,6 @@ class ShelfAdmin(TimeReadonlyAdminMixin, admin.ModelAdmin):
         ShelfFilter,
         ['books_type', InputFilter],
         ['id', InputFilter],
-        'name',
-        'owner__is_staff',
-        'owner__is_active',
+        'owner__username',
+        'books_type',
     ]
diff --git a/tests/test_project/migrations/0004_shelf_data.py b/tests/test_project/migrations/0004_sheft_data.py
similarity index 94%
rename from tests/test_project/migrations/0004_shelf_data.py
rename to tests/test_project/migrations/0004_sheft_data.py
index aca097ec..6cd75303 100644
--- a/tests/test_project/migrations/0004_shelf_data.py
+++ b/tests/test_project/migrations/0004_sheft_data.py
@@ -1,4 +1,4 @@
-# Generated by Django 3.1.13 on 2021-07-12 06:51
+# Generated by Django 3.1.13 on 2021-11-30 09:24
 
 from django.conf import settings
 from django.db import migrations, models
@@ -40,7 +40,7 @@ class Migration(migrations.Migration):
                 ],
                 max_length=50,
                 null=True,
-                verbose_name='Type of Books',
+                verbose_name='Type of book',
             ),
         ),
         migrations.AddField(
@@ -58,7 +58,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='shelf',
             name='owner',
-            field=models.OneToOneField(
+            field=models.ForeignKey(
                 blank=True,
                 null=True,
                 on_delete=django.db.models.deletion.CASCADE,
diff --git a/tests/test_project/models.py b/tests/test_project/models.py
index b630824d..b9590934 100644
--- a/tests/test_project/models.py
+++ b/tests/test_project/models.py
@@ -20,11 +20,11 @@ class Shelf(TimeStampedEditableModel):
     )
     name = models.CharField(_('name'), max_length=64)
     books_type = models.CharField(
-        _("Type of Books"), choices=TYPES, null=True, blank=True, max_length=50
+        _("Type of book"), choices=TYPES, null=True, blank=True, max_length=50
     )
     books_count = models.PositiveIntegerField(_("Number of books"), default=0)
     locked = models.BooleanField(_("Is locked"), default=True)
-    owner = models.OneToOneField(
+    owner = models.ForeignKey(
         "auth.User",
         blank=True,
         null=True,
diff --git a/tests/test_project/tests/config.json b/tests/test_project/tests/config.json
index c016a0f2..5dc61bee 100644
--- a/tests/test_project/tests/config.json
+++ b/tests/test_project/tests/config.json
@@ -5,9 +5,9 @@
   "admin_email": "admin@admin.com",
   "maximize_menu": "Maximize menu",
   "minimize_menu": "Minimize menu",
-  "site_name_xpath": "//*[@id=\"site-name\"]",
-  "hamburger_xpath": "//*[@class=\"hamburger\"]",
-  "main_content_xpath": "//*[@id=\"main-content\"]",
+  "site_name_css_selector": "#site-name",
+  "hamburger_css_selector": ".hamburger",
+  "main_content_css_selector": "#main-content",
   "login_url": "/admin/login/",
   "tester_username": "tester",
   "tester_password": "tester",
diff --git a/tests/test_project/tests/test_admin.py b/tests/test_project/tests/test_admin.py
index 227d2186..96979928 100644
--- a/tests/test_project/tests/test_admin.py
+++ b/tests/test_project/tests/test_admin.py
@@ -308,7 +308,7 @@ def test_filters(self):
             response = self.client.get(url)
             real_count = self._assert_contains(
                 response,
-                'class="ow-filter"',
+                'class="ow-filter',
                 status_code=response.status_code,
                 msg_prefix='',
                 html=False,
diff --git a/tests/test_project/tests/test_selenium.py b/tests/test_project/tests/test_selenium.py
index b2e5c6f7..095b8c56 100644
--- a/tests/test_project/tests/test_selenium.py
+++ b/tests/test_project/tests/test_selenium.py
@@ -6,7 +6,7 @@
 from selenium.webdriver.support import expected_conditions as EC
 from selenium.webdriver.support.ui import WebDriverWait
 
-from ..models import Shelf
+from ..models import Book, Shelf
 from . import CreateMixin
 from .utils import SeleniumTestMixin
 
@@ -74,7 +74,9 @@ def _test_login_and_logout_page(self, is_logged_in=True):
         # Test login page
         self.web_driver.refresh()
         WebDriverWait(self.web_driver, 2).until(
-            EC.visibility_of_element_located((By.XPATH, self.config['site_name_xpath']))
+            EC.visibility_of_element_located(
+                (By.CSS_SELECTOR, self.config['site_name_css_selector'])
+            )
         )
         hamburger = self._get_hamburger()
         logo = self._get_logo()
@@ -165,7 +167,7 @@ def _test_menu_dropdown(self, is_narrow=False, is_medium=False):
             try:
                 WebDriverWait(self.web_driver, 2).until(
                     EC.visibility_of_element_located(
-                        (By.XPATH, '//*[@class="nav"]/div[1]/div[2]')
+                        (By.CSS_SELECTOR, '#mg-dropdown-32')
                     )
                 )
             except TimeoutException:
@@ -192,7 +194,10 @@ def _test_menu_dropdown(self, is_narrow=False, is_medium=False):
             try:
                 WebDriverWait(self.web_driver, 2).until(
                     EC.visibility_of_element_located(
-                        (By.XPATH, '//*[@class="nav"]/div[1]/div[1]/div[1]/span[2]')
+                        (
+                            By.CSS_SELECTOR,
+                            '#mg-control-32 > div:nth-child(1) > span:nth-child(2)',
+                        )
                     )
                 )
             except TimeoutException:
@@ -229,7 +234,7 @@ def test_menu_on_wide_screen(self):
             self.web_driver.refresh()
             WebDriverWait(self.web_driver, 2).until(
                 EC.visibility_of_element_located(
-                    (By.XPATH, self.config['site_name_xpath'])
+                    (By.CSS_SELECTOR, self.config['site_name_css_selector'])
                 )
             )
             self._test_menu_state(True)
@@ -242,7 +247,7 @@ def test_menu_on_wide_screen(self):
             self.web_driver.refresh()
             WebDriverWait(self.web_driver, 2).until(
                 EC.visibility_of_element_located(
-                    (By.XPATH, self.config['hamburger_xpath'])
+                    (By.CSS_SELECTOR, self.config['hamburger_css_selector'])
                 )
             )
             self._test_menu_state(False)
@@ -272,7 +277,9 @@ def test_active_menu_group(self):
         url = reverse('admin:auth_user_changelist')
         self.open(url)
         WebDriverWait(self.web_driver, 2).until(
-            EC.visibility_of_element_located((By.XPATH, self.config['site_name_xpath']))
+            EC.visibility_of_element_located(
+                (By.CSS_SELECTOR, self.config['site_name_css_selector'])
+            )
         )
         with self.subTest('Test active menu group on wide screen'):
             active_mg = self._get_active_mg()
@@ -299,7 +306,7 @@ def test_active_menu_group(self):
             self.web_driver.refresh()
             WebDriverWait(self.web_driver, 2).until(
                 EC.visibility_of_element_located(
-                    (By.XPATH, self.config['hamburger_xpath'])
+                    (By.CSS_SELECTOR, self.config['hamburger_css_selector'])
                 )
             )
             active_mg = self._get_active_mg()
@@ -321,7 +328,7 @@ def test_active_menu_group(self):
             account_button = self._get_account_button()
             WebDriverWait(self.web_driver, 2).until(
                 EC.visibility_of_element_located(
-                    (By.XPATH, self.config['hamburger_xpath'])
+                    (By.CSS_SELECTOR, self.config['hamburger_css_selector'])
                 )
             )
             hamburger.click()
@@ -355,7 +362,9 @@ def test_menu_on_medium_screen(self):
         self._open_menu()
         self.web_driver.refresh()
         WebDriverWait(self.web_driver, 2).until(
-            EC.visibility_of_element_located((By.XPATH, self.config['hamburger_xpath']))
+            EC.visibility_of_element_located(
+                (By.CSS_SELECTOR, self.config['hamburger_css_selector'])
+            )
         )
         with self.subTest('Test menu remains close on page change or refresh'):
             self._test_menu_state(False)
@@ -390,6 +399,156 @@ def test_menu_on_narrow_screen(self):
         self.web_driver.set_window_size(1366, 768)
 
 
+class TestBasicFilter(SeleniumTestMixin, StaticLiveServerTestCase, CreateMixin):
+    shelf_model = Shelf
+    book_model = Book
+
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+
+    def setUp(self):
+        self.admin = self._create_admin()
+        self.web_driver.set_window_size(1600, 768)
+        self._create_test_data()
+
+    def _create_test_data(self):
+        # create two users
+        tester1 = self._create_admin(username='tester1')
+        self._create_admin(username='tester2')
+        # creating 2 Horror and 2 Fantasy shelfs.
+        # tester1 is the owner of all books
+        for i in range(2):
+            horror_shelf = self._create_shelf(
+                name='horror' + str(i), books_type='HORROR', owner=tester1
+            )
+        for i in range(2):
+            fantasy_shelf = self._create_shelf(
+                name='fantasy' + str(i), books_type='FANTASY', owner=tester1
+            )
+        self._create_book(name='horror book', shelf=horror_shelf)
+        self._create_book(name='fantasy book', shelf=fantasy_shelf)
+
+    def test_shelf_filter(self):
+        # It has total number of filters greater than 4
+        self.login()
+        url = reverse('admin:test_project_shelf_changelist')
+        self.open(url)
+        dropdown = self._get_filter_dropdown('type-of-book')
+        title = self._get_filter_title('type-of-book')
+        main_content = self._get_main_content()
+        selected_option = self._get_filter_selected_option('type-of-book')
+        with self.subTest('Test visibility of filter'):
+            self.assertEqual(self.check_exists_by_id('ow-changelist-filter'), True)
+
+        with self.subTest('Test visibility of filter button'):
+            self.assertEqual(self.check_exists_by_id('ow-apply-filter'), True)
+
+        with self.subTest('Test filter dropdown is not visible'):
+            self.assertEqual(dropdown.is_displayed(), False)
+
+        with self.subTest('Test anchor tag in filter options'):
+            self.assertEqual(
+                self.check_exists_by_css_selector('.type-of-book .filter-options a'),
+                True,
+            )
+
+        with self.subTest('Test filter dropdown is visbility'):
+            title.click()
+            self.wait_for_dropdown('type-of-book')
+            self.assertEqual(dropdown.is_displayed(), True)
+            title.click()
+            self.assertEqual(dropdown.is_displayed(), False)
+            title.click()
+            self.wait_for_dropdown('type-of-book')
+            self.assertEqual(dropdown.is_displayed(), True)
+            main_content.click()
+            self.assertEqual(dropdown.is_displayed(), False)
+
+        with self.subTest('Test changing of filter option'):
+            title.click()  # Open dropdown
+            self.wait_for_dropdown('type-of-book')
+            old_value = selected_option.get_attribute('innerText')
+            fantasy_option = self._get_filter_anchor('books_type__exact=FANTASY')
+            fantasy_option.click()
+            self.assertEqual(dropdown.is_displayed(), False)
+            self.assertNotEqual(selected_option.get_attribute('innerText'), old_value)
+            self.assertEqual(selected_option.get_attribute('innerText'), 'FANTASY')
+
+        filter_button = self._get_filter_button()
+        with self.subTest('Test apply filter'):
+            filter_button.click()
+            WebDriverWait(self.web_driver, 2).until(
+                EC.visibility_of_element_located((By.CSS_SELECTOR, '#site-name'))
+            )
+            self.assertEqual(self.check_exists_by_id('changelist-filter-clear'), True)
+            paginator = self.web_driver.find_element_by_css_selector('.paginator')
+            self.assertEqual(paginator.get_attribute('innerText'), '2 shelfs')
+
+        with self.subTest('Test clear filter button'):
+            clear_button = self._get_clear_button()
+            clear_button.click()
+            WebDriverWait(self.web_driver, 2).until(
+                EC.visibility_of_element_located((By.CSS_SELECTOR, '#site-name'))
+            )
+            paginator = self.web_driver.find_element_by_css_selector('.paginator')
+            self.assertEqual(paginator.get_attribute('innerText'), '4 shelfs')
+
+        with self.subTest('Test multiple filters'):
+            # Select Fantasy book type
+            books_type_title = self._get_filter_title('type-of-book')
+            username_title = self._get_filter_title('username')
+            books_type_title.click()
+            fantasy_option = self._get_filter_anchor('books_type__exact=FANTASY')
+            fantasy_option.click()
+            username_title.click()
+            username_option = self._get_filter_anchor('owner__username=tester2')
+            username_option.click()
+            filter_button = self._get_filter_button()
+            filter_button.click()
+            WebDriverWait(self.web_driver, 2).until(
+                EC.visibility_of_element_located((By.CSS_SELECTOR, '#site-name'))
+            )
+            paginator = self.web_driver.find_element_by_css_selector('.paginator')
+            self.assertEqual(paginator.get_attribute('innerText'), '0 shelfs')
+
+    def test_book_filter(self):
+        # It has total number of filters less than 4
+        self.login()
+        url = reverse('admin:test_project_book_changelist')
+        self.open(url)
+        with self.subTest('Test visibility of filter'):
+            self.assertEqual(self.check_exists_by_id('ow-changelist-filter'), True)
+
+        with self.subTest('Test filter button is not visible'):
+            self.assertEqual(self.check_exists_by_id('ow-apply-filter'), False)
+
+        with self.subTest('Test anchor tag in filter options'):
+            self.assertEqual(
+                self.check_exists_by_css_selector('.name .filter-options a'), True
+            )
+
+        with self.subTest('Test dropdown and apply filter'):
+            dropdown = self._get_filter_dropdown('name')
+            title = self._get_filter_title('name')
+            option = self._get_filter_anchor('name=horror+book')
+            selected_option = self._get_filter_selected_option('name')
+            old_value = selected_option.get_attribute('innerText')
+            self.assertEqual(dropdown.is_displayed(), False)
+            title.click()
+            self.wait_for_dropdown('name')
+            self.assertEqual(dropdown.is_displayed(), True)
+            option.click()
+            WebDriverWait(self.web_driver, 2).until(
+                EC.visibility_of_element_located((By.CSS_SELECTOR, '#site-name'))
+            )
+            selected_option = self._get_filter_selected_option('name')
+            self.assertNotEqual(old_value, selected_option.get_attribute('innerText'))
+            self.assertEqual(selected_option.get_attribute('innerText'), 'horror book')
+            paginator = self.web_driver.find_element_by_css_selector('.paginator')
+            self.assertEqual(paginator.get_attribute('innerText'), '1 book')
+
+
 class TestInputFilters(SeleniumTestMixin, CreateMixin, StaticLiveServerTestCase):
     shelf_model = Shelf
 
@@ -420,16 +579,14 @@ def test_input_filters(self):
             self.open(url)
             input_field = self._get_simple_input_filter()
             input_field.send_keys('Horror')
-            self._get_apply_filter().click()
+            self._get_filter_button().click()
             # Horror shelf is present
             self.web_driver.find_element_by_xpath(horror_result_xpath)
             with self.assertRaises(NoSuchElementException):
                 # Factual shelf is absent
                 self.web_driver.find_element_by_xpath(factual_result_xpath)
             # Both shelves should be present after clearing filter
-            self.web_driver.find_element_by_xpath(
-                '//*[@id="ow-changelist-filter"]/div[1]/div/div/div[1]/div[1]/form/a'
-            ).click()
+            self.web_driver.find_element_by_css_selector('.field-clear').click()
             self.web_driver.find_element_by_xpath(horror_result_xpath)
             self.web_driver.find_element_by_xpath(factual_result_xpath)
 
@@ -437,35 +594,31 @@ def test_input_filters(self):
             self.open(url)
             input_field = self._get_input_filter()
             input_field.send_keys('HORROR')
-            self._get_apply_filter().click()
+            self._get_filter_button().click()
             # Horror shelf is present
             self.web_driver.find_element_by_xpath(horror_result_xpath)
             with self.assertRaises(NoSuchElementException):
                 # Factual shelf is absent
                 self.web_driver.find_element_by_xpath(factual_result_xpath)
             # Both shelves should be present after clearing filter
-            self.web_driver.find_element_by_xpath(
-                '//*[@id="ow-changelist-filter"]/div[1]/div/div/div[2]/div[1]/form/a'
-            ).click()
+            self.web_driver.find_element_by_css_selector('.field-clear').click()
             self.web_driver.find_element_by_xpath(horror_result_xpath)
             self.web_driver.find_element_by_xpath(factual_result_xpath)
 
         with self.subTest('Test InputFilter: UUID'):
             self.open(url)
-            input_field = self.web_driver.find_element_by_xpath(
-                '//*[@id="ow-changelist-filter"]/div[1]/div/div/div[3]/div[1]/form/input'
+            input_field = self.web_driver.find_element_by_css_selector(
+                'input[name=id__exact]'
             )
             input_field.send_keys(str(horror_shelf.id))
-            self._get_apply_filter().click()
+            self._get_filter_button().click()
             # Horror shelf is present
             self.web_driver.find_element_by_xpath(horror_result_xpath)
             with self.assertRaises(NoSuchElementException):
                 # Factual shelf is absent
                 self.web_driver.find_element_by_xpath(factual_result_xpath)
             # Both shelves should be present after clearing filter
-            self.web_driver.find_element_by_xpath(
-                '//*[@id="ow-changelist-filter"]/div[1]/div/div/div[3]/div[1]/form/a'
-            ).click()
+            self.web_driver.find_element_by_css_selector('.field-clear').click()
             self.web_driver.find_element_by_xpath(horror_result_xpath)
             self.web_driver.find_element_by_xpath(factual_result_xpath)
 
@@ -477,15 +630,13 @@ def test_input_filters(self):
                 '//*[@id="ow-changelist-filter"]/div[1]/div/div/div[2]/div[1]/form/input'
             )
             input_field.send_keys(str(horror_shelf.id))
-            self._get_apply_filter().click()
+            self._get_filter_button().click()
             # Admin user is present
             self.web_driver.find_element_by_xpath(admin_xpath)
             with self.assertRaises(NoSuchElementException):
                 # User is absent
                 self.web_driver.find_element_by_xpath(user_xpath)
             # Both users should be present after clearing filter
-            self.web_driver.find_element_by_xpath(
-                '//*[@id="ow-changelist-filter"]/div[1]/div/div/div[2]/div[1]/form/a'
-            ).click()
+            self.web_driver.find_element_by_css_selector('.field-clear').click()
             self.web_driver.find_element_by_xpath(admin_xpath)
             self.web_driver.find_element_by_xpath(user_xpath)
diff --git a/tests/test_project/tests/utils.py b/tests/test_project/tests/utils.py
index a6a67807..e6f0e2e3 100644
--- a/tests/test_project/tests/utils.py
+++ b/tests/test_project/tests/utils.py
@@ -4,6 +4,7 @@
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from selenium import webdriver
+from selenium.common.exceptions import NoSuchElementException
 from selenium.webdriver.common.by import By
 from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
 from selenium.webdriver.support import expected_conditions as EC
@@ -36,7 +37,8 @@ def setUpClass(cls):
         capabilities = DesiredCapabilities.CHROME
         capabilities['goog:loggingPrefs'] = {'browser': 'ALL'}
         cls.web_driver = webdriver.Chrome(
-            options=chrome_options, desired_capabilities=capabilities
+            options=chrome_options,
+            desired_capabilities=capabilities,
         )
 
     @classmethod
@@ -56,7 +58,7 @@ def open(self, url, driver=None):
         driver.get(f'{self.live_server_url}{url}')
         WebDriverWait(self.web_driver, 2).until(
             EC.visibility_of_element_located(
-                (By.XPATH, self.config['main_content_xpath'])
+                (By.CSS_SELECTOR, self.config['main_content_css_selector'])
             )
         )
 
@@ -103,7 +105,7 @@ def login(self, username=None, password=None, driver=None):
         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()
+            driver.find_element_by_css_selector('input[type="submit"]').click()
 
     def logout(self):
         account_button = self._get_account_button()
@@ -127,8 +129,8 @@ def _get_main_content(self):
         return self.web_driver.find_element_by_id('main-content')
 
     def _get_menu_home_item_label(self):
-        return self.web_driver.find_element_by_xpath(
-            '//span[@class="label" and contains(.,"Home")]'
+        return self.web_driver.find_element_by_css_selector(
+            'a.menu-item:nth-child(1) > span:nth-child(2)'
         )
 
     def _get_logo(self):
@@ -138,16 +140,14 @@ def _get_container(self):
         return self.web_driver.find_element_by_id('container')
 
     def _get_test_mg_head(self):
-        return self.web_driver.find_element_by_xpath('//*[@class="nav"]/div[1]/div[1]')
+        return self.web_driver.find_element_by_css_selector('#mg-control-32')
 
     def _get_test_mg_icon(self):
-        return self.web_driver.find_element_by_xpath(
-            '//*[@class="nav"]/div[1]/div[1]/div[1]/span[1]'
-        )
+        return self.web_driver.find_element_by_css_selector('.auth')
 
     def _get_test_mg_label(self):
-        return self.web_driver.find_element_by_xpath(
-            '//*[@class="nav"]/div[1]/div[1]/div[1]/span[2]'
+        return self.web_driver.find_element_by_css_selector(
+            '#mg-control-32 > div:nth-child(1) > span:nth-child(2)'
         )
 
     def _get_active_mg(self):
@@ -157,11 +157,11 @@ def _get_active_mg_head(self):
         return self.web_driver.find_element_by_css_selector('.active-mg .mg-head')
 
     def _get_test_mg_dropdown(self):
-        return self.web_driver.find_element_by_xpath('//*[@class="nav"]/div[1]/div[2]')
+        return self.web_driver.find_element_by_css_selector('#mg-dropdown-32')
 
     def _get_test_mg_dropdown_label(self):
-        return self.web_driver.find_element_by_xpath(
-            '//*[@class="nav"]/div[1]/div[2]/div[1]'
+        return self.web_driver.find_element_by_css_selector(
+            '#mg-dropdown-32 > div:nth-child(1)'
         )
 
     def _get_account_button(self):
@@ -177,26 +177,19 @@ def _get_account_dropdown_username(self):
         return self.web_driver.find_element_by_css_selector('.account-menu-username')
 
     def _get_logout_link(self):
-        return self.web_driver.find_element_by_xpath(
-            '//a[@class="menu-link" and @href="/admin/logout/"]'
-        )
+        return self.web_driver.find_element_by_css_selector('.menu-link')
 
     def _get_menu_backdrop(self):
         return self.web_driver.find_element_by_css_selector('.menu-backdrop')
 
     def _get_simple_input_filter(self):
-        return self.web_driver.find_element_by_xpath(
-            '//*[@id="ow-changelist-filter"]/div[1]/div/div/div[1]/div[1]/form/input'
-        )
+        return self.web_driver.find_element_by_css_selector('input[name=shelf]')
 
     def _get_input_filter(self):
-        return self.web_driver.find_element_by_xpath(
-            '//*[@id="ow-changelist-filter"]/div[1]/div/div/div[2]/div[1]/form/input'
+        return self.web_driver.find_element_by_css_selector(
+            'input[name=books_type__exact]'
         )
 
-    def _get_apply_filter(self):
-        return self.web_driver.find_element_by_xpath('//*[@id="ow-apply-filter"]')
-
     def _open_menu(self):
         hamburger = self._get_hamburger()
         if not hamburger.is_displayed():
@@ -219,3 +212,58 @@ def _close_menu(self):
                 is_menu_close = True
         if not is_menu_close:
             hamburger.click()
+
+    def _get_filter(self):
+        return self.web_driver.find_element_by_id('ow-changelist-filter')
+
+    def _get_filter_button(self):
+        return self.web_driver.find_element_by_id('ow-apply-filter')
+
+    def _get_clear_button(self):
+        return self.web_driver.find_element_by_id('changelist-filter-clear')
+
+    def check_exists_by_id(self, id):
+        try:
+            self.web_driver.find_element_by_id(id)
+        except NoSuchElementException:
+            return False
+        return True
+
+    def check_exists_by_xpath(self, xpath):
+        try:
+            self.web_driver.find_element_by_xpath(xpath)
+        except NoSuchElementException:
+            return False
+        return True
+
+    def check_exists_by_css_selector(self, selector):
+        try:
+            self.web_driver.find_element_by_css_selector(selector)
+        except NoSuchElementException:
+            return False
+        return True
+
+    def _get_filter_selected_option(self, filter_class):
+        return self.web_driver.find_element_by_css_selector(
+            f'.{filter_class} .selected-option'
+        )
+
+    def _get_filter_dropdown(self, filter_class):
+        return self.web_driver.find_element_by_css_selector(
+            f'.{filter_class} .filter-options'
+        )
+
+    def _get_filter_title(self, filter_class):
+        return self.web_driver.find_element_by_css_selector(
+            f'.{filter_class} .filter-title'
+        )
+
+    def _get_filter_anchor(self, query):
+        return self.web_driver.find_element_by_xpath(f'//a[@href="?{query}"]')
+
+    def wait_for_dropdown(self, filter_class):
+        WebDriverWait(self.web_driver, 2).until(
+            EC.visibility_of_element_located(
+                (By.CSS_SELECTOR, f'.{filter_class} .filter-options')
+            )
+        )