【问题标题】:Trigger onchange via Selenium from Python通过 Python 中的 Selenium 触发 onchange
【发布时间】:2022-01-21 14:26:44
【问题描述】:

我有一个显示表单的 Django webapp。其中一个字段是 FileField,通过表单的 Django 模型定义:

来自models.py

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

document file_field 附加了一个onchange ajax 函数,该函数将解析上传的文件名,根据它检查一些数据库内容,并使用结果填充 html 页面上的其他字段。

来自forms.py

class DocumentForm(forms.ModelForm):
    class Meta:
        model = Document
        fields = ("document",)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.fields["customer"] = forms.CharField(initial="", required=True)
        self.fields["output_profile"] = forms.CharField(initial="", required=True)

        self.fields["document"].widget.attrs[
            "onchange"
        ] = "checkFileFunction(this.value, '/ajax/check_file/')"

来自urls.py

urlpatterns = [
    #...
    path("ajax/check_file/", views.check_file, name="ajax_check_file")
]

来自views.py

def check_file(request):

    full_data = {"my_errors": []}
    my_path = pathlib.Path(request.GET.get("file_path").replace("\\", os.sep))
    # parse customer ID from file_path
    # get data of customer from db
    # assemble everything into full_data

    return JsonResponse(full_data)

这是显示的完整 html 页面(从 Chrome 复制 => 显示源并清理了一些缩进和空格):

