diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py
index ef826bdba..3087c8597 100644
--- a/ietf/meeting/tests_js.py
+++ b/ietf/meeting/tests_js.py
@@ -24,7 +24,7 @@ from ietf.group import colors
 from ietf.person.models import Person
 from ietf.group.models import Group
 from ietf.group.factories import GroupFactory
-from ietf.meeting.factories import SessionFactory, TimeSlotFactory
+from ietf.meeting.factories import MeetingFactory, SessionFactory, TimeSlotFactory
 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,
@@ -39,7 +39,7 @@ if selenium_enabled():
     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
+    from selenium.common.exceptions import NoSuchElementException, TimeoutException
 
 
 @ifSeleniumEnabled
@@ -225,6 +225,137 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
 
         self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{} #session{}'.format(slot4.pk, s1.pk)))
 
+    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')
+
+        # ...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
 
diff --git a/ietf/static/ietf/js/edit-meeting-schedule.js b/ietf/static/ietf/js/edit-meeting-schedule.js
index 75f307199..65bf51214 100644
--- a/ietf/static/ietf/js/edit-meeting-schedule.js
+++ b/ietf/static/ietf/js/edit-meeting-schedule.js
@@ -385,11 +385,13 @@ jQuery(document).ready(function () {
         }
 
         function extractName(e) {
-            return e.querySelector(".session-label").innerHTML;
+            let labelElement = e.querySelector(".session-label");
+            return labelElement ? labelElement.innerHTML : '';
         }
 
         function extractParent(e) {
-            return e.querySelector(".session-parent").innerHTML;
+            let parentElement = e.querySelector(".session-parent");
+            return parentElement ? parentElement.innerHTML : '';
         }
 
         function extractDuration(e) {
@@ -400,15 +402,13 @@ jQuery(document).ready(function () {
             return e.querySelector(".session-info .comments") ? 0 : 1;
         }
 
-        let keyFunctions = [];
-        if (sortBy == "name")
-            keyFunctions = [extractName, extractDuration, extractId];
-        else if (sortBy == "parent")
-            keyFunctions = [extractParent, extractName, extractDuration, extractId];
-        else if (sortBy == "duration")
-            keyFunctions = [extractDuration, extractParent, extractName, extractId];
-        else if (sortBy == "comments")
-            keyFunctions = [extractComments, extractParent, extractName, extractDuration, extractId];
+        const keyFunctionMap = {
+            name: [extractName, extractDuration, extractId],
+            parent: [extractParent, extractName, extractDuration, extractId],
+            duration: [extractDuration, extractParent, extractName, extractId],
+            comments: [extractComments, extractParent, extractName, extractDuration, extractId]
+        };
+        let keyFunctions = keyFunctionMap[sortBy];
 
         let unassignedSessionsContainer = content.find(".unassigned-sessions .drop-target");