@@ -378,23 +378,35 @@ def delete_license(
378378 search_input_element .send_keys (license_name )
379379 search_input_element .send_keys (Keys .ENTER )
380380
381- license_name_xpath = _xpath_literal (value = license_name )
382-
383381 @beartype
384382 def _click_license_row (
385383 * ,
386384 driver : WebDriver ,
387385 ) -> bool :
388- """Find and click the row matching license_name."""
389- element = driver .find_element (
386+ """Find and click the row matching license_name.
387+
388+ The search filter is applied asynchronously, so the table can
389+ still hold unfiltered rows when this first runs. Wait for every
390+ row shown to match the search text before clicking, rather than
391+ clicking a row which is about to be replaced.
392+ """
393+ rows = driver .find_elements (
390394 by = By .XPATH ,
391395 value = (
392396 "//span[starts-with(@id, 'table_row_')"
393- " and contains(@id, '_app_name')"
394- f" and normalize-space(.)={ license_name_xpath } ]"
397+ " and contains(@id, '_app_name')]"
395398 ),
396399 )
397- element .click ()
400+ row_texts = [row .text .strip () for row in rows ]
401+ if not row_texts or not all (
402+ license_name in row_text for row_text in row_texts
403+ ):
404+ return False
405+ if license_name not in row_texts : # pragma: no cover
406+ # Every row contains the search text by now, but a row whose
407+ # text merely contains it is not the row we want.
408+ return False
409+ rows [row_texts .index (license_name )].click ()
398410 return True
399411
400412 thirty_second_wait .until (
0 commit comments