<!doctype html>
<html>

    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
        <link href="/static/css/main.css" rel="stylesheet" type="text/css">
        <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.css"/>
        <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.20/css/dataTables.jqueryui.css"/>

        <script src="/static/js/jquery.js"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
        <script type="text/javascript" src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.js"></script>
        <script type="text/javascript" src="https://cdn.datatables.net/1.10.20/js/dataTables.jqueryui.js"></script>

        <title>
            Convert RES FILE
        </title>

    </head>


    <body>

        <header id="header">
            <section class="top_menu_left">
                <a href="/accounts/login">Login</a>
                <a>&nbsp|&nbsp</a>
                <a href="/logout">Logout</a>
                <a>&nbsp|&nbsp</a>
                <a href="/edit_profile">Edit User</a>
                <a>&nbsp|&nbsp</a>
                <a href="/register">Register</a>
            </section>

            <section class="top_menu_right">
                <a href="/cartoon">About Us</a>
                <a>&nbsp|&nbsp</a>
                <a href="/contact">Contact Us</a>
                <a>&nbsp|&nbsp</a>
                <a href="/issue">Submit an issue</a>
                <a>&nbsp|&nbsp</a>
                <a href="/documentation">Documentation</a>
                <a>&nbsp|&nbsp</a>
                <a href="/home">Home</a>
            </section>

            <div id="topbanner" >
                <img src="/static/banner_small.png" alt="" width="100%" height="150"/>
            </div>

        </header>

        <aside id="leftsidebar">
            <section class="nav_account">
                <h4>Submit a New Request</h3>
                <ul>
                    <li><a href="/add_submission">Get Typing Results</a></li>
                    <li><a href="/add_comparison">Compare Typing Results</a></li>
                    <li><a href="/convert_typing_format">Convert Typing Format</a></li>
                </ul>
            </section>

            <section class="nav_tools">
                <h4>View Your Requests</h3>
                <ul>
                    <li><a href="/view_submission/?show_all=no">View My Submissions</a></li>
                </ul>
            </section>
        </aside>

        <section id="main">

    <p>&nbsp;</p>
    <h2>Convert Typing Results to Format of Choice</h2>
    <p>&nbsp;</p>

        <h3>Upload a file to our database</h3>
            <form method="post" enctype="multipart/form-data">
                <input type="hidden" name="csrfmiddlewaretoken" value="qqIKcsAynuE35MQ37dvjF5XeIyfcEbHb3wjtgygGZaigQReNxHLQewoDKcEb8Roj">

                <div id="div_id_document" class="form-group">
                    <label for="id_document" class=" requiredField">
                        Document<span class="asteriskField">*</span>
                    </label>

                    <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>
                </div>

                <input type="hidden" id="id_description" name="description" value="">

                <p>&nbsp;</p>

                <div id="div_id_customer" class="form-group">
                    <label for="id_customer" class=" requiredField">
                        Customer<span class="asteriskField">*</span>
                    </label>

                    <div class="">
                        <input type="text" name="customer" readonly class="textinput textInput form-control" required id="id_customer">
                    </div>
                </div>

                <div id="div_id_output_profile" class="form-group">
                    <label for="id_output_profile" class=" requiredField">
                        Output profile<span class="asteriskField">*</span>
                    </label>

                <div class="">
                    <input type="text" name="output_profile" readonly class="textinput textInput form-control" required id="id_output_profile">
                </div>

        </div>

        <div class="form-group">
            <div id="div_id_notify_me" class="form-check">
                <input type="checkbox" name="notify_me" style="width:15px;height:15px;" class="checkboxinput form-check-input" id="id_notify_me">
                <label for="id_notify_me" class="form-check-label">
                    Notify me
                </label>
            </div>
        </div>

        <p>
            <button class="linkbutton" type="submit" id="submit_btn">Convert</button>
            &nbsp;
            <button id="create-book" class="linkbutton" type="button" name="button" style="float: right;">Create an Output Profile</button>
        </p>
    </form>


    <div class="modal fade" tabindex="-1" role="dialog" id="modal">
        <div class="modal-dialog modal-lg" role="document">
            <div class="modal-content" id="form-modal-content">
            </div>
        </div>
    </div>

    <div class="modal fade" tabindex="-1" role="dialog" id="modal-check-res-file">
        <div class="modal-dialog" role="document">
            <div class="modal-content" id="form-modal-content-check-res-file">
            </div>
        </div>
    </div>

    <script>
        var formAjaxSubmit = function(form, modal) {
            $(form).on('submit', function (e) {
                e.preventDefault();
                console.log("submitting...");
                var my_val = $("#id_profile_name").val();
                var this_val =  $("#confirm_save").val();
                var res = this_val.split(",");
                var this_val_contains_my_val = (res.indexOf(my_val) > -1);

                if (this_val_contains_my_val === true) {
                    var conf = confirm("Are you sure want to overwrite an exsisting profile?");
                    }else {var conf = true;};
                if (conf === true) {
                $.ajax({
                    type: $(this).attr('method'),
                    url: "/new_customer_profile/",
                    data: $(this).serialize(),
                    success: function (xhr, ajaxOptions, thrownError) {

                        if ( $(xhr).find('.invalid-feedback').length > 0 ) {
                            $(modal).find('.modal-content').html(xhr);
                            formAjaxSubmit(form, modal);
                        } else {
                            $(modal).find('.modal-content').html(xhr);
                        }
                    },
                    error: function (xhr, ajaxOptions, thrownError) {
                    }
                });

                };
            });
        };

        $('#create-book').click(function() {
            console.log("hhallo");
            $('#form-modal-content').load('/new_customer_profile/', function () {
                var iam_alive = document.getElementById("modal");
                // check if iam_alive is defined, this is required if a session expired -> in that case the modal is lost and it would redirect to an almost empty page.
                if (iam_alive) {
                    $('#modal').modal('toggle');
                    formAjaxSubmit('#form-modal-body form', '#modal');
                }
                // if not iam_alive: redirect to login page
                else {
                    window.location.replace('/accounts/login/');
                }
            });
        });


        $('#check-res-file').click(function() {
            console.log("hhallo hier unten jetzt");
            $('#form-modal-content-check-res-file').load('/check_res_file/', function () {
                $('#modal-check-res-file').modal('toggle');
                //formAjaxSubmit('#form-modal-body form', '#modal');
            });
        });
    </script>

    <script type="text/javascript">
        $(document).ready(function() {
            var mycell = document.getElementById("create-book");
            mycell.style.display = "none";
        });
    </script>

    <script>
        function checkFileFunction(myfile, url) {
            $.ajax({                       // initialize an AJAX request
                url: url,                    // set the url of the request (= localhost:8000/hr/ajax/load-cities/)
                data: {"file_path": myfile},
                dataType: 'json',
                success: function (x) {
                    if (x.my_errors.length == 0) {
                        $('#id_customer').val(x.customer_name);
                        $('#id_output_profile').val(x.customer_profile);
                        $('#id_description').val(x.customer_file);
                    }else{
                        $('#id_customer').val("");
                        $('#id_customer').val("");
                        $('#id_output_profile').val("");
                        alert(x.my_errors);
                        var showme = function myFunction() {
                            var mycell = document.getElementById("create-book");
                            mycell.style.display = "block";
                            };
                        showme();
                    }
                    },
            });
        }
    </script>
    </section>
</body>
</html>

现在,我正在尝试通过 Selenium 使用 pytest 进行测试。

我可以通过 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 代码中实现任何建议的解决方案。 (我还没有写过这个应用程序的Django代码,我只是一个测试人员,我对Django和javascript的掌握还不是很好。我的母语编程语言是Python。)

我了解如何实施的唯一解决方案是之后发送 TAB 或 ENTER (file_field.send_keys(Keys.TAB)) 以更改焦点,但这会触发

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

(我输入的文件确实存在,路径没问题,可以成功调用.exists()。)

