From 0d870def1ae1a8aa990926767aecb533a284c970 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Wed, 8 Nov 2023 14:39:15 +0200 Subject: [PATCH 01/16] Exclude selenium test from large scale testing --- .github/workflows/django.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index ffc5de0d..e5e35d7d 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -83,7 +83,7 @@ jobs: TESTDB: postgres run: | export PYTHONWARNINGS=always - coverage run --source='wafer' manage.py test && coverage report --skip-covered + coverage run --source='wafer' manage.py test --exclude-tag selenium && coverage report --skip-covered sqlite: @@ -138,7 +138,7 @@ jobs: continue-on-error: ${{ matrix.django-version == 'main' }} run: | export PYTHONWARNINGS=always - coverage run --source='wafer' manage.py test && coverage report --skip-covered + coverage run --source='wafer' manage.py test --exclude-tag selenium && coverage report --skip-covered translations: runs-on: ubuntu-latest From a34e9bdc41d00b4ff24973c24ec6a2baa85b12a5 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Wed, 8 Nov 2023 14:39:43 +0200 Subject: [PATCH 02/16] Sketch in selenium helpers --- wafer/tests/utils.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/wafer/tests/utils.py b/wafer/tests/utils.py index dd8a5c2c..cb1e1d32 100644 --- a/wafer/tests/utils.py +++ b/wafer/tests/utils.py @@ -2,6 +2,14 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission +from django.test import tag +from django.contrib.staticfiles.testing import StaticLiveServerTestCase + +try: + # Guard for running this without selenium installed + from selenium import webdriver +except ImportError: + selenium = None def get_group(group): @@ -36,3 +44,38 @@ def mock_avatar_url(self): if self.user.email is None: return None return "avatar-%s" % self.user.email + +@tag('selenium') +class BaseWebdriverRunner(StaticLiveServerTestCase): + + def setUp(self): + """Create an ordinary user and an admin user for testing""" + if not selenium: + raise RuntimeError("Test requires selenium installed") + super().setUp() + self.admin_user = create_user('admin', email='admin@localhost', superuser=True) + self.admin_password = 'admin_password' + self.normal_user = create_user('normal', email='normal@localhost', superuser=False) + self.normal_password = 'normal_password' + + def normal_login(self): + """Login as an ordinary user""" + + def admin_login(self): + """Login as the admin user""" + + +@tag('chrome') +class ChromeTestRunner(BaseWebdriverRunner): + + def setUp(self): + super().setUp() + # Load the chrome webdriver + + +@tag('firefox') +class FirefoxTestRunner(BaseWebdriverRunner): + + def setUp(self): + super().setUp() + # Load the firefox webdriver From fa4885d880a44639ddee5467de2cc3a673ced10f Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Wed, 8 Nov 2023 14:40:02 +0200 Subject: [PATCH 03/16] Add framework for webdriver schedule tests --- .../tests/test_schedule_datetime_widget.py | 45 +++++ wafer/schedule/tests/test_schedule_editor.py | 158 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 wafer/schedule/tests/test_schedule_datetime_widget.py create mode 100644 wafer/schedule/tests/test_schedule_editor.py diff --git a/wafer/schedule/tests/test_schedule_datetime_widget.py b/wafer/schedule/tests/test_schedule_datetime_widget.py new file mode 100644 index 00000000..7e9182fc --- /dev/null +++ b/wafer/schedule/tests/test_schedule_datetime_widget.py @@ -0,0 +1,45 @@ +import datetime as D + +from django.utils import timezone + +from wafer.pages.models import Page +from wafer.tests.utils import ChromeTestRunner, FirefoxTestRunner + +from wafer.schedule.models import Venue, Slot, ScheduleBlock +from wafer.schedule.tests.test_views import make_pages, make_items + + +class ScheduleDateTimeJSMixin: + """Test the custom datetime widget""" + + def setUp(self): + """Create venue, block and slots so we use the slot admin page""" + super().setUp() + block1 = ScheduleBlock.objects.create( + start_time=D.datetime(2013, 9, 22, 7, 0, 0, + tzinfo=D.timezone.utc), + end_time=D.datetime(2013, 9, 22, 19, 0, 0, + tzinfo=D.timezone.utc), + ) + venue1 = Venue.objects.create(order=1, name='Venue 1') + venue1.blocks.add(block1) + + slot1 = Slot(start_time=D.datetime(2013, 9, 22, 12, 0, 0, + tzinfo=D.timezone.utc), + end_time=D.datetime(2013, 9, 22, 13, 0, 0, + tzinfo=D.timezone.utc)) + slot2 = Slot(previous_slot=slot1, + end_time=D.datetime(2013, 9, 22, 14, 0, 0, + tzinfo=D.timezone.utc)) + + def test_datetime_widget(self): + """Test that the datetime widget lists the desired times""" + self.admin_login() + + +class ChromeScheduleEditorTests(ScheduleDateTimeJSMixin, ChromeTestRunner): + pass + + +class FirefoxSchedultEditorTests(ScheduleDateTimeJSMixin, FirefoxTestRunner): + pass diff --git a/wafer/schedule/tests/test_schedule_editor.py b/wafer/schedule/tests/test_schedule_editor.py new file mode 100644 index 00000000..4439b84c --- /dev/null +++ b/wafer/schedule/tests/test_schedule_editor.py @@ -0,0 +1,158 @@ +import datetime as D + +from django.utils import timezone + +from wafer.pages.models import Page +from wafer.tests.utils import create_user, ChromeTestRunner, FirefoxTestRunner +from wafer.talks.tests.fixtures import create_talk +from wafer.talks.models import ACCEPTED + +from wafer.schedule.models import ScheduleBlock, Venue, Slot, ScheduleItem +from wafer.schedule.tests.test_views import make_pages, make_items + + +class EditorTestsMixin: + """Define the schedule editor tests independant of the + selenium webdriver. + + This is combined with the appropriate helper class + to create the actual test cases.""" + + def setUp(self): + """Create two day table with 3 slots each and 2 venues + and create some page and talks to populate the schedul""" + super().setUp() + # Schedule is + # Day1 + # Venue 1 Venue 2 + # 10-11 Item1 Item4 + # 11-12 Item2 Item5 + # 12-13 Item3 Item6 + # Day 2 + # Venue 1 Venue 2 Venue 3 + # 10-11 Item7 Item10 Item13 + # 11-12 Item8 Item11 Item14 + # 12-13 Item9 Item12 Item15 + block1 = ScheduleBlock.objects.create( + start_time=D.datetime(2013, 9, 22, 7, 0, 0, + tzinfo=D.timezone.utc), + end_time=D.datetime(2013, 9, 22, 19, 0, 0, + tzinfo=D.timezone.utc), + ) + + block2 = ScheduleBlock.objects.create( + start_time=D.datetime(2013, 9, 23, 7, 0, 0, + tzinfo=D.timezone.utc), + end_time=D.datetime(2013, 9, 23, 19, 0, 0, + tzinfo=D.timezone.utc), + ) + + venue1 = Venue.objects.create(order=1, name='Venue 1') + venue1.blocks.add(block1) + venue1.blocks.add(block2) + + venue2 = Venue.objects.create(order=2, name='Venue 2') + venue2.blocks.add(block1) + venue2.blocks.add(block2) + + venue3 = Venue.objects.create(order=3, name='Venue 3') + venue3.blocks.add(block2) + + self.venues = [venue1, venue2, venue3] + + b1_start1 = D.datetime(2013, 9, 22, 10, 0, 0, + tzinfo=D.timezone.utc) + b1_start2 = D.datetime(2013, 9, 22, 11, 0, 0, + tzinfo=D.timezone.utc) + b1_start3 = D.datetime(2013, 9, 22, 12, 0, 0, + tzinfo=D.timezone.utc) + b1_end = D.datetime(2013, 9, 22, 13, 0, 0, + tzinfo=D.timezone.utc) + + b1_slot1 = Slot(start_time=b1_start1, + end_time=b1_start2) + b1_slot2 = Slot(previous_slot=b1_slot1, + end_time=b1_start3) + b1_slot3 = Slot(previous_slot=b1_slot2, + end_time=b1_end) + + b2_start1 = D.datetime(2013, 9, 23, 10, 0, 0, + tzinfo=D.timezone.utc) + b2_start2 = D.datetime(2013, 9, 23, 11, 0, 0, + tzinfo=D.timezone.utc) + b2_start3 = D.datetime(2013, 9, 23, 12, 0, 0, + tzinfo=D.timezone.utc) + b2_end = D.datetime(2013, 9, 23, 13, 0, 0, + tzinfo=D.timezone.utc) + + b2_slot1 = Slot(start_time=b2_start1, + end_time=b2_start2) + b2_slot2 = Slot(previous_slot=b2_slot1, + end_time=b2_start3) + b2_slot3 = Slot(previous_slot=b2_slot2, + end_time=b2_end) + + self.pages = make_pages(12) + + self.block1_slots = [b1_slot1, b1_slot2, b1_slot3] + self.block2_slots = [b2_slot1, b2_slot2, b2_slot3] + + self.talk1 = create_talk('Test talk 1', status=ACCEPTED, username='john') + self.talk2 = create_talk('Test talk 2', status=ACCEPTED, username='james') + self.talk3 = create_talk('Test talk 3', status=ACCEPTED, username='jess') + self.talk4 = create_talk('Test talk 4', status=ACCEPTED, username='jonah') + + def test_access_schedule_editor_no_login(self): + """Test that the schedule editor isn't accessible if not logged in""" + + def test_access_schedule_editor_no_super(self): + """Test that the schedule editor isn't accessible for non-superuser accounts""" + self.normal_login() + + def test_access_schedule_editor_admin(self): + """Test that the schedule editor is accessible for superuser accounts""" + self.admin_login() + + def test_drag_talk(self): + """Test dragging talk behavior""" + self.admin_login() + # Drag a talk from the siderbar to a slot in the schedule + # Check that dragged talk is not on the list of unassigned talks + # Drag a talk from one slot to another + + def test_drag_page(self): + """Test dragging page behavior""" + self.admin_login() + # Drag a page from the siderbar to a slot in the schedule + # Check that this doesn't change the unassigned list + # Drag a page from one spot to another + + def test_swicth_day(self): + """Test selecting different days""" + self.admin_login() + + def test_remove_talk(self): + """Test removing talks""" + self.admin_login() + # Test delete button + # Check that unassigned list is updated correctly + # Test dragging a talk over an existing talk + # Check that unassigned list is updated correctly + + def test_remove_page(self): + """Test removing pages""" + self.admin_login() + # Test delete button + # Test dragging a page over an existing page + + def test_create_invalid_schedule(self): + """Test that an invalid schedule displays the errors""" + self.admin_login() + + +class ChromeScheduleEditorTests(EditorTestsMixin, ChromeTestRunner): + pass + + +class FirefoxSchedultEditorTests(EditorTestsMixin, FirefoxTestRunner): + pass From 552cfe6c04414e94dce77915cab873398eb61eb6 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Thu, 9 Nov 2023 16:33:31 +0200 Subject: [PATCH 04/16] Implement login method and add required group --- wafer/tests/utils.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/wafer/tests/utils.py b/wafer/tests/utils.py index cb1e1d32..010d4143 100644 --- a/wafer/tests/utils.py +++ b/wafer/tests/utils.py @@ -5,11 +5,17 @@ from django.test import tag from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from wafer.users.models import PROFILE_GROUP + try: # Guard for running this without selenium installed from selenium import webdriver + from selenium.webdriver.common.keys import Keys + from selenium.webdriver.common.by import By + from selenium.webdriver.support.ui import WebDriverWait + from selenium.webdriver.support import expected_conditions except ImportError: - selenium = None + webdriver = None def get_group(group): @@ -50,19 +56,40 @@ class BaseWebdriverRunner(StaticLiveServerTestCase): def setUp(self): """Create an ordinary user and an admin user for testing""" - if not selenium: + if not webdriver: raise RuntimeError("Test requires selenium installed") super().setUp() self.admin_user = create_user('admin', email='admin@localhost', superuser=True) self.admin_password = 'admin_password' self.normal_user = create_user('normal', email='normal@localhost', superuser=False) self.normal_password = 'normal_password' + # Required to load the user profile page because of the key-value fields + create_group(PROFILE_GROUP) + + def _login(self, name, password): + """Generic login handler""" + self.driver.get(f"{self.live_server_url}/accounts/login/") + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.NAME, "submit")) + ) + user_field = self.driver.find_element(By.NAME, 'username') + user_field.send_keys(name) + pass_field = self.driver.find_element(By.NAME, 'password') + pass_field.send_keys(password) + loginbut = self.driver.find_element(By.NAME, 'submit') + loginbut.click() + #self.driver.get(f"{self.live_server_url}/") + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.PARTIAL_LINK_TEXT, "Log out")) + ) def normal_login(self): """Login as an ordinary user""" + self._login(self.normal_user.username, self.normal_password) def admin_login(self): """Login as the admin user""" + self._login(self.admin_user.username, self.admin_password) @tag('chrome') @@ -71,7 +98,9 @@ class ChromeTestRunner(BaseWebdriverRunner): def setUp(self): super().setUp() # Load the chrome webdriver - + self.options = webdriver.ChromeOptions() + self.options.add_argument("--headless=new") + self.driver = webdriver.Chrome(options=self.options) @tag('firefox') class FirefoxTestRunner(BaseWebdriverRunner): @@ -79,3 +108,11 @@ class FirefoxTestRunner(BaseWebdriverRunner): def setUp(self): super().setUp() # Load the firefox webdriver + self.options = webdriver.FirefoxOptions() + self.options.add_argument('-headless') + # Disable options that may break selenium + # see https://github.com/mozilla/geckodriver/releases/tag/v0.33.0 and + # https://github.com/SeleniumHQ/selenium/issues/11736 + self.options.set_preference('fission.bfcacheInParent', False) + self.options.set_preference('fission.webContentIsolationStrategy', 0) + self.driver = webdriver.Firefox(options=self.options) From 5f66155cf94672e1a42590c19ddc2db413c4722c Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Thu, 23 Nov 2023 14:02:34 +0200 Subject: [PATCH 05/16] Use reverse for the url. Also explicitly call close (recommended by selenium docs) --- wafer/tests/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/wafer/tests/utils.py b/wafer/tests/utils.py index 010d4143..c6027a24 100644 --- a/wafer/tests/utils.py +++ b/wafer/tests/utils.py @@ -1,5 +1,6 @@ """Utilities for testing wafer.""" +from django.urls import reverse from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission from django.test import tag @@ -68,7 +69,8 @@ def setUp(self): def _login(self, name, password): """Generic login handler""" - self.driver.get(f"{self.live_server_url}/accounts/login/") + login_url = reverse('auth_login') + self.driver.get(f"{self.live_server_url}{login_url}") WebDriverWait(self.driver, 10).until( expected_conditions.presence_of_element_located((By.NAME, "submit")) ) @@ -91,6 +93,9 @@ def admin_login(self): """Login as the admin user""" self._login(self.admin_user.username, self.admin_password) + def tearDown(self): + self.driver.close() + @tag('chrome') class ChromeTestRunner(BaseWebdriverRunner): @@ -102,6 +107,7 @@ def setUp(self): self.options.add_argument("--headless=new") self.driver = webdriver.Chrome(options=self.options) + @tag('firefox') class FirefoxTestRunner(BaseWebdriverRunner): From d7460f3d29acab0164e9bfdf69f9d74fb5f7db68 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Thu, 23 Nov 2023 14:04:02 +0200 Subject: [PATCH 06/16] Add login checks to schedule editor --- wafer/schedule/tests/test_schedule_editor.py | 56 +++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/wafer/schedule/tests/test_schedule_editor.py b/wafer/schedule/tests/test_schedule_editor.py index 4439b84c..b2085072 100644 --- a/wafer/schedule/tests/test_schedule_editor.py +++ b/wafer/schedule/tests/test_schedule_editor.py @@ -1,6 +1,19 @@ import datetime as D +try: + from selenium.webdriver.common.keys import Keys + from selenium.webdriver.common.by import By + from selenium.webdriver.support.ui import WebDriverWait + from selenium.webdriver.support import expected_conditions + from selenium.common.exceptions import NoSuchElementException +except ImportError: + # These need to be non-fatal so the tests can be always loaded + # and the check in the Runner base class will fail these + # if stuff isn't loaded + pass + from django.utils import timezone +from django.urls import reverse from wafer.pages.models import Page from wafer.tests.utils import create_user, ChromeTestRunner, FirefoxTestRunner @@ -28,7 +41,7 @@ def setUp(self): # 10-11 Item1 Item4 # 11-12 Item2 Item5 # 12-13 Item3 Item6 - # Day 2 + # Day 2 # Venue 1 Venue 2 Venue 3 # 10-11 Item7 Item10 Item13 # 11-12 Item8 Item11 Item14 @@ -102,20 +115,54 @@ def setUp(self): self.talk3 = create_talk('Test talk 3', status=ACCEPTED, username='jess') self.talk4 = create_talk('Test talk 4', status=ACCEPTED, username='jonah') + schedue_edit_url = reverse('admin:schedule_editor') + self.edit_page = f"{self.live_server_url}{schedue_edit_url}" + def test_access_schedule_editor_no_login(self): """Test that the schedule editor isn't accessible if not logged in""" + self.driver.get(self.edit_page) + header = WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.TAG_NAME, "h1")) + ) + self.assertEqual('Django administration', header.text) + login = self.driver.find_element(By.ID, "login-form") + self.assertIsNotNone(login) + self.assertIn('login', login.get_attribute('action')) + # Check that no admin info has loaded + with self.assertRaises(NoSuchElementException): + self.driver.find_element(By.ID, 'allTalks') def test_access_schedule_editor_no_super(self): """Test that the schedule editor isn't accessible for non-superuser accounts""" self.normal_login() + self.driver.get(self.edit_page) + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.TAG_NAME, "h1")) + ) + error = self.driver.find_element(By.CLASS_NAME, "errornote") + self.assertIn("authenticated as normal", error.text) + self.assertIn("not authorized to access this page", error.text) + # Check that no admin info has loaded + with self.assertRaises(NoSuchElementException): + self.driver.find_element(By.ID, 'allTalks') def test_access_schedule_editor_admin(self): """Test that the schedule editor is accessible for superuser accounts""" self.admin_login() + self.driver.get(self.edit_page) + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.TAG_NAME, "h1")) + ) + all_talks = self.driver.find_element(By.ID, "allTalks") + self.assertTrue(all_talks is not None) + # Ordering should ensure that this is always talk 1 + talk_1_block = all_talks.find_element(By.TAG_NAME, "div") + self.assertIn(self.talk1.title, talk_1_block.text) def test_drag_talk(self): """Test dragging talk behavior""" self.admin_login() + self.driver.get(self.edit_page) # Drag a talk from the siderbar to a slot in the schedule # Check that dragged talk is not on the list of unassigned talks # Drag a talk from one slot to another @@ -123,6 +170,7 @@ def test_drag_talk(self): def test_drag_page(self): """Test dragging page behavior""" self.admin_login() + self.driver.get(self.edit_page) # Drag a page from the siderbar to a slot in the schedule # Check that this doesn't change the unassigned list # Drag a page from one spot to another @@ -130,10 +178,12 @@ def test_drag_page(self): def test_swicth_day(self): """Test selecting different days""" self.admin_login() + self.driver.get(self.edit_page) def test_remove_talk(self): """Test removing talks""" self.admin_login() + self.driver.get(self.edit_page) # Test delete button # Check that unassigned list is updated correctly # Test dragging a talk over an existing talk @@ -142,13 +192,15 @@ def test_remove_talk(self): def test_remove_page(self): """Test removing pages""" self.admin_login() + self.driver.get(self.edit_page) # Test delete button # Test dragging a page over an existing page def test_create_invalid_schedule(self): """Test that an invalid schedule displays the errors""" self.admin_login() - + self.driver.get(self.edit_page) + class ChromeScheduleEditorTests(EditorTestsMixin, ChromeTestRunner): pass From f4bdda65399aae01cd2c0175d823d278c99bfea2 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Thu, 23 Nov 2023 14:04:17 +0200 Subject: [PATCH 07/16] Add boilerplate to datetime widget tests --- .../tests/test_schedule_datetime_widget.py | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/wafer/schedule/tests/test_schedule_datetime_widget.py b/wafer/schedule/tests/test_schedule_datetime_widget.py index 7e9182fc..431d8b66 100644 --- a/wafer/schedule/tests/test_schedule_datetime_widget.py +++ b/wafer/schedule/tests/test_schedule_datetime_widget.py @@ -1,6 +1,19 @@ import datetime as D +try: + from selenium.webdriver.common.keys import Keys + from selenium.webdriver.common.by import By + from selenium.webdriver.support.ui import WebDriverWait + from selenium.webdriver.support import expected_conditions +except ImportError: + # These need to be non-fatal so the tests can be always loaded + # and the check in the Runner base class will fail these + # if stuff isn't loaded + pass + + from django.utils import timezone +from django.urls import reverse from wafer.pages.models import Page from wafer.tests.utils import ChromeTestRunner, FirefoxTestRunner @@ -10,32 +23,52 @@ class ScheduleDateTimeJSMixin: - """Test the custom datetime widget""" + """Test the custom datetime entries work as expected""" def setUp(self): """Create venue, block and slots so we use the slot admin page""" super().setUp() - block1 = ScheduleBlock.objects.create( + self.block1 = ScheduleBlock.objects.create( start_time=D.datetime(2013, 9, 22, 7, 0, 0, tzinfo=D.timezone.utc), end_time=D.datetime(2013, 9, 22, 19, 0, 0, tzinfo=D.timezone.utc), ) venue1 = Venue.objects.create(order=1, name='Venue 1') - venue1.blocks.add(block1) + venue1.blocks.add(self.block1) - slot1 = Slot(start_time=D.datetime(2013, 9, 22, 12, 0, 0, + self.slot1 = Slot(start_time=D.datetime(2013, 9, 22, 12, 0, 0, tzinfo=D.timezone.utc), end_time=D.datetime(2013, 9, 22, 13, 0, 0, tzinfo=D.timezone.utc)) - slot2 = Slot(previous_slot=slot1, + self.slot2 = Slot(previous_slot=self.slot1, end_time=D.datetime(2013, 9, 22, 14, 0, 0, tzinfo=D.timezone.utc)) - def test_datetime_widget(self): + def test_datetime_widget_slot_admin(self): """Test that the datetime widget lists the desired times""" self.admin_login() - + # Navigate to the slot list page + slot_admin_list_url = reverse('admin:schedule_slot_changelist') + self.driver.get(f"{self.live_server_url}{slot_admin_list_url}") + # select the time widget + # Confirm that the expected entries are there + # Navigate to an indivual slot page + # We need to specify kwargs as an explicit dict + slot_admin_change_url = reverse('admin:schedule_slot_change', + kwargs={'object_id': self.slot1.id}) + self.driver.get(f"{self.live_server_url}{slot_admin_change_url}") + # Also check the widget there + + def test_datetime_widget_schedule_block_admin(self): + """Test that the datetime widget lists the desired times""" + self.admin_login() + # Navigate to the schedule block page + block_admin_change_url = reverse('admin:schedule_scheduleblock_change', + kwargs={'object_id': self.block1.id}) + self.driver.get(f"{self.live_server_url}{block_admin_change_url}") + # Check the widget there + class ChromeScheduleEditorTests(ScheduleDateTimeJSMixin, ChromeTestRunner): pass From 7c3b9a73d24b56c70838009395669aba6726d228 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Thu, 23 Nov 2023 15:01:23 +0200 Subject: [PATCH 08/16] Fill in clock widget tests --- .../tests/test_schedule_datetime_widget.py | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/wafer/schedule/tests/test_schedule_datetime_widget.py b/wafer/schedule/tests/test_schedule_datetime_widget.py index 431d8b66..f313ee0a 100644 --- a/wafer/schedule/tests/test_schedule_datetime_widget.py +++ b/wafer/schedule/tests/test_schedule_datetime_widget.py @@ -5,6 +5,7 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions + from selenium.common.exceptions import NoSuchElementException except ImportError: # These need to be non-fatal so the tests can be always loaded # and the check in the Runner base class will fail these @@ -34,6 +35,7 @@ def setUp(self): end_time=D.datetime(2013, 9, 22, 19, 0, 0, tzinfo=D.timezone.utc), ) + self.block1.save() venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.blocks.add(self.block1) @@ -41,24 +43,53 @@ def setUp(self): tzinfo=D.timezone.utc), end_time=D.datetime(2013, 9, 22, 13, 0, 0, tzinfo=D.timezone.utc)) + self.slot1.save() self.slot2 = Slot(previous_slot=self.slot1, end_time=D.datetime(2013, 9, 22, 14, 0, 0, tzinfo=D.timezone.utc)) - def test_datetime_widget_slot_admin(self): - """Test that the datetime widget lists the desired times""" + def check_clock_button(self): + """Standard check for the clock button contents""" + # Find the clock button + clock_button = WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.CLASS_NAME, 'clock-icon')) + ) + # Check that the list isn't visible before we click + clock_box = self.driver.find_element(By.ID, 'clockbox0') + style = clock_box.get_attribute('style') + self.assertIn('display: none', style) + clock_button.click() + timelist = WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.CLASS_NAME, 'timelist')) + ) + clock_box = self.driver.find_element(By.ID, 'clockbox0') + style = clock_box.get_attribute('style') + self.assertIn('display: block', style) + seen_times = [] + for li in timelist.find_elements(By.TAG_NAME, 'li'): + ahref = li.find_element(By.TAG_NAME, 'a') + seen_times.append(ahref.text.strip()) + for time in range(8, 21): + time_str = f'{time:02d}:00' + self.assertIn(time_str, seen_times) + + def test_datetime_widget_slot_admin_changelist(self): + """Test that the datetime widget lists the desired times in the list view""" self.admin_login() # Navigate to the slot list page slot_admin_list_url = reverse('admin:schedule_slot_changelist') self.driver.get(f"{self.live_server_url}{slot_admin_list_url}") - # select the time widget - # Confirm that the expected entries are there - # Navigate to an indivual slot page + self.check_clock_button() + + def test_datetime_widget_slot_admin_change(self): + """Test that the datetime widget lists the desired times + on the individual slot admin page""" # We need to specify kwargs as an explicit dict + self.admin_login() slot_admin_change_url = reverse('admin:schedule_slot_change', - kwargs={'object_id': self.slot1.id}) + kwargs={'object_id': self.slot1.pk}) self.driver.get(f"{self.live_server_url}{slot_admin_change_url}") - # Also check the widget there + self.check_clock_button() def test_datetime_widget_schedule_block_admin(self): """Test that the datetime widget lists the desired times""" @@ -67,7 +98,7 @@ def test_datetime_widget_schedule_block_admin(self): block_admin_change_url = reverse('admin:schedule_scheduleblock_change', kwargs={'object_id': self.block1.id}) self.driver.get(f"{self.live_server_url}{block_admin_change_url}") - # Check the widget there + self.check_clock_button() class ChromeScheduleEditorTests(ScheduleDateTimeJSMixin, ChromeTestRunner): From 72327ccc6a8bc1afbea1a8b939b131400fab5bad Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Thu, 23 Nov 2023 17:24:53 +0200 Subject: [PATCH 09/16] Switch to using a single selenium driver for all tests in a class --- wafer/tests/utils.py | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/wafer/tests/utils.py b/wafer/tests/utils.py index c6027a24..4c16dab7 100644 --- a/wafer/tests/utils.py +++ b/wafer/tests/utils.py @@ -55,10 +55,15 @@ def mock_avatar_url(self): @tag('selenium') class BaseWebdriverRunner(StaticLiveServerTestCase): - def setUp(self): - """Create an ordinary user and an admin user for testing""" + @classmethod + def setUpClass(cls): + """Create the driver instance""" if not webdriver: raise RuntimeError("Test requires selenium installed") + super().setUpClass() + + def setUp(self): + """Create an ordinary user and an admin user for testing""" super().setUp() self.admin_user = create_user('admin', email='admin@localhost', superuser=True) self.admin_password = 'admin_password' @@ -80,9 +85,8 @@ def _login(self, name, password): pass_field.send_keys(password) loginbut = self.driver.find_element(By.NAME, 'submit') loginbut.click() - #self.driver.get(f"{self.live_server_url}/") WebDriverWait(self.driver, 10).until( - expected_conditions.presence_of_element_located((By.PARTIAL_LINK_TEXT, "Log out")) + expected_conditions.presence_of_element_located((By.CLASS_NAME, "wafer-profile")) ) def normal_login(self): @@ -93,32 +97,36 @@ def admin_login(self): """Login as the admin user""" self._login(self.admin_user.username, self.admin_password) - def tearDown(self): - self.driver.close() + @classmethod + def tearDownClass(cls): + cls.driver.quit() + super().tearDownClass() @tag('chrome') class ChromeTestRunner(BaseWebdriverRunner): - def setUp(self): - super().setUp() + @classmethod + def setUpClass(cls): + super().setUpClass() # Load the chrome webdriver - self.options = webdriver.ChromeOptions() - self.options.add_argument("--headless=new") - self.driver = webdriver.Chrome(options=self.options) + cls.options = webdriver.ChromeOptions() + cls.options.add_argument("--headless=new") + cls.driver = webdriver.Chrome(options=cls.options) @tag('firefox') class FirefoxTestRunner(BaseWebdriverRunner): - def setUp(self): - super().setUp() + @classmethod + def setUpClass(cls): + super().setUpClass() # Load the firefox webdriver - self.options = webdriver.FirefoxOptions() - self.options.add_argument('-headless') + cls.options = webdriver.FirefoxOptions() + cls.options.add_argument('-headless') # Disable options that may break selenium # see https://github.com/mozilla/geckodriver/releases/tag/v0.33.0 and # https://github.com/SeleniumHQ/selenium/issues/11736 - self.options.set_preference('fission.bfcacheInParent', False) - self.options.set_preference('fission.webContentIsolationStrategy', 0) - self.driver = webdriver.Firefox(options=self.options) + cls.options.set_preference('fission.bfcacheInParent', False) + cls.options.set_preference('fission.webContentIsolationStrategy', 0) + cls.driver = webdriver.Firefox(options=cls.options) From c5d97735500441481ba4c7c2b5e51352d8fe23f0 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Fri, 24 Nov 2023 15:10:12 +0200 Subject: [PATCH 10/16] Fill in some drag tests --- wafer/schedule/tests/test_schedule_editor.py | 159 +++++++++++++++++-- 1 file changed, 146 insertions(+), 13 deletions(-) diff --git a/wafer/schedule/tests/test_schedule_editor.py b/wafer/schedule/tests/test_schedule_editor.py index b2085072..c74b9d24 100644 --- a/wafer/schedule/tests/test_schedule_editor.py +++ b/wafer/schedule/tests/test_schedule_editor.py @@ -1,6 +1,9 @@ import datetime as D +import time + try: + from selenium import webdriver from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait @@ -60,6 +63,9 @@ def setUp(self): tzinfo=D.timezone.utc), ) + block1.save() + block2.save() + venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.blocks.add(block1) venue1.blocks.add(block2) @@ -73,6 +79,9 @@ def setUp(self): self.venues = [venue1, venue2, venue3] + for x in self.venues: + x.save() + b1_start1 = D.datetime(2013, 9, 22, 10, 0, 0, tzinfo=D.timezone.utc) b1_start2 = D.datetime(2013, 9, 22, 11, 0, 0, @@ -110,6 +119,9 @@ def setUp(self): self.block1_slots = [b1_slot1, b1_slot2, b1_slot3] self.block2_slots = [b2_slot1, b2_slot2, b2_slot3] + for x in self.block1_slots + self.block2_slots: + x.save() + self.talk1 = create_talk('Test talk 1', status=ACCEPTED, username='john') self.talk2 = create_talk('Test talk 2', status=ACCEPTED, username='james') self.talk3 = create_talk('Test talk 3', status=ACCEPTED, username='jess') @@ -153,48 +165,169 @@ def test_access_schedule_editor_admin(self): WebDriverWait(self.driver, 10).until( expected_conditions.presence_of_element_located((By.TAG_NAME, "h1")) ) - all_talks = self.driver.find_element(By.ID, "allTalks") - self.assertTrue(all_talks is not None) + all_talks_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "All Talks") + all_talks_link.click() + tab_pane = None + for pane in self.driver.find_elements(By.CLASS_NAME, "tab-pane"): + if 'active' in pane.get_attribute('class'): + tab_pane = pane # Ordering should ensure that this is always talk 1 - talk_1_block = all_talks.find_element(By.TAG_NAME, "div") + talk_1_block = tab_pane.find_element(By.CLASS_NAME, "draggable") self.assertIn(self.talk1.title, talk_1_block.text) def test_drag_talk(self): """Test dragging talk behavior""" + self.assertEqual(ScheduleItem.objects.count(), 0) self.admin_login() self.driver.get(self.edit_page) - # Drag a talk from the siderbar to a slot in the schedule + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.TAG_NAME, "h1")) + ) + # partial link text to avoid whitespace fiddling + talks_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "Unassigned Talks") + talks_link.click() + tab_pane = None + for pane in self.driver.find_elements(By.CLASS_NAME, "tab-pane"): + if 'active' in pane.get_attribute('class'): + tab_pane = pane + break + # Find the first talk + source = None + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if self.talk1.title in x.text: + source = x + break + # Find the first schedule item in the schedule tabls + target = self.driver.find_element(By.ID, "scheduleItem") + actions = webdriver.ActionChains(self.driver) + actions.drag_and_drop(source, target) + # Pause briefly to make sure the server has a chance to do stuff + actions.pause(0.5) + actions.perform() + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.CLASS_NAME, "close")) + ) + self.assertEqual(ScheduleItem.objects.count(), 1) + item = ScheduleItem.objects.first() + self.assertEqual(item.talk, self.talk1) # Check that dragged talk is not on the list of unassigned talks - # Drag a talk from one slot to another + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + self.assertNotIn(self.talk1.title, x.text) + # Try drag the last talk + source = None + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if self.talk4.title in x.text: + source = x + break + # Find the first schedule item in the schedule tabls + target = self.driver.find_element(By.ID, "scheduleItem") + actions.reset_actions() + actions.drag_and_drop(source, target) + actions.pause(0.5) + actions.perform() + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.CLASS_NAME, "close")) + ) + self.assertEqual(ScheduleItem.objects.count(), 2) + item = ScheduleItem.objects.last() + self.assertEqual(item.talk, self.talk4) + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + self.assertNotIn(self.talk1.title, x.text) + self.assertNotIn(self.talk4.title, x.text) def test_drag_page(self): """Test dragging page behavior""" + self.assertEqual(ScheduleItem.objects.count(), 0) self.admin_login() self.driver.get(self.edit_page) + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.TAG_NAME, "h1")) + ) # Drag a page from the siderbar to a slot in the schedule - # Check that this doesn't change the unassigned list - # Drag a page from one spot to another + talks_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "Pages") + talks_link.click() + tab_pane = None + for pane in self.driver.find_elements(By.CLASS_NAME, "tab-pane"): + if 'active' in pane.get_attribute('class'): + tab_pane = pane + break + # Find the first page + source = None + tab_items = [] + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + # We choose page 5 so we're dragging from the middle of the list + if 'test page 5' in x.text: + source = x + tab_items.append(x.text) + # Find the first schedule item in the schedule tabls + target = self.driver.find_element(By.ID, "scheduleItem") + actions = webdriver.ActionChains(self.driver) + actions.drag_and_drop(source, target) + actions.pause(0.5) + actions.perform() + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.CLASS_NAME, "close")) + ) + self.assertEqual(ScheduleItem.objects.count(), 1) + item = ScheduleItem.objects.first() + self.assertEqual(item.page, self.pages[5]) + # Check that this hasn't changed the page tab list + post_drag_tab_items = [] + found = False + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if 'test page 5' in x.text: + found = True + post_drag_tab_items.append(x.text) + self.assertTrue(found) + self.assertEqual(tab_items, post_drag_tab_items) + # Try drag the last page + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if 'test page 11' in x.text: + source = x + break + # Find the first schedule item in the schedule tabls + target = self.driver.find_element(By.ID, "scheduleItem") + actions = webdriver.ActionChains(self.driver) + actions.drag_and_drop(source, target) + actions.pause(0.5) + actions.perform() + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.CLASS_NAME, "close")) + ) + self.assertEqual(ScheduleItem.objects.count(), 2) + item = ScheduleItem.objects.last() + self.assertEqual(item.page, self.pages[11]) + # Check that this hasn't changed the page tab list + post_drag_tab_items = [] + found = False + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if 'test page 5' in x.text: + found = True + if 'test page 11' in x.text: + found = True + post_drag_tab_items.append(x.text) + self.assertTrue(found) + self.assertEqual(tab_items, post_drag_tab_items) def test_swicth_day(self): """Test selecting different days""" self.admin_login() self.driver.get(self.edit_page) - def test_remove_talk(self): - """Test removing talks""" + def test_drag_over(self): + """Test that dragging over an item replaces it""" self.admin_login() self.driver.get(self.edit_page) - # Test delete button # Check that unassigned list is updated correctly # Test dragging a talk over an existing talk # Check that unassigned list is updated correctly - def test_remove_page(self): - """Test removing pages""" + def test_drag_over_page(self): + """Test that dragging over a page with another page works""" self.admin_login() self.driver.get(self.edit_page) - # Test delete button # Test dragging a page over an existing page + # Check that page list is unchanged def test_create_invalid_schedule(self): """Test that an invalid schedule displays the errors""" From 4926e07ca1f831f8e6e0c3bac858145191db1b2b Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Fri, 24 Nov 2023 15:59:54 +0200 Subject: [PATCH 11/16] Fix test logic --- wafer/schedule/tests/test_schedule_editor.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/wafer/schedule/tests/test_schedule_editor.py b/wafer/schedule/tests/test_schedule_editor.py index c74b9d24..ba19e5ba 100644 --- a/wafer/schedule/tests/test_schedule_editor.py +++ b/wafer/schedule/tests/test_schedule_editor.py @@ -299,31 +299,40 @@ def test_drag_page(self): self.assertEqual(item.page, self.pages[11]) # Check that this hasn't changed the page tab list post_drag_tab_items = [] - found = False + found1 = False + found2 = False for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): if 'test page 5' in x.text: - found = True + found1 = True if 'test page 11' in x.text: - found = True + found2 = True post_drag_tab_items.append(x.text) - self.assertTrue(found) + self.assertTrue(found1) + self.assertTrue(found2) self.assertEqual(tab_items, post_drag_tab_items) def test_swicth_day(self): """Test selecting different days""" + # Create a couple of schedule items on each day + # Load schedule page self.admin_login() self.driver.get(self.edit_page) + # Verify we see the expected schedule items on day 1 + # Switch day + # Verify we see the expected schedule items on day 2 + def test_drag_over(self): """Test that dragging over an item replaces it""" + # Create a schedule with a single item self.admin_login() self.driver.get(self.edit_page) - # Check that unassigned list is updated correctly # Test dragging a talk over an existing talk # Check that unassigned list is updated correctly def test_drag_over_page(self): """Test that dragging over a page with another page works""" + # Create a schedule with a single item self.admin_login() self.driver.get(self.edit_page) # Test dragging a page over an existing page From 86877e9d07983181e9001fd74f92f978e95bd5c8 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Mon, 27 Nov 2023 10:22:40 +0200 Subject: [PATCH 12/16] Tweak page numbers to avoid some size issues. Flag tests we expect to fail currently --- wafer/schedule/tests/test_schedule_editor.py | 48 +++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/wafer/schedule/tests/test_schedule_editor.py b/wafer/schedule/tests/test_schedule_editor.py index ba19e5ba..48e057cc 100644 --- a/wafer/schedule/tests/test_schedule_editor.py +++ b/wafer/schedule/tests/test_schedule_editor.py @@ -15,6 +15,8 @@ # if stuff isn't loaded pass +from unittest import expectedFailure + from django.utils import timezone from django.urls import reverse @@ -114,7 +116,7 @@ def setUp(self): b2_slot3 = Slot(previous_slot=b2_slot2, end_time=b2_end) - self.pages = make_pages(12) + self.pages = make_pages(6) self.block1_slots = [b1_slot1, b1_slot2, b1_slot3] self.block2_slots = [b2_slot1, b2_slot2, b2_slot3] @@ -256,7 +258,7 @@ def test_drag_page(self): tab_items = [] for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): # We choose page 5 so we're dragging from the middle of the list - if 'test page 5' in x.text: + if 'test page 3' in x.text: source = x tab_items.append(x.text) # Find the first schedule item in the schedule tabls @@ -270,19 +272,19 @@ def test_drag_page(self): ) self.assertEqual(ScheduleItem.objects.count(), 1) item = ScheduleItem.objects.first() - self.assertEqual(item.page, self.pages[5]) + self.assertEqual(item.page, self.pages[3]) # Check that this hasn't changed the page tab list post_drag_tab_items = [] found = False for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): - if 'test page 5' in x.text: + if 'test page 3' in x.text: found = True post_drag_tab_items.append(x.text) self.assertTrue(found) self.assertEqual(tab_items, post_drag_tab_items) # Try drag the last page for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): - if 'test page 11' in x.text: + if 'test page 5' in x.text: source = x break # Find the first schedule item in the schedule tabls @@ -296,15 +298,15 @@ def test_drag_page(self): ) self.assertEqual(ScheduleItem.objects.count(), 2) item = ScheduleItem.objects.last() - self.assertEqual(item.page, self.pages[11]) + self.assertEqual(item.page, self.pages[5]) # Check that this hasn't changed the page tab list post_drag_tab_items = [] found1 = False found2 = False for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): - if 'test page 5' in x.text: + if 'test page 3' in x.text: found1 = True - if 'test page 11' in x.text: + if 'test page 5' in x.text: found2 = True post_drag_tab_items.append(x.text) self.assertTrue(found1) @@ -321,14 +323,16 @@ def test_swicth_day(self): # Switch day # Verify we see the expected schedule items on day 2 - - def test_drag_over(self): + @expectedFailure + def test_drag_over_talk(self): """Test that dragging over an item replaces it""" + # Expected to fail - see https://github.com/CTPUG/wafer/issues/689 # Create a schedule with a single item self.admin_login() self.driver.get(self.edit_page) # Test dragging a talk over an existing talk # Check that unassigned list is updated correctly + # FIXME: The schedule editor doesn't do this correctly def test_drag_over_page(self): """Test that dragging over a page with another page works""" @@ -338,10 +342,30 @@ def test_drag_over_page(self): # Test dragging a page over an existing page # Check that page list is unchanged - def test_create_invalid_schedule(self): - """Test that an invalid schedule displays the errors""" + @expectedFailure + def test_adding_clash(self): + """Test that introducing a speaker clash causes the + error section to be updated""" + # Expected to fail -see https://github.com/CTPUG/wafer/issues/158 + # Create initial schedule + self.admin_login() + self.driver.get(self.edit_page) + # Drag a talk into a clashing slot + # Verify errors are present + # FIXME: The schedule editor doesn't currently update this + + @expectedFailure + def test_removing_clash(self): + """Test that removing a speaker clash causes the + error section to be cleared""" + # Expected to fail -see https://github.com/CTPUG/wafer/issues/158 + # Create initial schedule self.admin_login() self.driver.get(self.edit_page) + # Verify we have the expected errors + # Delete one of the clashes + # Verify errors are gone + # FIXME: The schedule editor doesn't currently update this class ChromeScheduleEditorTests(EditorTestsMixin, ChromeTestRunner): From e75905560296fb691d336b2aeb403ee31f2f206c Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Mon, 27 Nov 2023 18:26:36 +0200 Subject: [PATCH 13/16] Add day switching tests --- wafer/schedule/tests/test_schedule_editor.py | 56 ++++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/wafer/schedule/tests/test_schedule_editor.py b/wafer/schedule/tests/test_schedule_editor.py index 48e057cc..dba2dc24 100644 --- a/wafer/schedule/tests/test_schedule_editor.py +++ b/wafer/schedule/tests/test_schedule_editor.py @@ -40,17 +40,6 @@ def setUp(self): """Create two day table with 3 slots each and 2 venues and create some page and talks to populate the schedul""" super().setUp() - # Schedule is - # Day1 - # Venue 1 Venue 2 - # 10-11 Item1 Item4 - # 11-12 Item2 Item5 - # 12-13 Item3 Item6 - # Day 2 - # Venue 1 Venue 2 Venue 3 - # 10-11 Item7 Item10 Item13 - # 11-12 Item8 Item11 Item14 - # 12-13 Item9 Item12 Item15 block1 = ScheduleBlock.objects.create( start_time=D.datetime(2013, 9, 22, 7, 0, 0, tzinfo=D.timezone.utc), @@ -316,12 +305,57 @@ def test_drag_page(self): def test_swicth_day(self): """Test selecting different days""" # Create a couple of schedule items on each day + item1 = ScheduleItem.objects.create(venue=self.venues[0], + talk_id=self.talk1.pk) + + item2 = ScheduleItem.objects.create(venue=self.venues[1], + talk_id=self.talk2.pk) + item1.slots.add(self.block1_slots[0]) + item2.slots.add(self.block1_slots[1]) + + item3 = ScheduleItem.objects.create(venue=self.venues[0], + page_id=self.pages[0].pk) + item4 = ScheduleItem.objects.create(venue=self.venues[1], + page_id=self.pages[1].pk) + item5 = ScheduleItem.objects.create(venue=self.venues[2], + page_id=self.pages[2].pk) + item3.slots.add(self.block2_slots[0]) + item4.slots.add(self.block2_slots[1]) + item5.slots.add(self.block2_slots[1]) + # Load schedule page self.admin_login() self.driver.get(self.edit_page) # Verify we see the expected schedule items on day 1 + td1 = self.driver.find_element(By.ID, f"scheduleItem{item1.pk}") + self.assertEqual(td1.tag_name, 'td') + td2 = self.driver.find_element(By.ID, f"scheduleItem{item2.pk}") + self.assertEqual(td2.tag_name, 'td') + with self.assertRaises(NoSuchElementException): + self.driver.find_element(By.ID, f"scheduleItem{item3.pk}") # Switch day + buttons = self.driver.find_elements(By.TAG_NAME, 'button') + self.assertIn('Sep', buttons[1].text) + buttons[1].click() + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.CLASS_NAME, "show")) + ) + days = self.driver.find_elements(By.PARTIAL_LINK_TEXT, "Sep") + days[1].click() + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.CLASS_NAME, "close")) + ) # Verify we see the expected schedule items on day 2 + td3 = self.driver.find_element(By.ID, f"scheduleItem{item3.pk}") + self.assertEqual(td3.tag_name, 'td') + td4 = self.driver.find_element(By.ID, f"scheduleItem{item4.pk}") + self.assertEqual(td4.tag_name, 'td') + td5 = self.driver.find_element(By.ID, f"scheduleItem{item5.pk}") + self.assertEqual(td5.tag_name, 'td') + with self.assertRaises(NoSuchElementException): + self.driver.find_element(By.ID, f"scheduleItem{item1.pk}") + with self.assertRaises(NoSuchElementException): + self.driver.find_element(By.ID, f"scheduleItem{item2.pk}") @expectedFailure def test_drag_over_talk(self): From a74f6bce57cfdfe4059929f6b895424eff86dfce Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Mon, 27 Nov 2023 22:27:59 +0200 Subject: [PATCH 14/16] Add drag-over tests --- wafer/schedule/tests/test_schedule_editor.py | 131 +++++++++++++++---- 1 file changed, 102 insertions(+), 29 deletions(-) diff --git a/wafer/schedule/tests/test_schedule_editor.py b/wafer/schedule/tests/test_schedule_editor.py index dba2dc24..a8953372 100644 --- a/wafer/schedule/tests/test_schedule_editor.py +++ b/wafer/schedule/tests/test_schedule_editor.py @@ -121,6 +121,14 @@ def setUp(self): schedue_edit_url = reverse('admin:schedule_editor') self.edit_page = f"{self.live_server_url}{schedue_edit_url}" + def _start(self): + """Helper method to login as admin and load the editor""" + self.admin_login() + self.driver.get(self.edit_page) + WebDriverWait(self.driver, 10).until( + expected_conditions.presence_of_element_located((By.TAG_NAME, "h1")) + ) + def test_access_schedule_editor_no_login(self): """Test that the schedule editor isn't accessible if not logged in""" self.driver.get(self.edit_page) @@ -151,11 +159,7 @@ def test_access_schedule_editor_no_super(self): def test_access_schedule_editor_admin(self): """Test that the schedule editor is accessible for superuser accounts""" - self.admin_login() - self.driver.get(self.edit_page) - WebDriverWait(self.driver, 10).until( - expected_conditions.presence_of_element_located((By.TAG_NAME, "h1")) - ) + self._start() all_talks_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "All Talks") all_talks_link.click() tab_pane = None @@ -169,11 +173,7 @@ def test_access_schedule_editor_admin(self): def test_drag_talk(self): """Test dragging talk behavior""" self.assertEqual(ScheduleItem.objects.count(), 0) - self.admin_login() - self.driver.get(self.edit_page) - WebDriverWait(self.driver, 10).until( - expected_conditions.presence_of_element_located((By.TAG_NAME, "h1")) - ) + self._start() # partial link text to avoid whitespace fiddling talks_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "Unassigned Talks") talks_link.click() @@ -229,14 +229,10 @@ def test_drag_talk(self): def test_drag_page(self): """Test dragging page behavior""" self.assertEqual(ScheduleItem.objects.count(), 0) - self.admin_login() - self.driver.get(self.edit_page) - WebDriverWait(self.driver, 10).until( - expected_conditions.presence_of_element_located((By.TAG_NAME, "h1")) - ) + self._start() # Drag a page from the siderbar to a slot in the schedule - talks_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "Pages") - talks_link.click() + page_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "Pages") + page_link.click() tab_pane = None for pane in self.driver.find_elements(By.CLASS_NAME, "tab-pane"): if 'active' in pane.get_attribute('class'): @@ -324,8 +320,7 @@ def test_swicth_day(self): item5.slots.add(self.block2_slots[1]) # Load schedule page - self.admin_login() - self.driver.get(self.edit_page) + self._start() # Verify we see the expected schedule items on day 1 td1 = self.driver.find_element(By.ID, f"scheduleItem{item1.pk}") self.assertEqual(td1.tag_name, 'td') @@ -362,19 +357,99 @@ def test_drag_over_talk(self): """Test that dragging over an item replaces it""" # Expected to fail - see https://github.com/CTPUG/wafer/issues/689 # Create a schedule with a single item - self.admin_login() - self.driver.get(self.edit_page) + item1 = ScheduleItem.objects.create(venue=self.venues[0], + talk_id=self.talk1.pk) + item1.slots.add(self.block1_slots[0]) + item1.save() + self._start() # Test dragging a talk over an existing talk - # Check that unassigned list is updated correctly + target = self.driver.find_element(By.ID, f"scheduleItem{item1.pk}") + talks_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "Unassigned Talks") + talks_link.click() + tab_pane = None + for pane in self.driver.find_elements(By.CLASS_NAME, "tab-pane"): + if 'active' in pane.get_attribute('class'): + tab_pane = pane + break + # Find the first talk + source = None + found = False + # Find the second talk and verify that talk 1 is not in + # the Unassigned Talk list + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if self.talk2.title in x.text: + source = x + if self.talk1.title in x.text: + found = True + self.assertFalse(found) + # Do the drag + actions = webdriver.ActionChains(self.driver) + actions.drag_and_drop(source, target) + actions.pause(0.5) + actions.perform() + # Check that schedule item and table entry are correct + item = ScheduleItem.objects.first() + self.assertEqual(item.talk, self.talk2) + target = self.driver.find_element(By.ID, f"scheduleItem{item1.pk}") + self.assertIn(self.talk2.title, target.text) + # Check that Unassigned list has been updated correctly + # (Talk 1 added, and talk 2 removed) + found2 = False + found1 = False + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if self.talk1.title in x.text: + found1 = True + if self.talk2.title in x.text: + found2 = True + self.assertFalse(found2) # FIXME: The schedule editor doesn't do this correctly + self.assertTrue(found1) def test_drag_over_page(self): """Test that dragging over a page with another page works""" # Create a schedule with a single item - self.admin_login() - self.driver.get(self.edit_page) + self._start() + + item1 = ScheduleItem.objects.create(venue=self.venues[0], + page_id=self.pages[0].pk) + item1.slots.add(self.block1_slots[0]) + item1.save() + self._start() # Test dragging a page over an existing page - # Check that page list is unchanged + target = self.driver.find_element(By.ID, f"scheduleItem{item1.pk}") + page_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "Pages") + page_link.click() + tab_pane = None + for pane in self.driver.find_elements(By.CLASS_NAME, "tab-pane"): + if 'active' in pane.get_attribute('class'): + tab_pane = pane + break + # Find the source page + source = None + # Find the second talk and verify that talk 1 is not in + # the Unassigned Talk list + before = [] + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if 'test page 3' in x.text: + source = x + before.append(x.text) + # Do the drag + actions = webdriver.ActionChains(self.driver) + actions.drag_and_drop(source, target) + actions.pause(0.5) + actions.perform() + # Check that schedule item and table entry are correct + item = ScheduleItem.objects.first() + self.assertEqual(item.page, self.pages[3]) + target = self.driver.find_element(By.ID, f"scheduleItem{item1.pk}") + self.assertIn('test page 3', target.text) + # Check that the list is unchanged + after = [] + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if 'test page 3' in x.text: + source = x + after.append(x.text) + self.assertEqual(before, after) @expectedFailure def test_adding_clash(self): @@ -382,8 +457,7 @@ def test_adding_clash(self): error section to be updated""" # Expected to fail -see https://github.com/CTPUG/wafer/issues/158 # Create initial schedule - self.admin_login() - self.driver.get(self.edit_page) + self._start() # Drag a talk into a clashing slot # Verify errors are present # FIXME: The schedule editor doesn't currently update this @@ -394,8 +468,7 @@ def test_removing_clash(self): error section to be cleared""" # Expected to fail -see https://github.com/CTPUG/wafer/issues/158 # Create initial schedule - self.admin_login() - self.driver.get(self.edit_page) + self._start() # Verify we have the expected errors # Delete one of the clashes # Verify errors are gone From 20b38f761ff57f228b08eb0403e370970e806390 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Mon, 27 Nov 2023 22:52:58 +0200 Subject: [PATCH 15/16] Add tests for updating validation info --- wafer/schedule/tests/test_schedule_editor.py | 80 +++++++++++++++++++- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/wafer/schedule/tests/test_schedule_editor.py b/wafer/schedule/tests/test_schedule_editor.py index a8953372..fe3a8619 100644 --- a/wafer/schedule/tests/test_schedule_editor.py +++ b/wafer/schedule/tests/test_schedule_editor.py @@ -113,10 +113,11 @@ def setUp(self): for x in self.block1_slots + self.block2_slots: x.save() - self.talk1 = create_talk('Test talk 1', status=ACCEPTED, username='john') + self.author_john = create_user('john') + self.talk1 = create_talk('Test talk 1', status=ACCEPTED, user=self.author_john) self.talk2 = create_talk('Test talk 2', status=ACCEPTED, username='james') self.talk3 = create_talk('Test talk 3', status=ACCEPTED, username='jess') - self.talk4 = create_talk('Test talk 4', status=ACCEPTED, username='jonah') + self.talk4 = create_talk('Test talk 4', status=ACCEPTED, user=self.author_john) schedue_edit_url = reverse('admin:schedule_editor') self.edit_page = f"{self.live_server_url}{schedue_edit_url}" @@ -457,10 +458,51 @@ def test_adding_clash(self): error section to be updated""" # Expected to fail -see https://github.com/CTPUG/wafer/issues/158 # Create initial schedule + item1 = ScheduleItem.objects.create(venue=self.venues[0], + talk_id=self.talk1.pk) + item1.slots.add(self.block1_slots[0]) + item1.save() + item2 = ScheduleItem.objects.create(venue=self.venues[1], + talk_id=self.talk2.pk) + item2.slots.add(self.block1_slots[0]) + item2.save() self._start() + # Verify that there are no validation errors + with self.assertRaises(NoSuchElementException): + self.driver.find_element(By.TAG_NAME, "strong") # Drag a talk into a clashing slot + target = self.driver.find_element(By.ID, f"scheduleItem{item2.pk}") + talks_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "Unassigned Talks") + talks_link.click() + tab_pane = None + for pane in self.driver.find_elements(By.CLASS_NAME, "tab-pane"): + if 'active' in pane.get_attribute('class'): + tab_pane = pane + break + # Find the first talk + source = None + found = False + # Find the second talk and verify that talk 1 is not in + # the Unassigned Talk list + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if self.talk4.title in x.text: + source = x + if self.talk1.title in x.text: + found = True + self.assertFalse(found) + # Do the drag + actions = webdriver.ActionChains(self.driver) + actions.drag_and_drop(source, target) + actions.pause(0.5) + actions.perform() # Verify errors are present # FIXME: The schedule editor doesn't currently update this + try: + vaidation = self.driver.find_element(By.TAG_NAME, "strong") + except NoSuchElementException: + vaidation = None + self.assertNotNone(vaidation) + self.assertIn('Validation errors:', validation.text) @expectedFailure def test_removing_clash(self): @@ -468,11 +510,41 @@ def test_removing_clash(self): error section to be cleared""" # Expected to fail -see https://github.com/CTPUG/wafer/issues/158 # Create initial schedule + item1 = ScheduleItem.objects.create(venue=self.venues[0], + talk_id=self.talk1.pk) + item1.slots.add(self.block1_slots[0]) + item1.save() + item2 = ScheduleItem.objects.create(venue=self.venues[1], + talk_id=self.talk4.pk) + item2.slots.add(self.block1_slots[0]) + item2.save() self._start() - # Verify we have the expected errors - # Delete one of the clashes + # Verify that there are no validation errors + validation = self.driver.find_element(By.TAG_NAME, "strong") + self.assertIn('Validation errors:', validation.text) + # Delete the clashing talk + target = self.driver.find_element(By.ID, f"scheduleItem{item2.pk}") + close = target.find_element(By.CLASS_NAME, 'close') + close.click() + talks_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "Unassigned Talks") + talks_link.click() + tab_pane = None + for pane in self.driver.find_elements(By.CLASS_NAME, "tab-pane"): + if 'active' in pane.get_attribute('class'): + tab_pane = pane + break + # Verify we've deleted the clashing talk + found = None + # Find the second talk and verify that talk 1 is not in + # the Unassigned Talk list + for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'): + if self.talk4.title in x.text: + found = True + self.assertTrue(found) # Verify errors are gone # FIXME: The schedule editor doesn't currently update this + with self.assertRaises(NoSuchElementException): + self.driver.find_element(By.TAG_NAME, "strong") class ChromeScheduleEditorTests(EditorTestsMixin, ChromeTestRunner): From f9573707a711705ac53564b658595942780f39d3 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Mon, 27 Nov 2023 23:01:08 +0200 Subject: [PATCH 16/16] Update readme --- README.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.rst b/README.rst index b65a94d4..eedfc69e 100644 --- a/README.rst +++ b/README.rst @@ -118,3 +118,16 @@ Translation Translations for wafer are managed at `weblate.org`_ .. _weblate.org: https://hosted.weblate.org/projects/wafer/ + + +Selenium tests +============== + +wafer includes a small set of selenium tests to test various bits of javascript +used in the site (mostly in the schedule editor). + +To run the tests, you will need to install selenium - ``pip install selenium`` +and also run ``rpm install`` to install the required javascript dependencies. + +The tests can be run using the ``selenium`` tag, or using the individual browser +tags (currently ``firefox`` and ``chrome``).