Selenium: Loop through a list of XPATH variables and compare strings

Question:

I’m writing a bot in Python that uses Selenium to play a web-based of Tic Tac Toe. I want to loop through an array of XPATHs that represent the game grid and check each square for the presence of the ‘O’ character. If the square is marked by an O, the number of that square should be appended to another list of marked squares.

This is the section of code that I’m trying to fix:

    for i in Board.squares:
        text = driver.find_element(By.XPATH, Board.squares[i]).text
        if text == 'O':
            LOGGER.info("Square " + str(i) + " marked by O.")
            Board.markedSquares.append(i)
            LOGGER.info("Appending marked squares list: " + str(Board.markedSquares))

But I get the following error in traceback:

11/04/2022 10:32:49 AM–root– INFO:First move: clicking square 3
Traceback (most recent call last):
  File "C:UserssourcereposReact project_2ttt_user_botRandomBot.py", line 97, in <module>
    playTTT()
  File "C:UserssourcereposReact project_2ttt_user_botRandomBot.py", line 72, in playTTT 
    text = driver.find_element(By.XPATH, Board.squares[i]).text
TypeError: list indices must be integers or slices, not str

Below is the full code for my bot. Everything works except for the for loop above, and I’m not sure how to fix it. Without this check, the bot can click on a square marked by an O, and the script will time out.

class Tags():
    square1 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[1]"
    square2 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[2]"
    square3 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[3]"
    square4 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[4]"
    square5 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[5]"
    square6 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[6]"
    square7 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[7]"
    square8 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[8]"
    square9 = "(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[9]"
    ohSquare = "//div[contains(@class, 'board-row')]//button[contains(text(), 'O')]"
    winner = "//div[contains(@class, 'game-info')]//div[contains(text(), 'Winner:')]"
    tie = "//div[contains(@class, 'game-info')]//div[contains(text(), 'tie')]"

class Board():
    squares = [Tags.square1,Tags.square2,Tags.square3,
               Tags.square4,Tags.square5,Tags.square6,
               Tags.square7,Tags.square8,Tags.square9]
    markedSquares = []

def firstMove():
    
    random_square = randint(0,8)
    time.sleep(5)
    
    element = driver.find_element(By.XPATH, Board.squares[random_square])
    element.click()
    Board.markedSquares.append(random_square)
    LOGGER.info("First move: clicking square " + str(random_square))

def playTTT():

    random_square = randint(0,8)
    time.sleep(5)

    try:
        driver.find_element(By.XPATH, Tags.winner).is_displayed()
        text = driver.find_element(By.XPATH, Tags.winner).text
        LOGGER.info(str(text))
        driver.save_screenshot("screenshot.png")
        driver.close()
    except NoSuchElementException:
        pass
    try:
        driver.find_element(By.XPATH, Tags.tie).is_displayed()
        LOGGER.info("Tie")
        driver.save_screenshot("screenshot.png")
        driver.close()
    except NoSuchElementException:
        pass

    for i in Board.squares:
        text = driver.find_element(By.XPATH, Board.squares[i]).text
        if text == 'O':
            LOGGER.info("Square " + str(i) + " marked by O.")
            Board.markedSquares.append(i)
            LOGGER.info("Appending marked squares list: " + str(Board.markedSquares))

    try:
        for i in Board.markedSquares:
            if i == random_square:
                LOGGER.info("Square number " + str(i) + " already marked. Recomputing...")
                break
            else:
                element = driver.find_element(By.XPATH, Board.squares[random_square])
                element.click()
                Board.markedSquares.append(random_square)
                LOGGER.info("Clicking square:" + str(random_square))
                break
        
        LOGGER.info("Contents of markedSquares: " + str(Board.markedSquares))
        playTTT()
    except InvalidSessionIdException:
        pass

if __name__=='__main__':
    firstMove()
    playTTT()

Answers:

You have to loop through the length of the ‘Board.squares‘ list, like this:

for i in range(len(Board.squares)):
    text = driver.find_element(By.XPATH, Board.squares[i]).text

Output:

(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[1]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[2]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[3]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[4]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[5]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[6]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[7]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[8]
(//div[contains(@class, 'board-row')]//button[contains(@class, 'square')])[9]
Answered By: AbiSaran

I have a suggestion to improve the readability of your code and simplify it somewhat.
You can take that for loop and put it in your last try statement like so:

    try:
        for i in range(len(squares)):
            text = driver.find_element(By.XPATH, squares[random_square]).text
            if text == 'O':
                LOGGER.info("Square " + str(random_square) + " marked by O.")
                break
            elif text == 'X':
                LOGGER.info("Square " + str(random_square) + " already marked by X.")
                break
            else:
                element = driver.find_element(By.XPATH, squares[random_square])
                element.click()
                LOGGER.info("Clicking square:" + str(random_square))
                break
        playTTT()

Instead of appending to a list of "markedSquares" and checking that list every time before you click on a square, check the random_square for the presence of an X or an O using .text and then use break statements to bust out of the loop and get a new random integer. This way, you won’t click on a square that’s already marked and you get rid of your second list.

This way you can also get rid of your firstMove method, get rid of the Board class and move the squares list into your main method, playTTT().

Answered By: Trevor