send_keys之后简单地选择一个不同的元素来转移焦点(即customer_field.click())也不会触发file_field的onchange函数。

如何通过 Python 中的 Selenium 触发 onchange 事件? 或者确保它被触发?

【问题讨论】:

  • 你怎么断定onchange没有被解雇?
  • @undetected Selenium 我有一个由 onchange 触发的函数。它在手动填充 file_field 时执行,但在使用 Selenium 填充时不会执行。所以我得出结论,onchange 没有发生。有不同的解释吗?
  • @undetectedSelenium 可以在上面的html代码中看到onchange-function:onchange="checkFileFunction(this.value, &amp;#x27;/ajax/check_file/&amp;#x27;)"有没有办法显式检查是否触发了onchange?

标签: python django selenium


【解决方案1】:

首先,您的 onchange 规范有点kludgy,最好指定为:

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

我在 Windows 10 下将 Selenium 与最新版本的 Chrome 及其 ChromeDriver 一起使用,并且在执行 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 程序,它向文件输入元素发送文件路径,然后等待最多 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()

打印:

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()

更新

我猜你错过了 部分中的 ,这似乎是 jQuery。但我会认为这个脚本标签位于 部分中,必须在 Selenium 找到文件元素时加载 jQuery。所以我承认你得到javascript error: $ is not defined 有点令人惊讶。我现在只能建议您尝试手动加载它,如下面的代码中所述。

如果前一种方法不起作用,我已经在下面的代码中重复了您应该按顺序尝试的 3 件事:

  1. 在发送击键之前给 jQuery 加载时间,以消除 $ 未定义 错误。
  2. 在发送击键之前手动加载 jQuery。
  3. 手动执行checkFileFunction
# 1. Give jQuery time to load before sending keystrokes:
import time
time.sleep(3)

# 2. if the above sleeping does not work,
# remove the above call to sleep and manually load jQuery.
# Specify where the home page would be loaded from:
document_path = '/home/usr/account/public_html' # for example
jQuery_path = document_path = '/static/js/jQuery.js'
with open(jQuery_path, 'r') as f:
    jQuery_js = f.read()
self.driver.execute_script(jQuery_js)

# Send keystrokes:
file_path = str(path/to/myfile)
file_field.send_keys(file_path)

# 3. Execute the following if the onchange doesn't fire by itself:
self.driver.execute_script(f"checkFileFunction('{file_path}', '/ajax/check_file/');")

【讨论】:

  • 感谢您的详尽回答!我让你的测试在我的机器上运行良好。我也在使用最新的 ChromeDriver。所以看起来onchange实际上不是问题吗?你有另一个想法可能是什么问题吗?我在我的问题中添加了更多的 Django 代码,也许这可以解决问题?
  • 我已经尝试添加你的最后一行,但我得到了一个javascript error: $ is not defined
  • 我猜你的 onchange 函数使用了 jQuery。如果是这种情况,请参阅my answer here
  • 这个 Django 应用程序中包含的唯一 .js 文件是包含在使用的 Python 包中的那些。
  • 嗯,有些东西是用来实现 Ajax 提取的。你在checkFileFunction 中编码了什么?在客户端,没有 Python 包,只有 HTML、JavaScript 和 CSS。如果您使用 Chrome 作为浏览器,您可以检查页面并查看加载了哪些源和 JavaScript 文件,或者仅查看页面源。查看页面源代码,如果您在 标记之后看到 jQuery 库的引用,那么在您尝试执行 checkFileFunction 时,它可能尚未加载,在这种情况下,您可能想要休眠在你这样做之前一段时间。 (更多...)
【解决方案2】:

事实证明,实际问题是我的手动测试是在通过python manage.py runserver 提供的 Django 应用程序上完成的。这调用了一些隐藏的 Django 魔法,包括在后台收集静态文件(css、jQuery.js 等)。

我现在了解到,为了在适当的服务器上提供 Django 应用程序,需要首先调用 python manage.py collectstatic。这将在父目录中生成一个static 文件夹,其中包含所有静态文件以及一个显式的jQuery.js

那么,当 Selenium 运行时,它会找到static 文件夹和其中的jQuery.js 文件。然后,一切都按预期进行,包括onchange

所以问题是,这个父 static 文件夹丢失了,我从未见过,因为通过 python manage.py runserver 为网站提供服务不需要它。

【讨论】:

  • 很高兴你终于发现了这一点。它让我明白了为什么 $ 未定义。
猜你喜欢
  • 1970-01-01
  • 2023-01-29
  • 1970-01-01
  • 2017-08-19
  • 2013-06-23
  • 1970-01-01
  • 2021-08-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多