Триггер 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, '/ajax/check_file/')" 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 она не нужна.