Триггер onchange через Selenium из Python

У меня есть Django webapp, отображающий форму. Одним из полей является поле FileField, определенное через Django модель формы:

class Document(models.Model):
    ...
    description = models.CharField(max_length=100, default="")
    document = models.FileField(upload_to="documents/", max_length=500)

К полю файла document подключена onchange ajax-функция.

Вот код html-страницы в том виде, в котором она отображается:

<div class="">
    <input type="file" name="document" onchange="checkFileFunction(this.value, &#x27;/ajax/check_file/&#x27;)" class="clearablefileinput form-control-file" required id="id_document">
 </div>            
                

Теперь я пытаюсь проверить это с помощью pytest через Selenium.

Я могу отправить путь к файлу в поле через send_keys(). Однако событие onchange, похоже, не срабатывает. (Оно работает нормально, когда я выбираю файл вручную.)

file_field = self.driver.find_element(By.NAME, "document")
file_field.clear()
file_field.send_keys(str(path/to/myfile))

Это зарегистрирует файл и он будет загружен, но функция onchange никогда не произойдет.

Я искал, и, похоже, другие тоже сталкивались с проблемой, когда send_keys не срабатывает событие onchange. Но я не смог реализовать ни одно из предложенных решений в своем Python-коде. (Я еще совсем не разбираюсь в JavaScript.)

Единственным решением, которое я понял, как реализовать, была отправка TAB или ENTER после этого (file_field.send_keys(Keys.TAB)) для изменения фокуса, но это вызывает

selenium.common.exceptions.InvalidArgumentException: Message: invalid argument: File not found

(Файл, который я ввел, существует, путь к нему правильный. Я могу успешно вызвать .exists() на нем.)

Как я могу вызвать событие onchange через Selenium из Python? Или иным образом убедиться, что оно вызывается?

Прежде всего, ваша спецификация onchange немного kludgy и предпочтительнее было бы указать как:

<input type="file" name="document" onchange="checkFileFunction(this.value, '/ajax/check_file/');">

Я использую Selenium с последней версией Chrome и его ChromeDriver под Windows 10 и не имею проблем с получением события onchange. Это можно продемонстрировать на примере следующего HTML-документа. Если событие onchange будет принято, то должен быть создан новый элемент div с id 'result', который будет содержать путь к выбранному имени файла:

Файл test.html

<!doctype html>
<html>
<head>
<title>Test</title>
<meta name=viewport content="width=device-width,initial-scale=1">
<meta charset="utf-8">
<script>
function checkFileFunction(value)
{
    const div = document.createElement('div');
    div.setAttribute('id', 'result');
    const content = document.createTextNode(value);
    div.appendChild(content);
    document.body.appendChild(div);
}
</script>
</head>
<body>
<input type="file" name="document" onchange="checkFileFunction(this.value);">
</body>
</html>

Далее у нас есть простая программа Selenium, которая посылает путь к файлу в элемент file input, затем ждет до 3 секунд (с вызовом driver.implicitly_wait(3)), пока на текущей странице не будет найден элемент с id значением 'result', а затем выводит текстовое значение. Этот элемент будет существовать, только если произойдет событие onchange:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("headless")
options.add_experimental_option('excludeSwitches', ['enable-logging'])
driver = webdriver.Chrome(options=options)

try:
    # Wait up to 3 seconds for an element to appear
    driver.implicitly_wait(3)

    driver.get('http://localhost/test.html')

    file_field = driver.find_element_by_name("document")
    file_field.clear()
    file_field.send_keys(r'C:\Util\chromedriver_win32.zip')
    result = driver.find_element_by_id('result')
    print(result.text)
finally:
    driver.quit()

Prints:

C:\Util\chromedriver_win32.zip

Теперь, если ваш драйвер другой и именно по этой причине не происходит событие onchange и вы не хотите или не можете перейти на последний ChromeDriver, то вы можете вручную выполнить функцию, указанную в аргументе onchange. В этой версии HTML-файла я не указал аргумент onchange, чтобы смоделировать ситуацию, когда его указание не имеет никакого эффекта:

Файл test.html Версия 2

<!doctype html>
<html>
<head>
<title>Test</title>
<meta name=viewport content="width=device-width,initial-scale=1">
<meta charset="utf-8">
<script>
function checkFileFunction(value)
{
    const div = document.createElement('div');
    div.setAttribute('id', 'result');
    const content = document.createTextNode(value);
    div.appendChild(content);
    document.body.appendChild(div);
}
</script>
</head>
<body>
<input type="file" name="document">
</body>
</html>

И новый код Selenium:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("headless")
options.add_experimental_option('excludeSwitches', ['enable-logging'])
driver = webdriver.Chrome(options=options)

try:
    # Wait up to 3 seconds for an element to appear
    driver.implicitly_wait(3)

    driver.get('http://localhost/test.html')

    file_field = driver.find_element_by_name("document")
    file_field.clear()
    file_path = r'C:\Util\chromedriver_win32.zip'
    file_field.send_keys(file_path)
    # Force execution of the onchange event function:
    driver.execute_script(f"checkFileFunction('{file_path}');")
    result = driver.find_element_by_id('result')
    print(result.text)
finally:
    driver.quit()

В вашем случае вы бы указали:

file_path = str(path/to/myfile)
file_field.send_keys(file_path)
self.driver.execute_script(f"checkFileFunction('{file_path}', '/ajax/check_file/');")

Как выяснилось, проблема заключалась в том, что мои ручные тесты проводились на приложении Django, обслуживаемом через python manage.py runserver. Это вызывает некоторую скрытую магию Django, включая сбор файлов статики (css, jQuery.js и т.д.) под капотом.

Теперь я узнал, что для того, чтобы обслуживать приложение Django на соответствующем сервере, нужно сначала вызвать python manage.py collectstatic. Это создаст папку static в родительском каталоге, которая содержит все статические файлы, а также явную папку jQuery.js.

Тогда, когда Selenium будет запущен, он найдет эту папку static и находящийся в ней файл jQuery.js. И затем все работает как ожидалось, включая onchange.

Итак, проблема заключалась в том, что отсутствовала родительская папка static, которую я никогда не видел, потому что для обслуживания сайта через python manage.py runserver она не нужна.

Вернуться на верх