Hướng dẫn python selenium wait for page to load after click - Selenium python đợi trang tải sau khi nhấp

Bài viết này ban đầu được xuất bản trên Obey The Testing Goat! Tác giả Harry Percival, tác giả của sự phát triển theo hướng thử nghiệm với Python và chúng tôi đang chia sẻ nó ở đây cho độc giả CodeShip.

Oft-Heard là tiếng khóc đã chết ...

Mỗi khi bạn bị cắn bởi một hành vi kỳ lạ trong một trong các bài kiểm tra selen của bạn. Bạn bảo nó nhấp vào một liên kết, và sau đó bạn hỏi nó một cái gì đó về trang mới và nó trả lại cho bạn một cái gì đó từ trang cũ:

old_value = browser.find_element_by_id('my-id').text
browser.find_element_by_link_text('my link').click()
new_value = browser.find_element_by_id('my-id').text
assert new_value != old_value ## fails unexpectedly

. "Tại sao bạn đã làm được điều đó?!" Bạn kêu lên trong một cơn thịnh nộ của lập trình viên. "Trong cuộc sống thực, khi bạn nhấp vào một liên kết, bạn thấy trình duyệt bắt đầu tải một trang mới và bạn chờ nó tải, phải không? Đó rõ ràng là những gì bạn muốn selen quá tầm thường để thực hiện! "

browser.find_element_by_link_text('my link').click()
# should just block until the next page has loaded

... với thời gian chờ lành mạnh có lẽ. Thậm chí còn có một tài liệu. API đã được kiểm tra xem một trang có được tải không! GRRR ... điều là, theo quan điểm selenium, nó không đơn giản như vậy (và tôi biết ơn David từ Mozilla (@AutomatedTester) vì đã kiên nhẫn giải thích điều này với tôi, hơn một lần.) Bạn thấy, Selenium không có cách nào để biết liệu bạn có yêu cầu nó nhấp vào một siêu liên kết "thực" đi đến một URL mới hay không, liệu liên kết có đi đến cùng một trang hay không Để làm một số công cụ UI phong phú trên cùng một trang. Hơn thế nữa, vì Selenium WebDriver đã trở nên tiên tiến hơn, các nhấp chuột giống như những cú nhấp chuột "thực" hơn nhiều. Điều này có lợi ích làm cho các bài kiểm tra của chúng tôi trở nên thực tế hơn, nhưng điều đó cũng có nghĩa là Selenium khó có thể theo dõi tác động mà một nhấp chuột có đối với nội bộ của trình duyệt. Nó có thể cố gắng thăm dò trình duyệt cho trạng thái tải trang của nó ngay sau khi nhấp vào, nhưng điều đó mở ra một điều kiện đua trong đó trình duyệt là đa nhiệm, chưa hoàn toàn làm tròn để xử lý với nhấp chuột và nó cung cấp cho bạn. của trang cũ. Vì vậy, thay vào đó, Selenium làm tốt nhất của nó. Đối số ngầm định ít nhất sẽ đặt một vòng thử lại nhỏ vào nếu bạn cố gắng lấy một yếu tố không tồn tại trên trang cũ:document.readyState API for checking on whether a page has loaded! Grrr... The thing is that, from the Selenium point of view, it's not that simple (and I'm grateful for David from Mozilla (@AutomatedTester) for patiently explaining this to me, more than once.) You see, Selenium has no way of telling whether you've asked it to click on a "real" hyperlink that goes to a new URL, or whether the link goes to the same page, or whether the click is going to be intercepted by some sort of JavaScript to do some rich UI stuff on the same page. More than that, since Selenium webdriver has become more advanced, clicks are much more like "real" clicks. This has the benefit of making our tests more realistic, but it also means it's hard for Selenium to be able to track the impact that a click has on the browser's internals. It might try to poll the browser for its page-loaded status immediately after clicking, but that's open to a race condition where the browser was multitasking, hasn't quite got round to dealing with the click yet, and it gives you the .readyState of the old page. So, instead, Selenium does its best. The implicitly_wait argument will at least put a little retry loop in if you try and fetch an element that doesn't exist on the old page:

browser.implicitly_wait(3)
old_value = browser.find_element_by_id('thing-on-old-page').text browser.find_element_by_link_text('my link').click()
new_value = browser.find_element_by_id('thing-on-new-page').text # will block for 3 seconds until thing-on-new-page appears
assert new_value != old_value

Nhưng vấn đề xảy ra khi #Thing-on-New-Page cũng tồn tại trên trang cũ. Vậy lam gi? Giải pháp "được đề xuất" là một sự chờ đợi rõ ràng:#thing-on-new-page also exists on the old page. So what to do? The "recommended" solution is an explicit wait:

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
old_value = browser.find_element_by_id('thing-on-old-page').text browser.find_element_by_link_text('my link').click()
WebDriverWait(browser, 3).until(
  expected_conditions.text_to_be_present_in_element(
    (By.ID, 'thing-on-new-page'),
    'expected new text'
  )
)

