-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
277 lines (236 loc) · 10.4 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Selenium Automation GUI Tool (Thread-Safe Dialog Edition)
=========================================================
Key Changes:
------------
1. We no longer capture XPath in a background thread. Instead, we use
Tk's 'after()' to poll the browser from the main thread.
2. This fixes the 'TclError' that arises when trying to open a dialog
from a non-main thread.
3. The browser is still started in a separate thread to avoid blocking,
but everything involving Tk dialogs and UI updates happens in the
main thread.
Usage:
------
1. Run the script.
2. Enter a URL and click "Start Browser".
3. Click "Add Click Action" or "Add Type Action".
- The JavaScript is injected to capture the next clicked element in the browser.
- The script polls the browser every second on the main thread until it sees
a captured XPath or times out at 10 seconds.
- For "Type Action", it will then open a prompt (askstring) for text.
4. Actions appear in the listbox if successful.
5. Click "Run Actions" to perform them with the specified delay.
"""
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import threading
import time
class SeleniumAutomationGUI:
def __init__(self, master: tk.Tk) -> None:
self.master = master
self.master.title("Selenium Automation Tool")
# === URL Input ===
self.url_label = ttk.Label(master, text="URL:")
self.url_label.grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
self.url_entry = ttk.Entry(master, width=50)
self.url_entry.grid(row=0, column=1, columnspan=2, padx=5, pady=5, sticky=tk.W)
self.start_browser_button = ttk.Button(master, text="Start Browser", command=self.start_browser)
self.start_browser_button.grid(row=0, column=3, padx=5, pady=5, sticky=tk.E)
# === Delay Time Input ===
self.delay_label = ttk.Label(master, text="Delay (seconds):")
self.delay_label.grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
self.delay_entry = ttk.Entry(master, width=10)
self.delay_entry.insert(0, "1")
self.delay_entry.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)
# === Action Buttons ===
self.add_click_button = ttk.Button(master, text="Add Click Action", command=self.add_click_action)
self.add_click_button.grid(row=2, column=0, padx=5, pady=5)
self.add_type_button = ttk.Button(master, text="Add Type Action", command=self.add_type_action)
self.add_type_button.grid(row=2, column=1, padx=5, pady=5)
self.run_button = ttk.Button(master, text="Run Actions", command=self.run_actions)
self.run_button.grid(row=2, column=2, padx=5, pady=5)
self.clear_button = ttk.Button(master, text="Clear Actions", command=self.clear_actions)
self.clear_button.grid(row=2, column=3, padx=5, pady=5)
# === Actions Listbox ===
self.actions_listbox = tk.Listbox(master, width=80, height=15)
self.actions_listbox.grid(row=3, column=0, columnspan=4, padx=5, pady=5)
# === Selenium Driver and Actions ===
self.driver = None
self.actions = []
# We store a queue of things we need to do after we get the XPath
self._capture_mode = None # 'click' or 'type'
self._poll_attempts = 0
self._max_attempts = 10
# Bind closing event
self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
def start_browser(self) -> None:
"""
Start the Chrome browser in a background thread to avoid blocking the main GUI.
"""
url = self.url_entry.get()
if not url:
messagebox.showerror("Error", "Please enter a URL")
return
def _start_driver():
try:
self.driver = webdriver.Chrome()
self.driver.get(url)
except Exception as e:
messagebox.showerror("Error", f"Could not start browser: {e}")
threading.Thread(target=_start_driver, daemon=True).start()
def add_click_action(self) -> None:
if not self.driver:
messagebox.showerror("Error", "Please start the browser first.")
return
self._capture_mode = 'click'
self._poll_attempts = 0
self.inject_xpath_listener()
def add_type_action(self) -> None:
if not self.driver:
messagebox.showerror("Error", "Please start the browser first.")
return
self._capture_mode = 'type'
self._poll_attempts = 0
self.inject_xpath_listener()
def inject_xpath_listener(self) -> None:
"""
Inject a JS snippet to capture the next clicked element's XPath.
Then poll from the main thread using root.after to avoid threading issues.
"""
js_code = """
(function() {
function getXPath(element) {
if (element.id !== '') return '//*[@id="' + element.id + '"]';
if (element === document.body) return '/html/body';
let ix = 0;
const siblings = element.parentNode.childNodes;
for (let i = 0; i < siblings.length; i++) {
const sibling = siblings[i];
if (sibling === element) {
return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
}
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
ix++;
}
}
}
// Attach a one-time event listener
document._capturedXPath = null; // Clear previous
document.addEventListener('click', function handler(e) {
const element = e.target;
const path = getXPath(element);
document._capturedXPath = path;
// Do NOT preventDefault or stopPropagation,
// to allow normal focusing, etc.
document.removeEventListener('click', handler, true);
}, { capture: true, once: true });
})();
"""
try:
self.driver.execute_script(js_code)
except Exception as e:
messagebox.showerror("Error", f"Could not inject JavaScript: {e}")
return
# Start polling for the captured XPath from the main thread
self.master.after(1000, self.poll_for_xpath)
def poll_for_xpath(self) -> None:
"""
Poll up to 10 times (every 1 second) to see if an element was clicked
and its XPath stored in document._capturedXPath.
"""
if not self.driver:
return # Driver might have closed
try:
xpath_value = self.driver.execute_script("return document._capturedXPath;")
except Exception as e:
messagebox.showerror("Error", f"Could not retrieve captured XPath: {e}")
return
if xpath_value:
# Successfully captured
self.handle_captured_xpath(xpath_value)
else:
self._poll_attempts += 1
if self._poll_attempts < self._max_attempts:
# Try again in 1 second
self.master.after(1000, self.poll_for_xpath)
else:
# Timed out
messagebox.showerror("Error", "No element was selected within the time limit. Action not added.")
def handle_captured_xpath(self, xpath_value: str) -> None:
"""
Once we have the XPath, handle it based on the capture mode (click or type).
For 'type', prompt for user input from the main thread.
"""
if self._capture_mode == 'click':
self.actions.append({
'type': 'click',
'xpath': xpath_value
})
self.actions_listbox.insert(tk.END, f"Click → {xpath_value}")
elif self._capture_mode == 'type':
# Prompt user for text. MUST happen on the main thread.
user_text = simpledialog.askstring("Text Input", "Enter text to type into the selected element:")
if not user_text:
messagebox.showinfo("Info", "No text provided. Action cancelled.")
return
self.actions.append({
'type': 'type',
'xpath': xpath_value,
'text': user_text
})
self.actions_listbox.insert(tk.END, f"Type '{user_text}' → {xpath_value}")
self._capture_mode = None # Reset
def run_actions(self) -> None:
if not self.driver:
messagebox.showerror("Error", "Please start the browser first.")
return
if not self.actions:
messagebox.showerror("Error", "No actions to run.")
return
try:
delay = float(self.delay_entry.get())
except ValueError:
messagebox.showerror("Error", "Delay must be a numeric value.")
return
def _execute_actions():
try:
for action in self.actions:
time.sleep(delay)
element = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, action['xpath']))
)
if action['type'] == 'click':
element.click()
elif action['type'] == 'type':
# Focus the field by clicking
element.click()
time.sleep(0.5)
element.clear()
element.send_keys(action['text'])
messagebox.showinfo("Success", "All actions have been executed.")
except Exception as e:
messagebox.showerror("Execution Error", str(e))
threading.Thread(target=_execute_actions, daemon=True).start()
def clear_actions(self) -> None:
self.actions.clear()
self.actions_listbox.delete(0, tk.END)
def on_closing(self) -> None:
if self.driver:
try:
self.driver.quit()
except Exception:
pass
self.master.destroy()
def main():
root = tk.Tk()
app = SeleniumAutomationGUI(root)
root.mainloop()
if __name__ == "__main__":
main()