From b05c11f370e69d7566d59d90883c8e8fec1c0024 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2026 00:39:36 -0400 Subject: [PATCH 1/4] Add a CDP-based `drag_and_drop()` method --- seleniumbase/core/browser_launcher.py | 1 + seleniumbase/core/sb_cdp.py | 126 ++++++++++++++++++++++++++ seleniumbase/fixtures/base_case.py | 2 +- 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 869f068eed1..fe4d359a22a 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -807,6 +807,7 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs): cdp.click_visible_elements = CDPM.click_visible_elements cdp.click_with_offset = CDPM.click_with_offset cdp.click_and_hold = CDPM.click_and_hold + cdp.drag_and_drop = CDPM.drag_and_drop cdp.mouse_click = CDPM.mouse_click cdp.get_parent = CDPM.get_parent cdp.remove_element = CDPM.remove_element diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index b4eea6066e1..713809a0cc8 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -973,6 +973,132 @@ def click_and_hold(self, selector, timeout=None, scroll=True, timeframe=2): self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.wait(0.2)) + def drag_and_drop( + self, drag_selector, drop_selector, timeout=None, scroll=True, + ): + """(Simulate a mouse drag & drop)""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + self.find_element(drag_selector, timeout=timeout) + drop_target = self.find_element(drop_selector, timeout=timeout) + if scroll: + drop_target.scroll_into_view() + script = f"""(() => {{ + var e = document.querySelector("{drag_selector}"), + n = document.querySelector("{drop_selector}"), + i = 0, o = 0, r = 1, + a = '', + s = 'alt' === a || '\ue00a' === a, + l = 'ctrl' === a || '\ue009' === a, + c = 'shift' === a || '\ue008' === a, + u = e.ownerDocument, + f = e.getBoundingClientRect(), + g = n ? n.getBoundingClientRect() : f, + p = f.left + f.width / 2, + d = f.top + f.height / 2, + h = g.left + (i || g.width / 2), + m = g.top + (o || g.height / 2), + v = u.elementFromPoint(p, d), + y = u.elementFromPoint(h, m); + if (!v || !y) {{ + var E = new Error( + 'source or target element is not interactable'); + throw E.code = 15, E; + }} + var _ = {{ + constructor: DataTransfer, + effectAllowed: null, + dropEffect: null, + types: [], + files: Object.setPrototypeOf([], null), + _items: Object.setPrototypeOf([], {{ + add: function(t, e) {{ + this[this.length] = {{ + _data: '' + t, + kind: 'string', + type: e, + getAsFile: function() {{}}, + getAsString: function(t) {{ t(this._data); }} + }}; + _.types.push(e); + }}, + remove: function(t) {{ + Array.prototype.splice.call(this, 65535 & t, 1); + _.types.splice(65535 & t, 1); + }}, + clear: function(t, e) {{ + this.length = 0; + _.types.length = 0; + }} + }}), + setData: function(t, e) {{ + this.clearData(t); this._items.add(e, t); }}, + getData: function(t) {{ + for ( + var e = this._items.length; + e-- && this._items[e].type !== t;); + return e >= 0 ? this._items[e]._data : null; + }}, + clearData: function(t) {{ + for ( + var e = this._items.length; + e-- && this._items[e].type !== t;); + this._items.remove(e); + }}, + setDragImage: function(t) {{}} + }}; + function w(t, e, n, i) {{ + for (var o = 0; o < e.length; ++o) {{ + var r = u.createEvent('MouseEvent'); + r.initMouseEvent( + e[o], !0, !0, u.defaultView, + 0, 0, 0, p, d, l, s, c, !1, 0, null); + t.dispatchEvent(r); + }} + i && setTimeout(i, n); + }} + function D(t, e, n, i) {{ + var o = u.createEvent('DragEvent'); + o.initMouseEvent( + e, !0, !0, u.defaultView, 0, 0, + 0, p, d, l, s, c, !1, 0, null); + Object.setPrototypeOf(o, null); + o.dataTransfer = _; + Object.setPrototypeOf(o, DragEvent.prototype); + t.dispatchEvent(o); + i && setTimeout(i, n); + }} + 'items' in DataTransfer.prototype && (_.items = _._items); + w(v, ['pointerdown', 'mousedown'], 1, function() {{ + for (var t = v; t && !t.draggable;) t = t.parentElement; + if (t && t.contains(v)) {{ + var e = y.getBoundingClientRect(); + D(v, 'dragstart', r, function() {{ + var t = y.getBoundingClientRect(); + p = t.left + h - e.left; + d = t.top + m - e.top; + D(y, 'dragenter', 1, function() {{ + D(y, 'dragover', r, function() {{ + D( + u.elementFromPoint(p, d), + 'drop', 1, function() {{ + D(v, 'dragend', 1, function() {{ + w( + u.elementFromPoint(p, d), + ['mouseup', 'pointerup']); + }}); + }}); + }}); + }}); + }}); + }} + }}); + }})();""" + self.evaluate(script) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + def mouse_click(self, selector, timeout=None, scroll=True): """(Simulate a mouse click)""" if not timeout: diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index b9483c39f96..667d7c90308 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -2998,7 +2998,7 @@ def drag_and_drop( drop_selector, drop_by ) if self.__is_cdp_swap_needed(): - self.cdp.gui_drag_and_drop(drag_selector, drop_selector) + self.cdp.drag_and_drop(drag_selector, drop_selector) return drag_element = self.wait_for_element_clickable( drag_selector, by=drag_by, timeout=timeout From 57f2a4ec5dd286222b114acb5d2272ad459d787d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2026 00:40:40 -0400 Subject: [PATCH 2/4] Update the docs --- examples/cdp_mode/ReadMe.md | 3 ++- help_docs/cdp_mode_methods.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index 0b4f62540f0..d97b008e340 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -390,9 +390,10 @@ sb.click_if_visible(selector, timeout=0, scroll=True) sb.click_visible_elements(selector, limit=0, scroll=True) sb.click_nth_element(selector, number, scroll=True) sb.click_nth_visible_element(selector, number, scroll=True) +sb.click_link(link_text) sb.click_with_offset(selector, x, y, center=False, scroll=True) sb.click_and_hold(selector, timeout=None, scroll=True, timeframe=2) -sb.click_link(link_text) +sb.drag_and_drop(drag_selector, drop_selector, timeout=None, scroll=True) sb.go_back() sb.go_forward() sb.get_navigation_history() diff --git a/help_docs/cdp_mode_methods.md b/help_docs/cdp_mode_methods.md index 7156091b031..55fa1d7fc86 100644 --- a/help_docs/cdp_mode_methods.md +++ b/help_docs/cdp_mode_methods.md @@ -36,9 +36,10 @@ sb.click_if_visible(selector, timeout=0, scroll=True) sb.click_visible_elements(selector, limit=0, scroll=True) sb.click_nth_element(selector, number, scroll=True) sb.click_nth_visible_element(selector, number, scroll=True) +sb.click_link(link_text) sb.click_with_offset(selector, x, y, center=False, scroll=True) sb.click_and_hold(selector, timeout=None, scroll=True, timeframe=2) -sb.click_link(link_text) +sb.drag_and_drop(drag_selector, drop_selector, timeout=None, scroll=True) sb.go_back() sb.go_forward() sb.get_navigation_history() From 9d9ffb4f3b335da6674917b395958c9378186067 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2026 00:41:12 -0400 Subject: [PATCH 3/4] Update CDP Mode examples --- examples/cdp_mode/raw_cdp_extended.py | 8 ++++---- examples/cdp_mode/raw_cdp_methods.py | 8 ++++---- examples/cdp_mode/raw_demo_site.py | 2 +- examples/cdp_mode/raw_drag_and_drop.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/cdp_mode/raw_cdp_extended.py b/examples/cdp_mode/raw_cdp_extended.py index e3073892da3..918111a8ee2 100644 --- a/examples/cdp_mode/raw_cdp_extended.py +++ b/examples/cdp_mode/raw_cdp_extended.py @@ -16,9 +16,9 @@ sb.set_value("input#mySlider", "100") sb.click_visible_elements("input.checkBoxClassB") sb.select_option_by_text("#mySelect", "Set to 75%") -sb.gui_hover_and_click("#myDropdown", "#dropOption2") -sb.gui_click_element("#checkBox1") -sb.gui_drag_and_drop("img#logo", "div#drop2") +sb.hover_and_click("#myDropdown", "#dropOption2") +sb.click("#checkBox1") +sb.drag_and_drop("img#logo", "div#drop2") sb.nested_click("iframe#myFrame3", ".fBox") -sb.sleep(2) +sb.sleep(1) sb.quit() diff --git a/examples/cdp_mode/raw_cdp_methods.py b/examples/cdp_mode/raw_cdp_methods.py index 53e0c99aea8..0e7e8fa7d0f 100644 --- a/examples/cdp_mode/raw_cdp_methods.py +++ b/examples/cdp_mode/raw_cdp_methods.py @@ -9,9 +9,9 @@ sb.set_value("input#mySlider", "100") sb.click_visible_elements("input.checkBoxClassB") sb.select_option_by_text("#mySelect", "Set to 75%") -sb.gui_hover_and_click("#myDropdown", "#dropOption2") -sb.gui_click_element("#checkBox1") -sb.gui_drag_and_drop("img#logo", "div#drop2") +sb.hover_and_click("#myDropdown", "#dropOption2") +sb.click("#checkBox1") +sb.drag_and_drop("img#logo", "div#drop2") sb.nested_click("iframe#myFrame3", ".fBox") -sb.sleep(2) +sb.sleep(1) sb.quit() diff --git a/examples/cdp_mode/raw_demo_site.py b/examples/cdp_mode/raw_demo_site.py index c1ef31b3697..fbf6e4e5feb 100644 --- a/examples/cdp_mode/raw_demo_site.py +++ b/examples/cdp_mode/raw_demo_site.py @@ -61,7 +61,7 @@ # Verify Drag and Drop sb.assert_element_not_visible("div#drop2 img#logo") - sb.gui_drag_and_drop("img#logo", "div#drop2") + sb.drag_and_drop("img#logo", "div#drop2") sb.assert_element("div#drop2 img#logo") # Click inside an iframe and test highlighting diff --git a/examples/cdp_mode/raw_drag_and_drop.py b/examples/cdp_mode/raw_drag_and_drop.py index 373dc602224..80427b4aae4 100644 --- a/examples/cdp_mode/raw_drag_and_drop.py +++ b/examples/cdp_mode/raw_drag_and_drop.py @@ -4,7 +4,7 @@ sb.activate_cdp_mode() sb.goto("https://seleniumbase.io/other/drag_and_drop") sb.assert_element_not_visible("#div1 img#drag1") - sb.gui_drag_and_drop("#drag1", "#div1") + sb.drag_and_drop("#drag1", "#div1") sb.assert_element("#div1 img#drag1") sb.sleep(1) From 89642ff4894e3351739dc359632566902379ca64 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2026 00:41:32 -0400 Subject: [PATCH 4/4] Version 4.51.8 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index cb615cae19d..db84d6ce6e1 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.51.7" +__version__ = "4.51.8"