Một số vấn đề với điều đó mặc dù:

  1. Xấu xí xấu xa *

  2. Nó không chung chung. Ngay cả khi tôi viết một trình bao bọc đẹp, thật tẻ nhạt khi phải gọi nó mỗi khi tôi nhấp vào một thứ, chỉ định một điều khác để chờ đợi mỗi lần.

  3. Và nó sẽ không hoạt động cho trường hợp khi tôi muốn kiểm tra xem một số văn bản vẫn giữ nguyên giữa tải trang.

Thực sự, tôi chỉ muốn một cách chờ đợi đáng tin cậy cho đến khi trang hoàn thành tải sau khi tôi nhấp vào một điều. Tôi hoàn toàn hiểu rằng David và bạn bè sẽ không cung cấp điều đó cho tôi theo mặc định bởi vì họ không thể biết được một cú nhấp chuột JavaScript là gì và những gì nhấp vào một trang mới, nhưng tôi biết. Nhưng làm thế nào để làm điều đó?

Một số thứ sẽ không hoạt động

Nỗ lực ngây thơ sẽ là một cái gì đó như thế này:

def wait_for(condition_function):
  start_time = time.time()
  while time.time() < start_time + 3:
    if condition_function():
      return True
    else:
      time.sleep(0.1)
  raise Exception(
   'Timeout waiting for {}'.format(condition_function.**name**)
  )
def click_through_to_new_page(link_text):
  browser.find_element_by_link_text('my link').click()
  def page_has_loaded():
    page_state = browser.execute_script(
      'return document.readyState;'
    )
    return page_state == 'complete'
  wait_for(page_has_loaded)

Chức năng Wait_for Helper là tốt, nhưng thật không may, click_through_to_new_page mở ra điều kiện cuộc đua nơi chúng tôi quản lý để thực thi tập lệnh trong trang cũ, trước khi trình duyệt bắt đầu xử lý nhấp chuột và page_has_loaded chỉ trả về đúng.wait_for helper function is good, but unfortunately click_through_to_new_page is open to the race condition where we manage to execute the script in the old page, before the browser has started processing the click, and page_has_loaded just returns true straight away.

Giải pháp làm việc hiện tại của chúng tôi

Tín dụng đầy đủ cho @TheMasmarks để đưa ra điều này: Nếu bạn giữ một số tài liệu tham khảo về các yếu tố từ trang cũ nằm xung quanh, thì chúng sẽ trở nên cũ kỹ khi DOM làm mới. Các yếu tố cũ khiến Selenium nâng cao một StaleElementReferenceException nếu bạn cố gắng tương tác với chúng. Vì vậy, chỉ cần thăm dò một cho đến khi bạn gặp lỗi. Chống đạn!StaleElementReferenceException if you try to interact with them. So just poll one until you get an error. Bulletproof!

def click_through_to_new_page(link_text):
  link = browser.find_element_by_link_text('my link')
  link.click()
  def link_has_gone_stale():
    try:
      # poll the link with an arbitrary call
      link.find_elements_by_id('doesnt-matter')
      return False
    except StaleElementReferenceException:
      return True
  wait_for(link_has_gone_stale)

Hoặc, đây là một phiên bản chung, được vệ sinh của cùng một thứ, dựa trên việc so sánh "ID" nội bộ của Selenium cho một đối tượng và được tạo thành một trình quản lý bối cảnh Pythonic đẹp: Cập nhật 2014-09-06: Xem nhận xét về bài đăng gốc. Có thể việc so sánh ID không hiệu quả bằng việc chờ đợi các ngoại lệ tham khảo cũ. Sẽ điều tra, nhưng bây giờ đã làm cho YMMV đó.

class wait_for_page_load(object):
  def __init__(self, browser):
    self.browser = browser
  def __enter__(self):
    self.old_page = self.browser.find_element_by_tag_name('html')
  def page_has_loaded(self):
    new_page = self.browser.find_element_by_tag_name('html')
    return new_page.id != self.old_page.id
  def __exit__(self, *_):
    wait_for(self.page_has_loaded)

Và bây giờ chúng ta có thể làm:

with wait_for_page_load(browser):
  browser.find_element_by_link_text('my link').click()

Và tôi nghĩ rằng điều đó có thể chỉ là chống đạn!

Và cho điểm thưởng ...

. Sử dụng chúng, cùng với công cụ trang trí @ContextManager và từ khóa năng suất magical-nhưng-slightly-scary, và bạn nhận được:staleness_of, as well as its own wait-for implementation. Use them, alongside the @contextmanager decorator and the magical-but-slightly-scary yield keyword, and you get:

from contextlib import contextmanager
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.expected_conditions import \
  staleness_of
class MySeleniumTest(SomeFunctionalTestClass):
  # assumes self.browser is a selenium webdriver
  @contextmanager
  def wait_for_page_load(self, timeout=30):
    old_page = self.browser.find_element_by_tag_name('html')
    yield
    WebDriverWait(self.browser, timeout).until(
      staleness_of(old_page)
    )
  def test_stuff(self):
    # example use
    with self.wait_for_page_load(timeout=10):
      self.browser.find_element_by_link_text('a link')
      # nice!

Lưu ý rằng giải pháp này chỉ hoạt động cho các nhấp chuột "không phải là JavaScript", tức là, các nhấp chuột sẽ khiến trình duyệt tải một trang hoàn toàn mới và do đó tải một phần tử cơ thể HTML hoàn toàn mới. Cho tôi biết bạn nghĩ gì!