datatracker/ietf/meeting/tests_js.py
Matthew Holloway de494790b6
fix: Selenium tests via scroll_and_click (#8150)
* fix: selenium tests scroll_and_click

* fix: reduce default timeout to 5 seconds

* fix: also use scroll_and_click on test_upcoming_materials_modal

* fix: remove conditional check on restoring scroll CSS

* fix: restore conditional check on restoring scroll CSS

* chore: code comments and adding jstest.py to coverage ignore
2024-11-05 15:03:21 +00:00

1677 lines
79 KiB
Python

# Copyright The IETF Trust 2014-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import time
import datetime
import shutil
import tempfile
import re
from django.utils import timezone
from django.utils.text import slugify
from django.db.models import F
import pytz
from django.conf import settings
from django.test.utils import override_settings
import debug # pyflakes:ignore
from ietf.doc.factories import DocumentFactory
from ietf.person.models import Person
from ietf.group.models import Group
from ietf.group.factories import GroupFactory
from ietf.meeting.factories import ( MeetingFactory, RoomFactory, SessionFactory, TimeSlotFactory,
ProceedingsMaterialFactory, ScheduleFactory, ConstraintFactory )
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting
from ietf.meeting.models import (Schedule, SchedTimeSessAssignment, Session,
Room, TimeSlot, Constraint, ConstraintName,
Meeting, SchedulingEvent, SessionStatusName)
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.utils.test_utils import assert_ical_response_is_valid
from ietf.utils.jstest import ( IetfSeleniumTestCase, ifSeleniumEnabled, selenium_enabled,
presence_of_element_child_by_css_selector )
from ietf.utils.timezone import datetime_today, datetime_from_date, date_today, timezone_not_near_midnight
if selenium_enabled():
from selenium.webdriver.common.action_chains import ActionChains
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 TimeoutException
# from selenium.webdriver.common.keys import Keys
@ifSeleniumEnabled
@override_settings(MEETING_SESSION_LOCK_TIME=datetime.timedelta(minutes=10))
class EditMeetingScheduleTests(IetfSeleniumTestCase):
def test_edit_meeting_schedule(self):
meeting = make_meeting_test_data()
schedule = Schedule.objects.filter(meeting=meeting, owner__user__username="plain").first()
room1 = Room.objects.get(name="Test Room")
slot1 = TimeSlot.objects.filter(meeting=meeting, location=room1, type='regular').order_by('time').first()
slot1b = TimeSlot.objects.filter(meeting=meeting, location=room1, type='regular').order_by('time').last()
self.assertNotEqual(slot1.pk, slot1b.pk)
room2 = Room.objects.create(meeting=meeting, name="Test Room2", capacity=1)
room2.session_types.add('regular')
slot2 = TimeSlot.objects.create(
meeting=meeting,
type_id='regular',
location=room2,
duration=datetime.timedelta(hours=2),
time=slot1.time - datetime.timedelta(minutes=10),
)
slot3 = TimeSlot.objects.create(
meeting=meeting,
type_id='regular',
location=room2,
duration=datetime.timedelta(hours=2),
time=max(slot1.end_time(), slot2.end_time()) + datetime.timedelta(minutes=10),
)
slot4 = TimeSlot.objects.create(
meeting=meeting,
type_id='regular',
location=room1,
duration=datetime.timedelta(hours=2),
time=slot1.time + datetime.timedelta(days=1),
)
s1, s2 = Session.objects.filter(meeting=meeting, type='regular')
s2.requested_duration = slot2.duration + datetime.timedelta(minutes=10)
s2.save()
SchedTimeSessAssignment.objects.filter(session=s1).delete()
s2b = SessionFactory(
meeting=meeting,
group=s2.group,
attendees=10,
requested_duration=datetime.timedelta(minutes=60),
add_to_schedule=False,
)
SchedulingEvent.objects.create(
session=s2b,
status=SessionStatusName.objects.get(slug='appr'),
by=Person.objects.get(name='(System)'),
)
Constraint.objects.create(
meeting=meeting,
source=s1.group,
target=s2.group,
name=ConstraintName.objects.get(slug="conflict"),
)
self.login()
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email()))
self.driver.get(url)
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.edit-meeting-schedule')))
self.assertEqual(len(self.driver.find_elements(By.CSS_SELECTOR, '.session.purpose-regular')), 3)
# select - show session info
s2_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2.pk))
s2b_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2b.pk))
self.assertNotIn('other-session-selected', s2b_element.get_attribute('class'))
s2_element.click()
# other session for group should be flagged for highlighting
s2b_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2b.pk))
self.assertIn('other-session-selected', s2b_element.get_attribute('class'))
# other session for group should appear in the info panel
session_info_container = self.driver.find_element(By.CSS_SELECTOR, '.session-info-container')
self.assertIn(s2.group.acronym, session_info_container.find_element(By.CSS_SELECTOR, ".title").text)
self.assertEqual(session_info_container.find_element(By.CSS_SELECTOR, ".other-session .time").text, "not yet scheduled")
# deselect
self.driver.find_element(By.CSS_SELECTOR, '.timeslot[data-type="regular"] .drop-target').click()
self.assertEqual(session_info_container.find_elements(By.CSS_SELECTOR, ".title"), [])
self.assertNotIn('other-session-selected', s2b_element.get_attribute('class'))
# unschedule
# we would like to do
#
# unassigned_sessions_element = self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions')
# ActionChains(self.driver).drag_and_drop(s2_element, unassigned_sessions_element).perform()
#
# but unfortunately, Selenium does not simulate drag and drop events, see
#
# https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/3604
#
# so for the time being we inject the Javascript workaround here and do it from JS
#
# https://storage.googleapis.com/google-code-attachments/selenium/issue-3604/comment-9/drag_and_drop_helper.js
self.driver.execute_script('!function(s){s.fn.simulateDragDrop=function(t){return this.each(function(){new s.simulateDragDrop(this,t)})},s.simulateDragDrop=function(t,a){this.options=a,this.simulateEvent(t,a)},s.extend(s.simulateDragDrop.prototype,{simulateEvent:function(t,a){var e="dragstart",n=this.createEvent(e);this.dispatchEvent(t,e,n),e="drop";var r=this.createEvent(e,{});r.dataTransfer=n.dataTransfer,this.dispatchEvent(s(a.dropTarget)[0],e,r),e="dragend";var i=this.createEvent(e,{});i.dataTransfer=n.dataTransfer,this.dispatchEvent(t,e,i)},createEvent:function(t){var a=document.createEvent("CustomEvent");return a.initCustomEvent(t,!0,!0,null),a.dataTransfer={data:{},types:[],setData:function(t,a){this.data[t]=a;this.types.includes(t)||this.types.push(t)},getData:function(t){return this.data[t]}},a},dispatchEvent:function(t,a,e){t.dispatchEvent?t.dispatchEvent(e):t.fireEvent&&t.fireEvent("on"+a,e)}})}(jQuery);')
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '.unassigned-sessions .drop-target'}});".format(s2.pk))
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.unassigned-sessions #session{}'.format(s2.pk))))
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(session=s2, schedule=schedule)), [])
# sorting unassigned
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.acronym, s.requested_duration, s.pk))]
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=name]').click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{} + #session{}'.format(*sorted_pks)))
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))]
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=parent]').click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.requested_duration, s.group.parent.acronym, s.group.acronym, s.pk))]
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=duration]').click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} ~ #session{}'.format(*sorted_pks)))
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (int(bool(s.comments)), s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))]
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=comments]').click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
# schedule
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s2.pk, slot1.pk))
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1.pk, s2.pk))))
assignment = SchedTimeSessAssignment.objects.get(session=s2, schedule=schedule)
self.assertEqual(assignment.timeslot, slot1)
# timeslot constraint hints when selected
s1_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s1.pk))
s1_element.click()
# violated due to constraints - both the timeslot and its timeslot label
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{}.would-violate-hint'.format(slot1.pk)))
# Find the timeslot label for slot1 - it's the first timeslot in the room group containing room 1
slot1_roomgroup_elt = self.driver.find_element(By.CSS_SELECTOR,
f'.day-flow .day:first-child .room-group[data-rooms="{room1.pk}"]'
)
self.assertTrue(
slot1_roomgroup_elt.find_elements(By.CSS_SELECTOR,
'.time-header > .time-label.would-violate-hint:first-child'
),
'Timeslot header label should show a would-violate hint for a constraint violation'
)
# violated due to missing capacity
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{}.would-violate-hint'.format(slot3.pk)))
# Find the timeslot label for slot3 - it's the second timeslot in the second room group
slot3_roomgroup_elt = self.driver.find_element(By.CSS_SELECTOR,
'.day-flow .day:first-child .room-group:nth-child(3)' # count from 2 - first-child is the day label
)
self.assertFalse(
slot3_roomgroup_elt.find_elements(By.CSS_SELECTOR,
'.time-header > .time-label.would-violate-hint:nth-child(2)'
),
'Timeslot header label should not show a would-violate hint for room capacity violation'
)
# reschedule
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s2.pk, slot2.pk))
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot2.pk, s2.pk))))
assignment = SchedTimeSessAssignment.objects.get(session=s2, schedule=schedule)
self.assertEqual(assignment.timeslot, slot2)
# too many attendees warning
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#session{}.too-many-attendees'.format(s2.pk)))
# overfull timeslot
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{}.overfull'.format(slot2.pk)))
# constraint hints
s1_element.click()
self.assertIn('would-violate-hint', s2_element.get_attribute('class'))
constraint_element = s2_element.find_element(By.CSS_SELECTOR, ".constraints span[data-sessions=\"{}\"].would-violate-hint".format(s1.pk))
self.assertTrue(constraint_element.is_displayed())
# current constraint violations
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s1.pk, slot1.pk))
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1.pk, s1.pk))))
constraint_element = s2_element.find_element(By.CSS_SELECTOR, ".constraints span[data-sessions=\"{}\"].violated-hint".format(s1.pk))
self.assertTrue(constraint_element.is_displayed())
# hide sessions in area
self.assertTrue(s1_element.is_displayed())
self.driver.find_element(By.CSS_SELECTOR, ".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click()
self.assertTrue(s1_element.is_displayed()) # should still be displayed
self.assertIn('hidden-parent', s1_element.get_attribute('class'),
'Session should be hidden when parent disabled')
self.scroll_and_click((By.CSS_SELECTOR, '#session{}'.format(s1.pk)))
self.assertNotIn('selected', s1_element.get_attribute('class'),
'Session should not be selectable when parent disabled')
self.driver.find_element(By.CSS_SELECTOR, ".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click()
self.assertTrue(s1_element.is_displayed())
self.assertNotIn('hidden-parent', s1_element.get_attribute('class'),
'Session should not be hidden when parent enabled')
s1_element.click() # try to select
self.assertIn('selected', s1_element.get_attribute('class'),
'Session should be selectable when parent enabled')
# hide timeslots
modal_open = self.driver.find_element(By.CSS_SELECTOR, "#timeslot-toggle-modal-open")
self.driver.execute_script("arguments[0].click();", modal_open) # FIXME: not working:
# modal_open.click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed())
self.driver.find_element(
By.CSS_SELECTOR,
"#timeslot-group-toggles-modal [value=\"{}\"]".format(
"ts-group-{}-{}".format(
slot2.time.astimezone(slot2.tz()).strftime("%Y%m%d-%H%M"),
int(slot2.duration.total_seconds() / 60),
),
),
).click()
self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [data-bs-dismiss=\"modal\"]").click()
self.assertTrue(not self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed())
# swap days
self.driver.find_element(
By.CSS_SELECTOR,
".day .swap-days[data-dayid=\"{}\"]".format(
slot4.time.astimezone(slot4.tz()).date().isoformat(),
),
).click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal").is_displayed())
self.driver.find_element(
By.CSS_SELECTOR,
"#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(
slot1.time.astimezone(slot1.tz()).date().isoformat(),
),
).click()
self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal button[type=\"submit\"]").click()
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot4.pk, s1.pk)),
'Session s1 should have moved to second meeting day')
# swap timeslot column - put session in a differently-timed timeslot
self.scroll_and_click((By.CSS_SELECTOR,
'.day .swap-timeslot-col[data-timeslot-pk="{}"]'.format(slot1b.pk)
)) # open modal on the second timeslot for room1
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#swap-timeslot-col-modal").is_displayed())
self.driver.find_element(By.CSS_SELECTOR,
'#swap-timeslot-col-modal input[name="target_timeslot"][value="{}"]'.format(slot4.pk)
).click() # select room1 timeslot that has a session in it
self.driver.find_element(By.CSS_SELECTOR, '#swap-timeslot-col-modal button[type="submit"]').click()
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1b.pk, s1.pk)),
'Session s1 should have moved to second timeslot on first meeting day')
def test_past_flags(self):
"""Test that timeslots and sessions in the past are marked accordingly
Would also like to test that past-hint flags are applied when a session is dragged, but that
requires simulating HTML5 drag-and-drop. Have not yet found a good way to do this.
"""
wait = WebDriverWait(self.driver, 2)
meeting = MeetingFactory(type_id='ietf')
room = RoomFactory(meeting=meeting)
# get current time in meeting time zone
right_now = timezone.now().astimezone(
pytz.timezone(meeting.time_zone)
)
if not settings.USE_TZ:
right_now = right_now.replace(tzinfo=None)
past_timeslots = [
TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(hours=n),
duration=datetime.timedelta(hours=1), location=room)
for n in range(1,4)
]
future_timeslots = [
TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(hours=n),
duration=datetime.timedelta(hours=1), location=room)
for n in range(1,4)
]
now_timeslots = [
# timeslot just barely in the past (to avoid race conditions) but overlapping now
TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(seconds=1),
duration=datetime.timedelta(hours=1), location=room),
# next slot is < MEETING_SESSION_LOCK_TIME in the future
TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(minutes=9),
duration=datetime.timedelta(hours=1), location=room)
]
past_sessions = [
SchedTimeSessAssignment.objects.create(
schedule=meeting.schedule,
timeslot=ts,
session=SessionFactory(meeting=meeting, add_to_schedule=False),
).session
for ts in past_timeslots
]
future_sessions = [
SchedTimeSessAssignment.objects.create(
schedule=meeting.schedule,
timeslot=ts,
session=SessionFactory(meeting=meeting, add_to_schedule=False),
).session
for ts in future_timeslots
]
now_sessions = [
SchedTimeSessAssignment.objects.create(
schedule=meeting.schedule,
timeslot=ts,
session=SessionFactory(meeting=meeting, add_to_schedule=False),
).session
for ts in now_timeslots
]
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number))
self.login(username=meeting.schedule.owner.user.username)
self.driver.get(url)
past_flags = self.driver.find_elements(By.CSS_SELECTOR,
','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in past_timeslots)
)
self.assertGreaterEqual(len(past_flags), len(past_timeslots) + len(past_sessions),
'Expected at least one flag for each past timeslot and session')
now_flags = self.driver.find_elements(By.CSS_SELECTOR,
','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in now_timeslots)
)
self.assertGreaterEqual(len(now_flags), len(now_timeslots) + len(now_sessions),
'Expected at least one flag for each "now" timeslot and session')
future_flags = self.driver.find_elements(By.CSS_SELECTOR,
','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in future_timeslots)
)
self.assertGreaterEqual(len(future_flags), len(future_timeslots) + len(future_sessions),
'Expected at least one flag for each future timeslot and session')
wait.until(expected_conditions.presence_of_element_located(
(By.CSS_SELECTOR, '#timeslot{}.past'.format(past_timeslots[0].pk))
))
for flag in past_flags:
self.assertTrue(flag.is_displayed(), 'Past timeslot or session not flagged as past')
for flag in now_flags:
self.assertTrue(flag.is_displayed(), '"Now" timeslot or session not flagged as past')
for flag in future_flags:
self.assertFalse(flag.is_displayed(), 'Future timeslot or session is flagged as past')
def test_past_swap_days_buttons(self):
"""Swap days buttons should be hidden for past items"""
wait = WebDriverWait(self.driver, 2)
meeting = MeetingFactory(
type_id='ietf',
date=timezone.now() - datetime.timedelta(days=3),
days=7,
time_zone=timezone_not_near_midnight(),
)
room = RoomFactory(meeting=meeting)
# get current time in meeting time zone
right_now = timezone.now().astimezone(
pytz.timezone(meeting.time_zone)
)
if not settings.USE_TZ:
right_now = right_now.replace(tzinfo=None)
past_timeslots = [
TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(days=n),
duration=datetime.timedelta(hours=1), location=room)
for n in range(4) # includes 0
]
future_timeslots = [
TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(days=n),
duration=datetime.timedelta(hours=1), location=room)
for n in range(1,4)
]
now_timeslots = [
# timeslot just barely in the past (to avoid race conditions) but overlapping now
TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(seconds=1),
duration=datetime.timedelta(hours=1), location=room),
# next slot is < MEETING_SESSION_LOCK_TIME in the future
TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(minutes=9),
duration=datetime.timedelta(hours=1), location=room)
]
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number))
self.login(username=meeting.schedule.owner.user.username)
self.driver.get(url)
past_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-days[data-start="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
for ts in past_timeslots
)
)
self.assertEqual(len(past_swap_days_buttons), len(past_timeslots), 'Missing past swap days buttons')
future_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-days[data-start="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
for ts in future_timeslots
)
)
self.assertEqual(len(future_swap_days_buttons), len(future_timeslots), 'Missing future swap days buttons')
now_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-days[data-start="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
for ts in now_timeslots
)
)
# only one "now" button because both sessions are on the same day
self.assertEqual(len(now_swap_days_buttons), 1, 'Missing "now" swap days button')
wait.until(
expected_conditions.presence_of_element_located(
(By.CSS_SELECTOR, '.timeslot.past') # wait until timeslots are updated by JS
)
)
# check that swap buttons are disabled for past days
self.assertFalse(
any(button.is_displayed() for button in past_swap_days_buttons),
'Past swap days buttons still visible for official schedule',
)
self.assertTrue(
all(button.is_displayed() for button in future_swap_days_buttons),
'Future swap days buttons not visible for official schedule',
)
self.assertFalse(
any(button.is_displayed() for button in now_swap_days_buttons),
'"Now" swap days buttons still visible for official schedule',
)
# Open the swap days modal to verify that past day radios are disabled.
# Use a middle day because whichever day we click will be disabled as an
# option to swap. If we used the first or last day, a fencepost error in
# disabling options by date might be hidden.
clicked_index = 1
# scroll so the button we want to click is just below the navbar, otherwise it may
# fall beneath the sessions panel
navbar = self.driver.find_element(By.CSS_SELECTOR, '.navbar')
self.driver.execute_script(
'window.scrollBy({top: %s, behavior: "instant"})' % (
future_swap_days_buttons[1].location['y'] - navbar.size['height']
)
)
future_swap_days_buttons[clicked_index].click()
try:
modal = wait.until(
expected_conditions.visibility_of_element_located(
(By.CSS_SELECTOR, '#swap-days-modal')
)
)
except TimeoutException:
self.fail('Modal never appeared')
self.assertFalse(
any(radio.is_enabled()
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_day"][value="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
for ts in past_timeslots)
)),
'Past day is enabled in swap-days modal for official schedule',
)
# future_timeslots[:-1] in the next selector because swapping a day with itself is disabled
enabled_timeslots = (ts for ts in future_timeslots if ts != future_timeslots[clicked_index])
self.assertTrue(
all(radio.is_enabled()
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_day"][value="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
for ts in enabled_timeslots)
)),
'Future day is not enabled in swap-days modal for official schedule',
)
self.assertFalse(
any(radio.is_enabled()
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_day"][value="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
for ts in now_timeslots)
)),
'"Now" day is enabled in swap-days modal for official schedule',
)
def test_past_swap_timeslot_col_buttons(self):
"""Swap timeslot column buttons should be hidden for past items"""
wait = WebDriverWait(self.driver, 2)
meeting = MeetingFactory(type_id='ietf', date=timezone.now() - datetime.timedelta(days=3), days=7)
room = RoomFactory(meeting=meeting)
# get current time in meeting time zone
right_now = timezone.now().astimezone(
pytz.timezone(meeting.time_zone)
)
if not settings.USE_TZ:
right_now = right_now.replace(tzinfo=None)
past_timeslots = [
TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(hours=n),
duration=datetime.timedelta(hours=1), location=room)
for n in range(1,4) # does not include 0 to avoid race conditions
]
future_timeslots = [
TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(hours=n),
duration=datetime.timedelta(hours=1), location=room)
for n in range(1,4)
]
now_timeslots = [
# timeslot just barely in the past (to avoid race conditions) but overlapping now
TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(seconds=1),
duration=datetime.timedelta(hours=1), location=room),
# next slot is < MEETING_SESSION_LOCK_TIME in the future
TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(minutes=9),
duration=datetime.timedelta(hours=1), location=room)
]
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number))
self.login(username=meeting.schedule.owner.user.username)
self.driver.get(url)
past_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'*[data-start="{}"] .swap-timeslot-col'.format(ts.utc_start_time().isoformat()) for ts in past_timeslots
)
)
self.assertEqual(len(past_swap_ts_buttons), len(past_timeslots), 'Missing past swap timeslot col buttons')
future_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'*[data-start="{}"] .swap-timeslot-col'.format(ts.utc_start_time().isoformat()) for ts in future_timeslots
)
)
self.assertEqual(len(future_swap_ts_buttons), len(future_timeslots), 'Missing future swap timeslot col buttons')
now_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'[data-start="{}"] .swap-timeslot-col'.format(ts.utc_start_time().isoformat()) for ts in now_timeslots
)
)
self.assertEqual(len(now_swap_ts_buttons), len(now_timeslots), 'Missing "now" swap timeslot col buttons')
wait.until(
expected_conditions.presence_of_element_located(
(By.CSS_SELECTOR, '.timeslot.past') # wait until timeslots are updated by JS
)
)
# check that swap buttons are disabled for past days
self.assertFalse(
any(button.is_displayed() for button in past_swap_ts_buttons),
'Past swap timeslot col buttons still visible for official schedule',
)
self.assertTrue(
all(button.is_displayed() for button in future_swap_ts_buttons),
'Future swap timeslot col buttons not visible for official schedule',
)
self.assertFalse(
any(button.is_displayed() for button in now_swap_ts_buttons),
'"Now" swap timeslot col buttons still visible for official schedule',
)
# Open the swap days modal to verify that past day radios are disabled.
# Use a middle day because whichever day we click will be disabled as an
# option to swap. If we used the first or last day, a fencepost error in
# disabling options by date might be hidden.
clicked_index = 1
self.driver.execute_script("arguments[0].click();", future_swap_ts_buttons[clicked_index]) # FIXME: not working:
# future_swap_ts_buttons[clicked_index].click()
try:
modal = wait.until(
expected_conditions.visibility_of_element_located(
(By.CSS_SELECTOR, '#swap-timeslot-col-modal')
)
)
except TimeoutException:
self.fail('Modal never appeared')
self.assertFalse(
any(radio.is_enabled()
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_timeslot"][value="{}"]'.format(ts.pk) for ts in past_timeslots)
)),
'Past timeslot is enabled in swap-timeslot-col modal for official schedule',
)
# future_timeslots[:-1] in the next selector because swapping a timeslot with itself is disabled
enabled_timeslots = (ts for ts in future_timeslots if ts != future_timeslots[clicked_index])
self.assertTrue(
all(radio.is_enabled()
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_timeslot"][value="{}"]'.format(ts.pk) for ts in enabled_timeslots)
)),
'Future timeslot is not enabled in swap-timeslot-col modal for official schedule',
)
self.assertFalse(
any(radio.is_enabled()
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_timeslot"][value="{}"]'.format(ts.pk) for ts in now_timeslots)
)),
'"Now" timeslot is enabled in swap-timeslot-col modal for official schedule',
)
def test_unassigned_sessions_sort(self):
"""Unassigned session sorting should behave correctly
Sorting options and list of sort criteria
name (name, duration, id)
parent (parent, name, duration, id)
duration (duration, parent, name, id)
comments (presence of comments, parent, name, duration, id)
"""
# Define helpers
def sort_by_position(driver, sessions):
"""Helper to sort sessions by the position of their session element in the unscheduled box"""
def _sort_key(sess):
elt = driver.find_element(By.ID, 'session{}'.format(sess.pk))
return (elt.location['y'], elt.location['x'])
return sorted(sessions, key=_sort_key)
wait = WebDriverWait(self.driver, 2)
def wait_for_order(sessions, expected_order, fail_message):
"""Helper to wait for sorting to complete"""
try:
wait.until(
lambda driver: sort_by_position(driver, sessions) == expected_order,
)
except TimeoutException:
pass # Fall through to the assertion which will fail, don't throw a confusing timeout exception
self.assertEqual(sort_by_position(self.driver, sessions), expected_order, fail_message)
# Start the test here
# set up several WGs in various areas, including no area.
area_acronyms = ['A', 'B', 'C', 'D']
areas = [GroupFactory(type_id='area', acronym=acro) for acro in area_acronyms]
# now create WGs with acronyms that sort differently than by area (g00, g01, g02...)
num = 0
wgs = []
group_acro = lambda n: 'g{:02d}'.format(n)
for _ in range(2):
wgs.append(GroupFactory(acronym=group_acro(num), type_id='wg', parent=None))
num += 1
for area in areas:
wgs.append(GroupFactory(acronym=group_acro(num), type_id='wg', parent=area))
num += 1
# Create an IETF meeting...
meeting = MeetingFactory(type_id='ietf')
# ...add a room that has no timeslots to be sure it's handled...
RoomFactory(meeting=meeting)
# ...and sessions for the groups. Use durations that are in a different order than
# area or name. The wgs list is in ascending acronym order, so use descending durations.
sessions = []
for n, wg in enumerate(wgs[::-1]):
sessions.append(
SessionFactory(
meeting=meeting,
group=wg,
requested_duration=datetime.timedelta(minutes=30 + 5 * n),
status_id='schedw',
add_to_schedule=False,
)
)
# Finally, assign comments to some sessions. Assign every 3rd until we reach the end.
# This should be a different sort than any of the other axes.
for sess in sessions[::3]:
sess.comments = 'special request'
sess.save()
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number))
self.login('secretary')
self.driver.get(url)
select = self.driver.find_element(By.NAME, 'sort_unassigned')
options = {
opt.get_attribute('value'): opt
for opt in select.find_elements(By.TAG_NAME, 'option')
}
# check sorting by name
options['name'].click()
self.assertEqual(select.get_attribute('value'), 'name')
expected_order = sorted(
sessions,
key=lambda s: (
s.group.acronym,
s.requested_duration,
)
)
wait_for_order(sessions, expected_order, 'Failed to sort by name')
# check sorting by parent
options['parent'].click()
self.assertEqual(select.get_attribute('value'), 'parent')
expected_order = sorted(
sessions,
key=lambda s: (
s.group.parent.acronym if s.group.parent else '',
s.group.acronym,
s.requested_duration,
)
)
wait_for_order(sessions, expected_order, 'Failed to sort by parent')
# check sorting by duration
options['duration'].click()
self.assertEqual(select.get_attribute('value'), 'duration')
expected_order = sorted(
sessions,
key=lambda s: (
s.requested_duration,
s.group.parent.acronym if s.group.parent else '',
s.group.acronym,
)
)
wait_for_order(sessions, expected_order, 'Failed to sort by duration')
# check sorting by comments
options['comments'].click()
self.assertEqual(select.get_attribute('value'), 'comments')
expected_order = sorted(
sessions,
key=lambda s: (
0 if len(s.comments) > 0 else 1,
s.group.parent.acronym if s.group.parent else '',
s.group.acronym,
s.requested_duration,
)
)
wait_for_order(sessions, expected_order, 'Failed to sort by comments')
def test_unassigned_sessions_drop_target_visible_when_empty(self):
"""The drop target for unassigned sessions should not collapse to 0 size
This test checks that issue #3174 has not regressed. A test that exercises
moving items from the schedule into the unassigned-sessions area is needed,
but as of 2021-05-04, Selenium does not support the HTML5 drag-and-drop
event interface. See:
https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/3604
https://gist.github.com/rcorreia/2362544
Note, however, that the workarounds are inadequate - they do not handle
all of the events needed by the editor.
"""
# Set up a meeting and a schedule a plain user can edit
schedule = ScheduleFactory(meeting__type_id='ietf', owner__user__username="plain")
meeting = schedule.meeting
# Open the editor
self.login()
url = self.absreverse(
'ietf.meeting.views.edit_meeting_schedule',
kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email())
)
self.driver.get(url)
# Check that the drop target for unassigned sessions is actually empty
drop_target = self.driver.find_element(By.CSS_SELECTOR,
'.unassigned-sessions .drop-target'
)
self.assertEqual(len(drop_target.find_elements(By.CLASS_NAME, 'session')), 0,
'Unassigned sessions box is not empty, test is broken')
# Check that the drop target has non-zero size
self.assertGreater(drop_target.size['height'], 0,
'Drop target for unassigned sessions collapsed to 0 height')
self.assertGreater(drop_target.size['width'], 0,
'Drop target for unassigned sessions collapsed to 0 width')
def test_session_constraint_hints(self):
"""Selecting a session should mark conflicting sessions
To test for recurrence of https://github.com/ietf-tools/datatracker/issues/3327 need to have some constraints that
do not conflict. Testing with only violated constraints does not exercise the code adequately.
"""
meeting = MeetingFactory(type_id='ietf', date=date_today(), populate_schedule=False)
TimeSlotFactory.create_batch(5, meeting=meeting)
schedule = ScheduleFactory(meeting=meeting)
sessions = SessionFactory.create_batch(5, meeting=meeting, add_to_schedule=False)
groups = [s.group for s in sessions]
# Now set up constraints
# Get an arbitrary enabled group conflict ConstraintName
constraint_names = meeting.enabled_constraint_names().filter(is_group_conflict=True)
self.assertGreaterEqual(len(constraint_names), 2, 'Not enough constraint names enabled to perform test')
# one-way conflict from group 0 to 1
ConstraintFactory(meeting=meeting, name=constraint_names[0], source=groups[0], target=groups[1], person=None)
# one-way conflict from group 2 to 0
ConstraintFactory(meeting=meeting, name=constraint_names[0], source=groups[2], target=groups[0], person=None)
# two-way conflict between groups 0 and 3
ConstraintFactory(meeting=meeting, name=constraint_names[0], source=groups[0], target=groups[3], person=None)
ConstraintFactory(meeting=meeting, name=constraint_names[0], source=groups[3], target=groups[0], person=None)
# constraints that are not active when selecting sessions[0]
ConstraintFactory(meeting=meeting, name=constraint_names[1], source=groups[1], target=groups[2], person=None)
ConstraintFactory(meeting=meeting, name=constraint_names[1], source=groups[3], target=groups[4], person=None)
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule',
kwargs=dict(num=meeting.number, owner=schedule.owner.email(), name=schedule.name))
self.login(schedule.owner.user.username)
self.driver.get(url)
session_elements = [self.driver.find_element(By.CSS_SELECTOR, f'#session{sess.pk}') for sess in sessions]
session_elements[0].click()
# All conflicting sessions should be flagged with the would-violate-hint class.
self.assertIn('would-violate-hint', session_elements[1].get_attribute('class'),
'Constraint violation should be indicated on conflicting session')
self.assertIn('would-violate-hint', session_elements[2].get_attribute('class'),
'Constraint violation should be indicated on conflicting session')
self.assertIn('would-violate-hint', session_elements[3].get_attribute('class'),
'Constraint violation should be indicated on conflicting session')
# And the non-conflicting session should not be flagged
self.assertNotIn('would-violate-hint', session_elements[4].get_attribute('class'),
'Constraint violation should not be indicated on non-conflicting session')
@ifSeleniumEnabled
class SlideReorderTests(IetfSeleniumTestCase):
def setUp(self):
super(SlideReorderTests, self).setUp()
self.session = SessionFactory(meeting__type_id='ietf', status_id='sched')
self.session.presentations.create(document=DocumentFactory(type_id='slides',name='one'),order=1)
self.session.presentations.create(document=DocumentFactory(type_id='slides',name='two'),order=2)
self.session.presentations.create(document=DocumentFactory(type_id='slides',name='three'),order=3)
def secr_login(self):
self.login('secretary')
#@override_settings(DEBUG=True)
def testReorderSlides(self):
return
url = self.absreverse('ietf.meeting.views.session_details',
kwargs=dict(
num=self.session.meeting.number,
acronym = self.session.group.acronym,))
self.secr_login()
self.driver.get(url)
#debug.show('unicode(self.driver.page_source)')
second = self.driver.find_element(By.CSS_SELECTOR, '#slides tr:nth-child(2)')
third = self.driver.find_element(By.CSS_SELECTOR, '#slides tr:nth-child(3)')
ActionChains(self.driver).drag_and_drop(second,third).perform()
time.sleep(0.1) # The API that modifies the database runs async
names=self.session.presentations.values_list('document__name',flat=True)
self.assertEqual(list(names),['one','three','two'])
@ifSeleniumEnabled
class InterimTests(IetfSeleniumTestCase):
def setUp(self):
super(InterimTests, self).setUp()
self.materials_dir = self.tempdir('materials')
self.saved_agenda_path = settings.AGENDA_PATH
settings.AGENDA_PATH = self.materials_dir
self.meeting = make_meeting_test_data(create_interims=True)
# Create a group with a plenary interim session for testing type filters
somegroup = GroupFactory(acronym='sg', name='Some Group')
sg_interim = make_interim_meeting(somegroup, date_today() + datetime.timedelta(days=20))
sg_sess = sg_interim.session_set.first()
sg_slot = sg_sess.timeslotassignments.first().timeslot
sg_sess.purpose_id = 'plenary'
sg_sess.type_id = 'plenary'
sg_slot.type_id = 'plenary'
sg_sess.save()
sg_slot.save()
self.wait = WebDriverWait(self.driver, 2)
def tearDown(self):
settings.AGENDA_PATH = self.saved_agenda_path
shutil.rmtree(self.materials_dir)
super(InterimTests, self).tearDown()
def tempdir(self, label):
# Borrowed from test_utils.TestCase
slug = slugify(self.__class__.__name__.replace('.','-'))
suffix = "-{label}-{slug}-dir".format(**locals())
return tempfile.mkdtemp(suffix=suffix)
def displayed_interims(self, groups=None):
sessions = add_event_info_to_session_qs(
Session.objects.filter(
meeting__type_id='interim',
timeslotassignments__schedule=F('meeting__schedule'),
timeslotassignments__timeslot__time__gte=timezone.now()
)
).filter(current_status__in=('sched','canceled'))
meetings = []
for s in sessions:
if groups is None or s.group.acronym in groups:
s.meeting.calendar_label = s.group.acronym # annotate with group
meetings.append(s.meeting)
return meetings
def all_ietf_meetings(self):
meetings = Meeting.objects.filter(
type_id='ietf',
date__gte=timezone.now()-datetime.timedelta(days=7)
)
for m in meetings:
m.calendar_label = 'IETF %s' % m.number
return meetings
def find_upcoming_meeting_entries(self):
return self.driver.find_elements(By.CSS_SELECTOR,
'table#upcoming-meeting-table a.ietf-meeting-link, table#upcoming-meeting-table a.interim-meeting-link'
)
def assert_upcoming_meeting_visibility(self, visible_meetings=None):
"""Assert that correct items are visible in current browser window
If visible_meetings is None (the default), expects all items to be visible.
"""
expected = {mtg.number for mtg in visible_meetings}
not_visible = set()
unexpected = set()
entries = self.find_upcoming_meeting_entries()
for entry in entries:
entry_text = entry.get_attribute('innerHTML').strip() # gets text, even if element is hidden
nums = [n for n in expected if n in entry_text]
self.assertLessEqual(len(nums), 1, 'Multiple matching meeting numbers')
if len(nums) > 0: # asserted that it's at most 1, so if it's not 0, it's 1.
expected.remove(nums[0])
if not entry.is_displayed():
not_visible.add(nums[0])
continue
# Found an unexpected row - this is only a problem if it is visible
if entry.is_displayed():
unexpected.add(entry_text)
self.assertEqual(expected, set(), "Missing entries for expected iterim meetings.")
self.assertEqual(not_visible, set(), "Hidden rows for expected interim meetings.")
self.assertEqual(unexpected, set(), "Unexpected row visible")
def assert_upcoming_meeting_calendar(self, visible_meetings=None):
"""Assert that correct items are sent to the calendar"""
def advance_month():
button = self.wait.until(
expected_conditions.element_to_be_clickable(
(By.CSS_SELECTOR, 'div#calendar button.fc-next-button')))
self.driver.execute_script("arguments[0].click();", button) # FIXME-LARS: no idea why this fails:
# self.scroll_to_element(button)
# button.click()
seen = set()
not_visible = set()
unexpected = set()
# Test that we see all the expected meetings when we scroll through the
# entire year. We only check the group names / IETF numbers. This should
# be good enough to catch filtering errors but does not validate the
# details of what's shown on the calendar. Need 13 iterations instead of
# 12 in order to check the starting month of the following year, which
# will usually contain the day 1 year from the start date.
for _ in range(13):
entries = self.driver.find_elements(By.CSS_SELECTOR,
'div#calendar div.fc-event-main'
)
for entry in entries:
meetings = [m for m in visible_meetings if m.calendar_label in entry.text]
if len(meetings) > 0:
seen.add(meetings[0])
if not entry.is_displayed():
not_visible.add(meetings[0])
continue
# Found an unexpected row - this is ok as long as it's hidden
if entry.is_displayed():
unexpected.add(entry.text)
advance_month()
self.assertCountEqual(seen, visible_meetings, "Expected calendar entries not shown.")
self.assertCountEqual(not_visible, set(), "Hidden calendar entries for expected interim meetings.")
self.assertCountEqual(unexpected, set(), "Unexpected calendar entries visible")
def do_upcoming_view_filter_test(self, querystring, visible_meetings=()):
self.login()
self.driver.get(self.absreverse('ietf.meeting.views.upcoming') + querystring)
time.sleep(0.2) # gross, but give the filter JS time to do its thing
self.assert_upcoming_meeting_visibility(visible_meetings)
self.assert_upcoming_meeting_calendar(visible_meetings)
self.assert_upcoming_view_filter_matches_ics_filter(querystring)
# Check the ical links
simplified_querystring = querystring.replace(' ', '') # remove spaces
if simplified_querystring in ['?show=', '?hide=', '?show=&hide=']:
simplified_querystring = '' # these empty querystrings will be dropped (not an exhaustive list)
ics_link = self.driver.find_element(By.LINK_TEXT, 'Download as .ics')
self.assertIn(simplified_querystring, ics_link.get_attribute('href'))
webcal_link = self.driver.find_element(By.LINK_TEXT, 'Subscribe with webcal')
self.assertIn(simplified_querystring, webcal_link.get_attribute('href'))
def assert_upcoming_view_filter_matches_ics_filter(self, filter_string):
"""The upcoming view and ics view should show matching events for a given filter
The upcoming ics view shows more detail than the upcoming view, so this
test expands the upcoming meeting list into the corresponding set of expected
sessions.
This must be called after using self.driver.get to load the upcoming page
to be checked.
"""
ics_url = self.absreverse('ietf.meeting.views.upcoming_ical')
# parse out the meetings shown on the upcoming view
upcoming_meetings = self.find_upcoming_meeting_entries()
visible_meetings = [mtg for mtg in upcoming_meetings if mtg.is_displayed()]
# Have list of meetings, now get sessions that should be shown
expected_ietfs = []
expected_interim_sessions = []
expected_schedules = []
for meeting_elt in visible_meetings:
# meeting_elt is an anchor element
label_text = meeting_elt.get_attribute('innerHTML')
match = re.search(r'(?P<ietf>IETF\s+)?(?P<number>\S+)', label_text)
meeting = Meeting.objects.get(number=match.group('number'))
if match.group('ietf'):
expected_ietfs.append(meeting)
else:
expected_interim_sessions.extend([s.pk for s in meeting.session_set.all()])
if meeting.schedule:
expected_schedules.extend([meeting.schedule, meeting.schedule.base])
# Now find the sessions we expect to see - should match the upcoming_ical view
expected_assignments = list(SchedTimeSessAssignment.objects.filter(
schedule__in=expected_schedules,
session__in=expected_interim_sessions,
timeslot__time__gte=datetime_today(),
))
# The UID formats should match those in the upcoming.ics template
expected_uids = [
'ietf-%s-%s' % (item.session.meeting.number, item.timeslot.pk)
for item in expected_assignments
] + [
'ietf-%s' % (ietf.number) for ietf in expected_ietfs
]
r = self.client.get(ics_url + filter_string)
assert_ical_response_is_valid(self, r,
expected_event_uids=expected_uids,
expected_event_count=len(expected_uids))
def test_upcoming_view_default(self):
"""By default, all upcoming interims and IETF meetings should be displayed"""
ietf_meetings = set(self.all_ietf_meetings())
self.do_upcoming_view_filter_test('', ietf_meetings.union(self.displayed_interims()))
def test_upcoming_view_show_ietf_meetings(self):
self.do_upcoming_view_filter_test('?show=ietf-meetings', self.all_ietf_meetings())
def test_upcoming_view_filter_show_group(self):
# Show none
self.do_upcoming_view_filter_test('?show=')
# Show one
self.do_upcoming_view_filter_test('?show=mars', self.displayed_interims(groups=['mars']))
# Show two
self.do_upcoming_view_filter_test('?show=mars,ames',self.displayed_interims(groups=['mars', 'ames']))
# Show two plus ietf-meetings
self.do_upcoming_view_filter_test(
'?show=ietf-meetings,mars,ames',
set(self.all_ietf_meetings()).union(self.displayed_interims(groups=['mars', 'ames']))
)
def test_upcoming_view_filter_show_area(self):
mars = Group.objects.get(acronym='mars')
area = mars.parent
self.do_upcoming_view_filter_test('?show=%s' % area.acronym, self.displayed_interims(groups=['mars', 'ames']))
def test_upcoming_view_filter_show_type(self):
self.do_upcoming_view_filter_test('?show=plenary', self.displayed_interims(groups=['sg']))
def test_upcoming_view_filter_hide_group(self):
mars = Group.objects.get(acronym='mars')
area = mars.parent
# Without anything shown, should see only ietf meetings
self.do_upcoming_view_filter_test('?hide=mars')
# With group shown
self.do_upcoming_view_filter_test('?show=ames,mars&hide=mars', self.displayed_interims(groups=['ames']))
# With area shown
self.do_upcoming_view_filter_test('?show=%s&hide=mars' % area.acronym, self.displayed_interims(groups=['ames']))
# With type shown
self.do_upcoming_view_filter_test('?show=plenary&hide=sg')
def test_upcoming_view_filter_hide_area(self):
mars = Group.objects.get(acronym='mars')
area = mars.parent
# Without anything shown, should see nothing
self.do_upcoming_view_filter_test('?hide=%s' % area.acronym)
# With area shown
self.do_upcoming_view_filter_test('?show=%s&hide=%s' % (area.acronym, area.acronym))
# With group shown
self.do_upcoming_view_filter_test('?show=mars&hide=%s' % area.acronym)
# With type shown
self.do_upcoming_view_filter_test('?show=regular&hide=%s' % area.acronym)
# with IETF meetings shown
self.do_upcoming_view_filter_test('?show=ietf-meetings,hide=%s' % area.acronym, self.all_ietf_meetings())
def test_upcoming_view_filter_hide_type(self):
# Without anything shown, should see nothing
self.do_upcoming_view_filter_test('?hide=regular')
# With group shown
self.do_upcoming_view_filter_test('?show=mars&hide=regular')
# With type shown
self.do_upcoming_view_filter_test(
'?show=plenary,regular&hide=regular',
self.displayed_interims(groups=['sg'])
)
# With interim-meetings shown
self.do_upcoming_view_filter_test('?show=plenary,regular&hide=regular', self.displayed_interims(groups=['sg']))
def test_upcoming_view_filter_whitespace(self):
"""Whitespace in filter lists should be ignored"""
self.do_upcoming_view_filter_test('?show=mars , ames &hide= ames', self.displayed_interims(groups=['mars']))
def test_upcoming_view_time_zone_selection(self):
def _assert_interim_tz_correct(sessions, tz):
zone = pytz.timezone(tz)
for session in sessions:
ts = session.official_timeslotassignment().timeslot
start = ts.utc_start_time().astimezone(zone).strftime('%Y-%m-%d %H:%M')
end = ts.utc_end_time().astimezone(zone).strftime('%H:%M')
meeting_link = self.driver.find_element(By.LINK_TEXT, session.meeting.number)
time_td = meeting_link.find_element(By.XPATH, '../../td[contains(@class, "session-time")]')
self.assertIn('%s-%s' % (start, end), time_td.text)
def _assert_ietf_tz_correct(meetings, tz):
zone = pytz.timezone(tz)
for meeting in meetings:
meeting_zone = pytz.timezone(meeting.time_zone)
start_dt = meeting_zone.localize(datetime.datetime.combine(
meeting.date,
datetime.time.min
))
end_dt = meeting_zone.localize(datetime.datetime.combine(
start_dt + datetime.timedelta(days=meeting.days - 1),
datetime.time.max
))
start = start_dt.astimezone(zone).strftime('%Y-%m-%d')
end = end_dt.astimezone(zone).strftime('%Y-%m-%d')
meeting_link = self.driver.find_element(By.LINK_TEXT, "IETF " + meeting.number)
time_td = meeting_link.find_element(By.XPATH, '../../td[contains(@class, "meeting-time")]')
self.assertIn('%s to %s' % (start, end), time_td.text)
sessions = [m.session_set.first() for m in self.displayed_interims()]
self.assertGreater(len(sessions), 0)
ietf_meetings = self.all_ietf_meetings()
self.assertGreater(len(ietf_meetings), 0)
self.driver.get(self.absreverse('ietf.meeting.views.upcoming'))
tz_select_input = self.driver.find_element(By.ID, 'timezone-select')
tz_select_bottom_input = self.driver.find_element(By.ID, 'timezone-select-bottom')
# For things we click, need to click the labels / actually visible items. The actual inputs are hidden
# and managed by the JS.
local_tz_link = self.driver.find_element(By.CSS_SELECTOR, 'label[for="local-timezone"]')
utc_tz_link = self.driver.find_element(By.CSS_SELECTOR, 'label[for="utc-timezone"]')
local_tz_bottom_link = self.driver.find_element(By.CSS_SELECTOR, 'label[for="local-timezone-bottom"]')
utc_tz_bottom_link = self.driver.find_element(By.CSS_SELECTOR, 'label[for="utc-timezone-bottom"]')
# wait for the select box to be updated - look for an arbitrary time zone to be in
# its options list to detect this
arbitrary_tz = 'America/Halifax'
arbitrary_tz_opt = self.wait.until(
expected_conditions.presence_of_element_located(
(By.CSS_SELECTOR, '#timezone-select > option[value="%s"]' % arbitrary_tz)
)
)
tz_selector_clickables = self.driver.find_elements(By.CSS_SELECTOR, ".tz-display .select2")
self.assertEqual(len(tz_selector_clickables), 2)
(tz_selector_top, tz_selector_bottom) = tz_selector_clickables
arbitrary_tz_bottom_opt = tz_select_bottom_input.find_element(By.CSS_SELECTOR,
'#timezone-select-bottom > option[value="%s"]' % arbitrary_tz)
utc_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value="UTC"]')
utc_tz_bottom_opt= tz_select_bottom_input.find_element(By.CSS_SELECTOR, 'option[value="UTC"]')
# Moment.js guesses local time zone based on the behavior of Selenium's web client. This seems
# to inherit Django's settings.TIME_ZONE but I don't know whether that's guaranteed to be consistent.
# To avoid test fragility, ask Moment what it considers local and expect that.
local_tz = self.driver.execute_script('return moment.tz.guess();')
local_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value=%s]' % local_tz)
local_tz_bottom_opt = tz_select_bottom_input.find_element(By.CSS_SELECTOR, 'option[value="%s"]' % local_tz)
# Should start off in local time zone
self.assertTrue(local_tz_opt.is_selected())
self.assertTrue(local_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, local_tz)
_assert_ietf_tz_correct(ietf_meetings, local_tz)
# click 'utc' button
utc_tz_link.click()
self.wait.until(expected_conditions.element_to_be_selected(utc_tz_opt))
self.assertFalse(local_tz_opt.is_selected())
self.assertFalse(local_tz_bottom_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_bottom_opt.is_selected())
self.assertTrue(utc_tz_opt.is_selected())
self.assertTrue(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, 'UTC')
_assert_ietf_tz_correct(ietf_meetings, 'UTC')
# click back to 'local'
local_tz_link.click()
self.wait.until(expected_conditions.element_to_be_selected(local_tz_opt))
self.assertTrue(local_tz_opt.is_selected())
self.assertTrue(local_tz_bottom_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_bottom_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
self.assertFalse(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, local_tz)
_assert_ietf_tz_correct(ietf_meetings, local_tz)
# Now select a different item from the select input
tz_selector_top.click()
self.wait.until(
expected_conditions.presence_of_element_located(
(By.CSS_SELECTOR, 'span.select2-container .select2-results li[id$="America/Halifax"]')
)
).click()
self.wait.until(expected_conditions.element_to_be_selected(arbitrary_tz_opt))
self.assertFalse(local_tz_opt.is_selected())
self.assertFalse(local_tz_bottom_opt.is_selected())
self.assertTrue(arbitrary_tz_opt.is_selected())
self.assertTrue(arbitrary_tz_bottom_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
self.assertFalse(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, arbitrary_tz)
_assert_ietf_tz_correct(ietf_meetings, arbitrary_tz)
# Now repeat those tests using the widgets at the bottom of the page
# click 'utc' button
self.scroll_to_element(utc_tz_bottom_link)
utc_tz_bottom_link.click()
self.wait.until(expected_conditions.element_to_be_selected(utc_tz_opt))
self.assertFalse(local_tz_opt.is_selected())
self.assertFalse(local_tz_bottom_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_bottom_opt.is_selected())
self.assertTrue(utc_tz_opt.is_selected())
self.assertTrue(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, 'UTC')
_assert_ietf_tz_correct(ietf_meetings, 'UTC')
# click back to 'local'
self.scroll_to_element(local_tz_bottom_link)
local_tz_bottom_link.click()
self.wait.until(expected_conditions.element_to_be_selected(local_tz_opt))
self.assertTrue(local_tz_opt.is_selected())
self.assertTrue(local_tz_bottom_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_bottom_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
self.assertFalse(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, local_tz)
_assert_ietf_tz_correct(ietf_meetings, local_tz)
# Now select a different item from the select input
self.scroll_to_element(tz_selector_bottom)
tz_selector_bottom.click()
self.wait.until(
expected_conditions.presence_of_element_located(
(By.CSS_SELECTOR, 'span.select2-container .select2-results li[id$="America/Halifax"]')
)
).click()
self.wait.until(expected_conditions.element_to_be_selected(arbitrary_tz_opt))
self.assertFalse(local_tz_opt.is_selected())
self.assertFalse(local_tz_bottom_opt.is_selected())
self.assertTrue(arbitrary_tz_opt.is_selected())
self.assertTrue(arbitrary_tz_bottom_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
self.assertFalse(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, arbitrary_tz)
_assert_ietf_tz_correct(ietf_meetings, arbitrary_tz)
def test_upcoming_materials_modal(self):
"""Test opening and closing a materials modal
This does not test dynamic reloading of the meeting materials - it relies on the main
agenda page testing that. If the materials modal handling diverges between here and
there, this should be updated to include that test.
"""
url = self.absreverse('ietf.meeting.views.upcoming')
self.driver.get(url)
interim = self.displayed_interims(['mars'])[0]
session = interim.session_set.first()
assignment = session.official_timeslotassignment()
slug = assignment.slug()
# modal should start hidden
modal_div = self.driver.find_element(By.CSS_SELECTOR, 'div#modal-%s' % slug)
self.assertFalse(modal_div.is_displayed())
# Click the 'materials' button
open_modal_button_locator = (By.CSS_SELECTOR, '[data-bs-target="#modal-%s"]' % slug)
self.scroll_and_click(open_modal_button_locator)
self.wait.until(
expected_conditions.visibility_of(modal_div),
'Modal did not become visible after clicking open button',
)
# Now close the modal
close_modal_button = self.wait.until(
presence_of_element_child_by_css_selector(
modal_div,
'.modal-footer button[data-bs-dismiss="modal"]',
),
'Modal close button not found or not clickable',
)
time.sleep(0.3) # gross, but the button is clickable while still fading in
close_modal_button.click()
self.wait.until(
expected_conditions.invisibility_of_element(modal_div),
'Modal was not hidden after clicking close button',
)
@ifSeleniumEnabled
class ProceedingsMaterialTests(IetfSeleniumTestCase):
def setUp(self):
super().setUp()
self.wait = WebDriverWait(self.driver, 2)
self.meeting = MeetingFactory(type_id='ietf', number='123', date=date_today())
def test_add_proceedings_material(self):
url = self.absreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=self.meeting.number, material_type='supporters'),
)
self.login('secretary')
self.driver.get(url)
# get the UI elements
use_url_checkbox = self.wait.until(
expected_conditions.element_to_be_clickable((By.ID, 'id_use_url'))
)
choose_file_button = self.wait.until(
expected_conditions.presence_of_element_located((By.ID, 'id_file'))
)
external_url_field = self.wait.until(
expected_conditions.presence_of_element_located((By.ID, 'id_external_url'))
)
# should start with use_url unchecked for a new material
self.assertTrue(choose_file_button.is_displayed(),
'File chooser should be shown by default')
self.assertFalse(external_url_field.is_displayed(),
'URL field should be hidden by default')
# enable URL
use_url_checkbox.click()
self.wait.until(expected_conditions.invisibility_of_element(choose_file_button),
'File chooser should be hidden when URL option is checked')
self.wait.until(expected_conditions.visibility_of(external_url_field),
'URL field should appear when URL option is checked')
# disable URL
use_url_checkbox.click()
self.wait.until(expected_conditions.visibility_of(choose_file_button),
'File chooser should appear when URL option is unchecked')
self.wait.until(expected_conditions.invisibility_of_element(external_url_field),
'URL field should be hidden when URL option is unchecked')
def test_replace_proceedings_material_shows_correct_default(self):
doc_mat = ProceedingsMaterialFactory(meeting=self.meeting)
url_mat = ProceedingsMaterialFactory(meeting=self.meeting, document__external_url='https://example.com')
# check the document material
url = self.absreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=self.meeting.number, material_type=doc_mat.type.slug),
)
self.login('secretary')
self.driver.get(url)
use_url_checkbox = self.wait.until(
expected_conditions.element_to_be_clickable((By.ID, 'id_use_url'))
)
choose_file_button = self.wait.until(
expected_conditions.presence_of_element_located((By.ID, 'id_file'))
)
external_url_field = self.wait.until(
expected_conditions.presence_of_element_located((By.ID, 'id_external_url'))
)
# should start with use_url unchecked for a document material
self.assertFalse(use_url_checkbox.is_selected(), 'URL option should be unchecked for a document material')
self.assertTrue(choose_file_button.is_displayed(),
'File chooser should be shown by default')
self.assertFalse(external_url_field.is_displayed(),
'URL field should be hidden by default')
# check the URL material
url = self.absreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=self.meeting.number, material_type=url_mat.type.slug),
)
self.driver.get(url)
use_url_checkbox = self.wait.until(
expected_conditions.element_to_be_clickable((By.ID, 'id_use_url'))
)
choose_file_button = self.wait.until(
expected_conditions.presence_of_element_located((By.ID, 'id_file'))
)
external_url_field = self.wait.until(
expected_conditions.presence_of_element_located((By.ID, 'id_external_url'))
)
# should start with use_url unchecked for a document material
self.assertTrue(use_url_checkbox.is_selected(), 'URL option should be checked for URL material')
self.assertFalse(choose_file_button.is_displayed(),
'File chooser should be hidden by default')
self.assertTrue(external_url_field.is_displayed(),
'URL field should be shown by default')
@ifSeleniumEnabled
class EditTimeslotsTests(IetfSeleniumTestCase):
"""Test the timeslot editor"""
def setUp(self):
super().setUp()
self.meeting: Meeting = MeetingFactory( # type: ignore[annotation-unchecked]
type_id='ietf',
number=120,
date=date_today() + datetime.timedelta(days=10),
populate_schedule=False,
)
self.edit_timeslot_url = self.absreverse(
'ietf.meeting.views.edit_timeslots',
kwargs=dict(num=self.meeting.number),
)
self.wait = WebDriverWait(self.driver, 2)
def do_delete_test(self, selector, keep, delete, cancel=False):
self.login('secretary')
self.driver.get(self.edit_timeslot_url)
delete_button = self.wait.until(
expected_conditions.element_to_be_clickable(
(By.CSS_SELECTOR, selector)
))
delete_button.click()
if cancel:
cancel_button = self.wait.until(
expected_conditions.element_to_be_clickable(
(By.CSS_SELECTOR, '#delete-modal button[data-bs-dismiss="modal"]')
))
cancel_button.click()
else:
confirm_button = self.wait.until(
expected_conditions.element_to_be_clickable(
(By.CSS_SELECTOR, '#confirm-delete-button')
))
confirm_button.click()
self.wait.until(
expected_conditions.invisibility_of_element_located(
(By.CSS_SELECTOR, '#delete-modal')
))
if cancel:
keep.extend(delete)
delete = []
self.assertEqual(
TimeSlot.objects.filter(pk__in=[ts.pk for ts in delete]).count(),
0,
'Not all expected timeslots deleted',
)
self.assertEqual(
TimeSlot.objects.filter(pk__in=[ts.pk for ts in keep]).count(),
len(keep),
'Not all expected timeslots kept'
)
def do_delete_timeslot_test(self, cancel=False):
delete = [TimeSlotFactory(meeting=self.meeting)]
keep = [TimeSlotFactory(meeting=self.meeting)]
self.do_delete_test(
'#timeslot-table #timeslot{} .delete-button'.format(delete[0].pk),
keep,
delete
)
def test_delete_timeslot(self):
"""Delete button for a timeslot should delete that timeslot"""
self.do_delete_timeslot_test(cancel=False)
def test_delete_timeslot_cancel(self):
"""Timeslot should not be deleted on cancel"""
self.do_delete_timeslot_test(cancel=True)
def do_delete_time_interval_test(self, cancel=False):
delete_time_local = datetime_from_date(self.meeting.date, self.meeting.tz()).replace(hour=10)
delete_time = delete_time_local.astimezone(datetime.timezone.utc)
duration = datetime.timedelta(minutes=60)
delete: [TimeSlot] = TimeSlotFactory.create_batch( # type: ignore[annotation-unchecked]
2,
meeting=self.meeting,
time=delete_time_local,
duration=duration,
)
keep: [TimeSlot] = [ # type: ignore[annotation-unchecked]
TimeSlotFactory(
meeting=self.meeting,
time=keep_time,
duration=duration
)
for keep_time in (
# same day, but 2 hours later
delete_time + datetime.timedelta(hours=2),
# next day, but same wall clock time
datetime_from_date(self.meeting.get_meeting_date(1), self.meeting.tz()).replace(hour=10),
)
]
selector = (
'#timeslot-table '
'.delete-button[data-delete-scope="column"]'
'[data-col-id="{}T{}-{}"]'.format(
delete_time_local.date().isoformat(),
delete_time_local.strftime('%H:%M'),
(delete_time + duration).astimezone(self.meeting.tz()).strftime('%H:%M'))
)
self.do_delete_test(selector, keep, delete, cancel)
def test_delete_time_interval(self):
"""Delete button for a time interval should delete all timeslots in that interval"""
self.do_delete_time_interval_test(cancel=False)
def test_delete_time_interval_cancel(self):
"""Should not delete a time interval on cancel"""
self.do_delete_time_interval_test(cancel=True)
def do_delete_day_test(self, cancel=False):
delete_day = self.meeting.date
hours = [10, 12]
other_days = [self.meeting.get_meeting_date(d) for d in range(1, 3)]
delete: [TimeSlot] = [ # type: ignore[annotation-unchecked]
TimeSlotFactory(
meeting=self.meeting,
time=datetime_from_date(delete_day, self.meeting.tz()).replace(hour=hour),
) for hour in hours
]
keep: [TimeSlot] = [ # type: ignore[annotation-unchecked]
TimeSlotFactory(
meeting=self.meeting,
time=datetime_from_date(day, self.meeting.tz()).replace(hour=hour),
) for day in other_days for hour in hours
]
selector = (
'#timeslot-table '
'.delete-button[data-delete-scope="day"][data-date-id="{}"]'.format(
delete_day.isoformat()
)
)
self.do_delete_test(selector, keep, delete, cancel)
def test_delete_day(self):
"""Delete button for a day should delete all timeslots on that day"""
self.do_delete_day_test(cancel=False)
def test_delete_day_cancel(self):
"""Should not delete a day on cancel"""
self.do_delete_day_test(cancel=True)
# The following are useful debugging tools
# If you add this to a LiveServerTestCase and run just this test, you can browse
# to the test server with the data loaded by setUp() to debug why, for instance,
# a particular view isn't giving you what you expect
# def testJustSitThere(self):
# time.sleep(10000)
# The LiveServerTestCase server runs in a mode like production - it hides crashes with the
# user-friendly message about mail being sent to the maintainers, and eats that mail.
# Loading the page that crashed with just a TestCase will at least let you see the
# traceback.
#
#from ietf.utils.test_utils import TestCase
#class LookAtCrashTest(TestCase):
# def setUp(self):
# make_meeting_test_data()
#
# def testOpenSchedule(self):
# url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num='72',name='test-schedule'))
# r = self.client.get(url)