-
Notifications
You must be signed in to change notification settings - Fork 2
/
seleniummanager.py
690 lines (611 loc) · 35.1 KB
/
seleniummanager.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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
"""
seleniummanager.py: Responsible for everything to do with Selenium including launching the webdriver, getting the
the solutions to the Datacamp questions, and solving the questions.
Contributors: Jackson Elia
"""
import pyperclip
import selenium
import selenium.common.exceptions
from ast import literal_eval
from html import unescape
from selenium.webdriver import ActionChains
from selenium.webdriver.common.alert import Alert
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
from selenium.common.exceptions import ElementNotInteractableException, TimeoutException, \
ElementClickInterceptedException, StaleElementReferenceException
from time import sleep
from terminal import DColors, DTerminal
from typing import List, Tuple
class SeleniumManager:
driver: selenium.webdriver
def __init__(self, driver: selenium.webdriver, terminal: DTerminal):
self.driver = driver
self.t = terminal
def login(self, username: str, password: str, link="https://www.datacamp.com/users/sign_in", timeout=15):
"""
Logs into datacamp.
:param username: Username or email for login
:param password: Corresponding password for login
:param link: The URL of the login page
:param timeout: How long before the program quits when it cannot locate an element
"""
self.driver.get(link)
self.t.log("Website loaded")
try:
# Username find and enter
try:
WebDriverWait(self.driver, timeout=timeout).until(
lambda d: d.find_element(By.ID, "user_email")).send_keys(username)
self.t.log("Username entered")
except ElementNotInteractableException:
self.t.log("Username error")
return
except TimeoutException:
self.t.log("Username field timed out before found, try again")
return
# Next button click
WebDriverWait(self.driver, timeout=timeout).until(
lambda d: d.find_element(By.XPATH, '//*[@id="new_user"]/button')).click()
# Password find and enter
sleep(1)
try:
WebDriverWait(self.driver, timeout=timeout).until(
lambda d: d.find_element(By.ID, "user_password")).send_keys(password)
self.t.log("Password entered")
except ElementNotInteractableException:
self.t.log("Password error")
return
except TimeoutException:
self.t.log("Password field timed out before found, try again")
return
# Sign in button click
WebDriverWait(self.driver, timeout=timeout).until(
lambda d: d.find_element(By.XPATH, '//*[@id="new_user"]/div[1]/div[3]/input')).click()
self.t.log("-*- Signed in -*-")
self.t.log(f"Successful with email: {username} and password: " + "*" * len(password))
# Finds the user profile to ensure that the login was registered
try:
WebDriverWait(self.driver, timeout=timeout).until(lambda d: d.find_element(By.CLASS_NAME, "mfe-app-atlas-header-ne9j7e"))
self.t.log("*****************")
self.t.log("Sign in confirmed")
self.t.log("*****************")
except TimeoutException:
self.t.log(DColors.red + "Error verifying sign in")
except TimeoutException:
self.t.log(DColors.red + "Next button or Sign in button not present, try logging in again")
def get_solutions_and_exercises(self, link: str) -> Tuple[list, List[dict]]:
"""
Uses a datacamp assignment link to get all the solutions for a chapter
:param link: The URL of the page
"""
self.driver.get(link)
script = self.driver.find_element(By.XPATH, "/html/body/script[1]").get_attribute("textContent")
script = unescape(script)
solutions = []
exercise_dicts = []
for segment in script.split(",["):
if ',"solution",' in segment and '"type","NormalExercise","id"' in segment:
# Slices solution from src code
solution = segment[segment.find('"solution","') + 12: segment.find('","type"')]
# Formats solution into usable strings/code
try:
solution = literal_eval('"' + unescape(literal_eval('"' + solution + '"')) + '"')
solutions.append(solution)
# TODO: Find a better way of doing this
# Every once and a while, if there is a string in the solution that represents a file path, it can break literal eval
except SyntaxError:
# Filters segments to only get the solutions, then removes some of the slashes
solution = solution.replace("\\\\n", "\n")
solution = solution.replace("\\\\t", "\t")
deletion_index_list = []
for index, char in enumerate(solution):
if char == "\\":
# Other escape characters not that important like who uses \ooo lol
if solution[index + 1] != "n" and solution[index + 1] != "t" and solution[
index - 2: index + 1] != ") \\":
deletion_index_list.append(index)
# Reverses list, so it doesn't mess with index when removing the character
for index in reversed(deletion_index_list):
solution = solution[:index] + solution[index + 1:]
solutions.append(solution)
number_1_found = 0
for segment in script.split(",["):
if 'Exercise","title","' in segment:
# Makes sure it only gets the full set of solutions once
if ',"number",1,"' in segment:
number_1_found += 1
if number_1_found > 1:
break
exercise_dict = {"type": segment[8:segment.find('Exercise","title","') + 8],
"number": segment[segment.find(',"number",') + 10:segment.find(',"url","')],
"link": segment[segment.find(',"url","') + 8:segment.find('"]]')]}
exercise_dicts.append(exercise_dict)
return solutions, exercise_dicts
def auto_solve_course(self, starting_link: str, timeout=10, reset_course=False, wait_length=0):
"""
Solves a whole Datacamp course.
:param starting_link: The link of the Datacamp course to solve
:param timeout: How long it waits for elements to appear
:param reset_course: If the course and xp earned from the course should be reset
:param wait_length: Delay in between exercises
"""
if reset_course:
self.driver.get(starting_link)
self.reset_course(timeout)
chapter_link = starting_link
done_with_course = False
while not done_with_course:
solutions, exercises = self.get_solutions_and_exercises(chapter_link)
next_chapter, chapter_link = self.auto_solve_chapter(exercise_list=exercises, solutions=solutions,
timeout=timeout, wait_length=wait_length)
done_with_course = not next_chapter
# TODO: Let user set how long in between solving
def auto_solve_chapter(self, exercise_list: List[dict], solutions: List[str], wait_length: int, timeout=10) -> \
Tuple[bool, str]:
"""
Automatically solves a Datacamp chapter, if it desyncs it will redo the chapter.
:param exercise_list: List of dicts that contain information about each exercise
:param solutions: List of solutions for each exercises
:param wait_length: Delay in between exercises
:param timeout: How long it waits for elements to appear
:return: A boolean for if there is another chapter in the course and a string with the url to that chapter
"""
self.driver.get(exercise_list[0]["link"])
max_tries = 2
for exercise in exercise_list:
exercise_solved = False
tries = 0
while not exercise_solved and tries < max_tries:
self.driver.get(exercise["link"])
self.t.log("******************")
self.t.log("Got exercise link: " + exercise["link"])
self.t.log("******************")
# User can set this to add delay in between exercises
sleep(wait_length)
tries += 1
match exercise["type"]:
case "VideoExercise":
self.t.log("Solving Video Exercise")
self.solve_video_exercise(timeout)
# It almost never messes up on video exercises
exercise_solved = True
case "NormalExercise":
self.t.log("Solving Normal Exercise")
if self.solve_normal_exercise(solutions[0], timeout):
solutions.pop(0)
exercise_solved = True
elif tries == max_tries:
solutions.pop(0)
case "BulletExercise":
self.t.log("Solving Bullet Exercise")
exercise_solved, solutions_used = self.solve_bullet_exercises(solutions, timeout)
if exercise_solved:
for i in range(solutions_used):
solutions.pop(0)
elif tries == max_tries:
for i in range(solutions_used):
solutions.pop(0)
# TODO: Find better way of managing solutions used for tab exercises, currently if it desyncs on a tab exercise it has to restart the module
case "TabExercise":
self.t.log("Solving Tab Exercise")
exercise_solved, solutions_used = self.solve_tab_exercises(solutions, timeout)
if exercise_solved:
for i in range(solutions_used):
solutions.pop(0)
elif tries == max_tries:
for i in range(solutions_used):
solutions.pop(0)
case "PureMultipleChoiceExercise":
self.t.log("Solving Pure Multiple Choice Exercise")
exercise_solved = self.solve_multiple1(timeout)
case "MultipleChoiceExercise":
self.t.log("Solving Multiple Choice Exercise")
exercise_solved = self.solve_multiple2(timeout)
case "DragAndDropExercise":
self.t.log("Solving Drag and Drop Exercise")
exercise_solved = self.solve_drag_and_drop(timeout)
case _:
self.t.log("What entered was an exercise not accounted for, raise an issue on the github")
# Refreshes the page to deal with popup
self.driver.refresh()
if exercise_list[-1]["type"] == "VideoExercise":
self.click_submit(timeout)
else:
# Clicks on the page, then enters in shortcut for the arrow button at the top
try:
self.wait_for_element(timeout=timeout, xpath='//*[@id="gl-aside"]').click()
except (TimeoutException, ElementClickInterceptedException):
self.t.log("The Exercise bar could not be clicked or found")
ActionChains(self.driver).key_down(Keys.CONTROL).send_keys("k").key_up(Keys.CONTROL).perform()
self.t.log("Sent ctrl + k")
# Waiting for next chapter to load
sleep(timeout)
if "https://app.datacamp.com/learn/courses" not in self.driver.current_url:
self.t.log(self.driver.current_url)
return True, self.driver.current_url
else:
self.t.log("-*- Finished Course -*-")
return False, ""
def reset_course(self, timeout: int):
"""
Resets all progress on the Datacamp course. Used to make sure all of the solve functions work properly.
:param timeout: How long it should wait to see certain buttons
"""
try:
course_outline_button = WebDriverWait(self.driver, timeout=timeout).until(
lambda d: d.find_element(By.CLASS_NAME, "css-b29ve4"))
course_outline_button.click()
self.t.log("Course outline button clicked")
self.t.log("-Resetting course XP-")
reset_button = WebDriverWait(self.driver, timeout=timeout).until(
lambda d: d.find_element(By.XPATH, "//button[contains(@data-cy,'outline-reset')]"))
# Several errors can happen with the rest button loading differently
try:
reset_button.click()
except ElementClickInterceptedException:
self.driver.execute_script("arguments[0].click();", reset_button)
sleep(.2)
# Presses enter twice to deal with the popups
Alert(self.driver).accept()
sleep(.2)
Alert(self.driver).accept()
self.t.log("/--------------\\")
self.t.log("Reset successful")
self.t.log("\\--------------/")
except TimeoutException:
self.t.log("The Course Outline Button was not found before timeout")
self.t.log(DColors.red + "Reset Failed")
def solve_video_exercise(self, timeout: int):
"""
Solves a Video exercise by clicking the "Got it" button.
:param timeout: How long it should wait to see the "Got it" button
"""
exercise_url = self.driver.current_url
self.click_submit(timeout=timeout)
sleep(2)
# Sometimes the got it button just doesn't get clicked
if self.driver.current_url == exercise_url:
try:
got_it_button = WebDriverWait(self.driver, timeout=2).until(
EC.element_to_be_clickable((By.XPATH, "//button[contains(@data-cy,'submit-button')]")))
self.driver.execute_script("arguments[0].click();", got_it_button)
self.t.log("Clicked the Got it button")
self.t.log("/--------------------------------\\")
self.t.log("Video Exercise solved successfully")
self.t.log("\\--------------------------------/")
return True
except ElementNotInteractableException:
self.t.log("Got it button could not be clicked")
return False
except TimeoutException:
self.t.log(DColors.red + "/ Got it button not found /")
return False
def solve_normal_exercise(self, solution: str, timeout: int) -> bool:
"""
Solves a Normal Exercise by pasting the solution into the editor tab, clicking the "Submit Answer" button and
then clicking the "Continue" button.
:param solution: The correct answer to the current Normal Exercise
:param timeout: How long it should wait to sees certain elements in the normal exercise
"""
try:
script_margin = self.wait_for_element(timeout, class_name="margin-view-overlays")
sleep(1)
# Clicks on the script to put it in focus
try:
script_margin.click()
except AttributeError:
self.t.log(DColors.red + "Script Margin could not be found")
action_chain = ActionChains(self.driver)
# TODO: Make it work for OSX
# Sends CTRL + A
pyperclip.copy(solution)
sleep(.2)
action_chain.key_down(Keys.CONTROL).send_keys("a").key_up(Keys.CONTROL).perform()
# Pastes the solution
action_chain.key_down(Keys.CONTROL).send_keys("v").key_up(Keys.CONTROL).perform()
except TimeoutException:
self.t.log(DColors.red + "/ Editor tab not found, most likely not a normal exercise /")
self.click_submit(timeout=timeout)
return self.find_continue(xpath="//button[contains(@data-cy,'next-exercise-button')]", timeout=timeout)
# TODO: Verify answers we correct through checking for if hints are given
def solve_bullet_exercises(self, solutions: List[str], timeout: int) -> Tuple[bool, int]:
"""
Solves a Bullet exercise by pasting the solution into the editor tab, clicking the "Submit Answer" button,
repeating this until it has completed all of the sub exercises, then clicking the "Continue" button.
:param solutions: The correct answer to the current Bullet exercise
:param timeout: How long it should wait to sees certain elements in the Bullet exercise
:return: How many solutions were used; The number of Bullet exercises
"""
answers_are_correct = True
number_of_exercises = "0"
try:
number_of_exercises = WebDriverWait(self.driver, timeout=timeout).until(lambda d: d.find_element(By.XPATH,
'//*[@id="gl-aside"]/div/aside/div/div/div/div[2]/div[1]/div/div/h5')) \
.text[-1]
for i in range(int(number_of_exercises)):
script_margin = self.wait_for_element(timeout, class_name="margin-view-overlays")
sleep(3) # Necessary for ctrl + a to select everything properly
# Clicks on the script to put it in focus
try:
script_margin.click()
except AttributeError:
self.t.log(DColors.red + "Script Margin could not be found, attempting to continue anyways")
action_chain = ActionChains(self.driver)
# Sends CTRL + A
action_chain.key_down(Keys.CONTROL).send_keys("a").key_up(Keys.CONTROL).perform()
# Copies the solution to clipboard
pyperclip.copy(solutions[i])
# Pastes the solution
# TODO: Make it work for OSX
action_chain.key_down(Keys.CONTROL).send_keys("v").key_up(Keys.CONTROL).perform()
self.click_submit(timeout=timeout)
sleep(timeout)
# TODO: Check if this is necessary anymore
if self.check_for_incorrect_submission(timeout=timeout):
answers_are_correct = False
# Clears clipboard
pyperclip.copy("")
if answers_are_correct:
return self.find_continue(xpath="//button[contains(@data-cy,'next-exercise-button')]",
timeout=timeout), int(number_of_exercises)
self.driver.refresh()
return self.solve_bullet_exercises(solutions, timeout)
except TimeoutException:
self.t.log(DColors.red + "Editor tab timed out, most likely not a bullet exercise")
return False, int(number_of_exercises)
except TypeError:
self.t.log(DColors.red + "Exercises and solving desynced, wait for restart of course")
return False, int(number_of_exercises)
def solve_tab_exercises(self, solutions: List[str], timeout: int) -> Tuple[bool, int]:
"""
Solves a Tab exercise by pasting the final solution into the editor tab, clicking the "Submit Answer" button,
repeating this until it has completed all of the sub exercises, then clicking the "Continue" button.
:param solutions: The correct answer to the current Tab exercise
:param timeout: How long it should wait to sees certain elements in the Tab exercise
:return: How many solutions were used; The number of Tab exercises
"""
solutions_used = 0
try:
number_of_exercises = WebDriverWait(self.driver, timeout=timeout).until(lambda d: d.find_element(By.XPATH,
'//*[@id="gl-aside"]/div/aside/div/div/div/div[2]/div[1]/div/div/h5')) \
.text[-1]
for i in range(int(number_of_exercises)):
try:
WebDriverWait(self.driver, timeout=timeout).until(lambda d: d.find_element(By.XPATH,
'//*[@id="gl-aside"]/div/aside/div/div/div/div[2]/div[1]/div')) \
.click()
except (TimeoutException, ElementClickInterceptedException):
self.t.log(DColors.red + "The Instruction bar could not be clicked or found")
ActionChains(self.driver).send_keys(Keys.PAGE_DOWN).perform()
# Some Tab exercises have multiple choice questions
possible_multiple_choice_options = len(
self.driver.find_elements_by_xpath("//ul[contains(@class,'exercise--multiple-choice')]/*"))
if possible_multiple_choice_options > 0:
self.t.log("Tab exercise has a multiple choice section")
self.t.log(f"There are {possible_multiple_choice_options} options")
for j in range(possible_multiple_choice_options):
try:
radio_input_button = WebDriverWait(self.driver, timeout=timeout).until(
lambda d: d.find_element(By.XPATH,
f'//*[@id="gl-aside"]/div/aside/div/div/div/div[2]/div[2]/div/div/div/div/div/div/div[3]/ul/li[{j + 1}]/div/div/label'))
radio_input_button.click()
self.t.log("Clicked option " + str(j))
self.click_submit(timeout=timeout)
tab_number = int(WebDriverWait(self.driver, timeout=timeout).until(
lambda d: d.find_element(By.XPATH,
'//*[@id="gl-aside"]/div/aside/div/div/div/div[2]/div[1]/div/div/h5'))
.text[-3])
sleep(1)
if tab_number == i + 2:
break
# This is for if the multiple choice exercise is the last exercise
elif tab_number == number_of_exercises:
if self.find_continue(xpath="//button[contains(@data-cy,'next-exercise-button')]",
timeout=timeout):
self.t.log(f"It used {solutions_used} solutions!")
return True, solutions_used
except (ElementNotInteractableException, ElementClickInterceptedException):
self.t.log(DColors.red + "Radio button could not be clicked")
except TimeoutException:
self.t.log(DColors.red + "/ Radio button not found /")
else:
script_margin = self.wait_for_element(timeout, class_name="margin-view-overlays")
sleep(3) # Necessary for ctrl + a to select everything properly
try:
script_margin.click()
except AttributeError:
self.t.log(DColors.red + "Script Margin could not be found")
self.t.log("Clicked on Script")
# Copies the solution to clipboard
pyperclip.copy(solutions[solutions_used])
solutions_used += 1
# Clicks on the script to put it in focus
try:
script_margin.click()
except AttributeError:
self.t.log(DColors.red + "Script Margin could not be found")
# Sends CTRL + A
ActionChains(self.driver).key_down(Keys.CONTROL).send_keys("a").key_up(Keys.CONTROL).perform()
# Pastes the solution
ActionChains(self.driver).key_down(Keys.CONTROL).send_keys("v").key_up(Keys.CONTROL).perform()
self.t.log("Pasted answer")
self.click_submit(timeout=timeout)
self.wait_for_element(timeout=timeout, xpath="//button[contains(@data-cy,'submit-button')]")
# Clears clipboard
pyperclip.copy("")
return self.find_continue(xpath="//button[contains(@data-cy,'next-exercise-button')]",
timeout=timeout), solutions_used
except ElementNotInteractableException:
self.t.log(DColors.red + "Editor tab could not be clicked")
except TimeoutException:
self.t.log(DColors.red + "/ Number of exercises or Editor tab not found, most likely not a bullet exercise /")
return False, solutions_used
except TypeError:
self.t.log(DColors.red + "Exercises and solving desynced, wait for restart of course")
return False, solutions_used
def solve_multiple1(self, timeout: int) -> bool:
"""
Solves a Pure Multiple Choice exercise by sending the number that corresponds to each multiple choice option and
the enter key until it finds the correct answer, then it clicks the "Continue" button.
:param timeout: How long it should wait to sees certain elements in the Pure Multiple Choice exercise
"""
# Gets the amount of the child elements (the multiple choice options) in the parent element
WebDriverWait(self.driver, timeout=timeout).until(
lambda d: d.find_element(By.XPATH, '//*[@id="root"]/div/main/div[1]/section/div/div[5]/div/div/ul'))
multiple_choice_options = len(
self.driver.find_elements_by_xpath('//*[@id="root"]/div/main/div[1]/section/div/div[5]/div/div/ul/*'))
self.t.log(f"Found {multiple_choice_options} multiple choice options")
for i in range(multiple_choice_options):
self.t.log("Trying option " + str(i + 1))
WebDriverWait(self.driver, timeout=timeout).until(lambda d: d.find_element(By.XPATH, "//body")).click()
ActionChains(self.driver).send_keys(str(i + 1), Keys.ENTER).perform()
if self.find_continue(xpath="//button[contains(@data-cy,'completion-pane-continue-button')]",
timeout=timeout):
return True
return self.find_continue(xpath="//button[contains(@data-cy,'completion-pane-continue-button')]",
timeout=timeout)
def solve_multiple2(self, timeout: int) -> bool:
"""
Solves a Multiple Choice exercise by going through each of the options and checking to see if it is the correct
one.
:param timeout: How long it should wait to sees certain elements in the Multiple Choice exercise
"""
# Gets the length of the child elements (the multiple choice options) in the parent element
# TODO: Find out if this is necessary
try:
WebDriverWait(self.driver, timeout=timeout).until(lambda d: d.find_element(By.XPATH,
'//*[@id="gl-aside"]/div/aside/div/div/div/div[2]/div[2]/div/div/div[2]/ul'))
self.t.log("Exercise loaded")
except ElementNotInteractableException:
self.t.log(DColors.red + "Radio button could not be clicked")
except TimeoutException:
self.t.log(DColors.red + "/ Radio button not found /")
multiple_choice_options = len(self.driver.find_elements_by_xpath(
'//*[@id="gl-aside"]/div/aside/div/div/div/div[2]/div[2]/div/div/div[2]/ul/*'))
self.t.log(f"Found {multiple_choice_options} multiple choice options")
for i in range(multiple_choice_options):
self.t.log("Trying option " + str(i + 1))
try:
radio_input_button = WebDriverWait(self.driver, timeout=timeout).until(
lambda d: d.find_element(By.XPATH,
f'//*[@id="gl-aside"]/div/aside/div/div/div/div[2]/div[2]/div/div/div[2]/ul/li[{i + 1}]/div/div/label'))
radio_input_button.click()
self.t.log("Clicked a radio button")
self.click_submit(timeout=timeout)
if self.find_continue(xpath="//button[contains(@data-cy,'next-exercise-button')]", timeout=timeout):
return True
except ElementNotInteractableException:
self.t.log(DColors.red + "Radio button could not be clicked")
except TimeoutException:
self.t.log(DColors.red + "/ Radio button not found /")
sleep(1) # Might not be necessary
return self.find_continue(xpath="//button[contains(@data-cy,'next-exercise-button')]", timeout=timeout)
def solve_drag_and_drop(self, timeout: int) -> bool:
"""
Skips drag and drop by showing answer, clicking submit answer, and clicking continue.
:param timeout: How long it should wait to sees certain elements in the drag and drop exercise
"""
try:
show_hint_button = WebDriverWait(self.driver, timeout=timeout).until(lambda d: d.find_element(By.XPATH,
'//*[@id="root"]/div/main/div[2]/div/div[1]/section/div[1]/div[5]/div/section/nav/div/button'))
show_hint_button.click()
show_answer_button = WebDriverWait(self.driver, timeout=timeout).until(lambda d: d.find_element(By.XPATH,
'//*[@id="root"]/div/main/div[2]/div/div[1]/section/div[1]/div[5]/div/section/nav/div/button'))
show_answer_button.click()
self.click_submit(timeout=timeout)
sleep(1)
ActionChains(self.driver).send_keys(Keys.ENTER).perform()
self.t.log("Skipped drag and drop exercise successfully")
return True
except TimeoutException:
self.t.log(DColors.red + "/ One of the buttons not found before timeout, most likely was not a drag and drop exercise /")
return False
# Only bullet exercises seem to be having this problem
def check_for_incorrect_submission(self, timeout: int, xpath="//button[contains(@aria-label,'Incorrect')]") -> bool:
"""
Checks to see if the answer entered was incorrect by checking for an element that marks it
:param timeout: How long it should wait to find the element
:param xpath: XPATH of the element, default should work
:return: Boolean for if it found the element or not
"""
try:
WebDriverWait(self.driver, timeout=timeout / 2).until(EC.element_to_be_clickable((By.XPATH, xpath)))
self.t.log("Answer was wrong")
return True
except TimeoutException:
return False
def click_submit(self, timeout: int, xpath="//button[contains(@data-cy,'submit-button')]") -> bool:
"""
Clicks the submit button for an exercise.
:param timeout: How long it should wait to find the submit button
:param xpath: The xpath of the submit button, default value should work for all of them
:return: A boolean for if it successfully clicked the submit button
"""
try:
WebDriverWait(self.driver, timeout=timeout).until(EC.element_to_be_clickable((By.XPATH, xpath))).click()
self.t.log("[] Clicked the Submit button []")
return True
except ElementNotInteractableException:
self.t.log("Submit button could not be clicked")
return False
except TimeoutException:
self.t.log("/ Submit button not found /")
return False
def wait_for_element(self, timeout: int, xpath="", class_name=""):
"""
Waits for an element and returns one if found. Can use either xpath or class name to find the element.
:param timeout: How long it should wait to find the element
:param xpath: The xpath of the element (don't specify if searching by class name)
:param class_name: The class name of the element (don't specify if searching by xpath)
"""
try:
element = None
if xpath != "":
element = WebDriverWait(self.driver, timeout=timeout).until(
EC.element_to_be_clickable((By.XPATH, xpath)))
elif class_name != "":
element = WebDriverWait(self.driver, timeout=timeout).until(
EC.element_to_be_clickable((By.CLASS_NAME, class_name)))
else:
raise ValueError
if type(element) == WebElement and element is not None:
return element
except StaleElementReferenceException:
self.t.log(DColors.red + "Element was stale")
self.wait_for_element(timeout, xpath="//button[contains(@data-cy,'submit-button')]")
except TimeoutException:
self.t.log("/ Element not found /")
return
if xpath != "":
self.wait_for_element(timeout, xpath=xpath)
elif class_name != "":
self.wait_for_element(timeout, class_name=class_name)
def find_continue(self, xpath: str, timeout: int) -> bool:
"""
Returns a boolean based on if the continue button was found or not.
:param timeout: How long it should wait to find the continue button
:param xpath: The xpath of the continue button
"""
try:
WebDriverWait(self.driver, timeout=timeout).until(lambda d: d.find_element(By.XPATH, xpath))
self.t.log("Found the Continue button")
self.t.log("/--------------------------\\")
self.t.log("Exercise solved successfully")
self.t.log("\\--------------------------/")
return True
except TimeoutException:
try:
WebDriverWait(self.driver, timeout=2).until(
lambda d: d.find_element(By.XPATH, '//*[@id="root"]/div/main/div[2]/div/div/div[3]/button'))
self.t.log("Found the Continue button")
self.t.log("/--------------------------\\")
self.t.log("Exercise solved successfully")
self.t.log("\\--------------------------/")
return True
except TimeoutException:
self.t.log("/ Continue button not found /")
return False