pythonangularautomationplaywrightplaywright-python

My Playwright python script doesn't find input form fields


So I want to automate adding many team players, I wrote a script to do this with Playwright. As the players are much, the issues is that the script don't seem to find the input fields like the first name, last name and email address I have tried everything but to no avail, the website uses Angular for their frontend so their input fields are very tricky. Below is the code to wait for the inputs to become visible and fill it, please help me guys

import json
import random
from playwright.sync_api import sync_playwright, expect

# =========================
# CONFIGURATION
# =========================
COOKIE_FILE = "cookies.json"
BASE_URL = ""
TEAM_URL = ""

# Names
first_names = ["Emma", "John", "Samuel"]
last_names = ["jerry","Jude","Englehart"]

# =========================
# HELPER FUNCTIONS
# =========================
def random_name():
    """Generate a random name."""
    return random.choice(first_names), random.choice(last_names)

def save_cookies(context):
    """Save cookies to a file after login."""
    cookies = context.cookies()
    with open(COOKIE_FILE, "w") as f:
        json.dump(cookies, f)

def load_cookies(context):
    """Load cookies if they exist (avoid logging in again)."""
    try:
        with open(COOKIE_FILE, "r") as f:
            cookies = json.load(f)
            context.add_cookies(cookies)
            return True
    except FileNotFoundError:
        return False

# =========================
# MAIN SCRIPT
# =========================
# Load emails from players.txt
with open("players.txt", "r") as f:
    emails = [line.strip() for line in f if line.strip()]

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False, args=["--ignore-certificate-errors"])
    context = browser.new_context(ignore_https_errors=True)
    page = context.new_page()

    # Try using cookies for login
    if load_cookies(context):
        print("βœ… Using saved cookies for login.")
        page.goto(TEAM_URL, wait_until="domcontentloaded", timeout=60000)
    else:
        print("πŸ”‘ Logging in with credentials.")
        # Fresh login
        page.goto(BASE_URL)
        page.get_by_role("button", name="Login").click()
        
        # Wait for login form to load and fill in the credentials
        page.get_by_role("textbox", name="Username or email address").wait_for(state="visible", timeout=10000)
        page.get_by_role("textbox", name="Username or email address").fill("")
        
        page.get_by_role("textbox", name="Password").wait_for(state="visible", timeout=10000)
        page.get_by_role("textbox", name="Password").fill("")
        
        page.get_by_role("button", name="LOG IN").click()

        # Wait for successful login by checking if we're redirected to the team URL
        page.wait_for_url(TEAM_URL, wait_until="domcontentloaded", timeout=30000)

        # Save cookies after successful login
        save_cookies(context)

    # Navigate to squad page
    page.goto(TEAM_URL, wait_until="domcontentloaded", timeout=60000)

    page.get_by_role("link", name="squad").click()

    # Loop through players from file
    for email in emails:
        first, last = random_name()
        print(f"βž• Adding {first} {last} ({email})")

        # Open add member form
        page.get_by_role("button", name="ADD NEW").wait_for(state="visible", timeout=15000)
        page.get_by_role("button", name="ADD NEW").click()
        page.get_by_role("button", name="Add Single Member").click()

        # Wait for the input fields to become visible and fill them
        first_name_input = page.locator('div.ts-text-input input').first
        last_name_input = page.locator('div.ts-text-input input').nth(1)
        email_input = page.locator('div.ts-text-input input').nth(2)

        first_name_input.wait_for(state="visible", timeout=10000)
        first_name_input.fill(first)

        last_name_input.wait_for(state="visible", timeout=10000)
        last_name_input.fill(last)

        email_input.wait_for(state="visible", timeout=10000)
        email_input.fill(email)

        # Save and confirm
        page.get_by_role("button", name="SAVE").click()
        page.get_by_text("NO", exact=True).click(force=True)

        # Verify player was added
        try:
            expect(page.get_by_text(first)).to_be_visible(timeout=5000)
            print(f"βœ… Successfully added {first} {last} ({email})")
        except Exception:
            print(f"❌ Failed to verify {first} {last} ({email})")

    print("πŸŽ‰ Finished adding all players.")
    browser.close()


Solution

  • Here's an approach that works for me:

    from playwright.sync_api import sync_playwright # 1.53.0
    
    USERNAME = "<your username>"
    PASSWORD = "<your password>"
    BASE_URL = "https://www.teamstats.net/"
    TEAM_URL = "https://www.teamstats.net/enyimbafcaba/home"
    emails = (
        "alex.smith@example.com",
        "casey.lee@dummy.net",
        "foo.bar@example.com",
        "blah.x@dummy.net",
    )
    
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False, args=["--ignore-certificate-errors"])
        context = browser.new_context(ignore_https_errors=True)
        page = context.new_page()
    
        print("πŸ”‘ Logging in with credentials.")
        page.goto(BASE_URL)
        page.get_by_role("button", name="Login").click()
        page.get_by_role("textbox", name="Username or email address").wait_for()
        page.get_by_role("textbox", name="Username or email address").fill(USERNAME)
        page.get_by_role("textbox", name="Password").wait_for()
        page.get_by_role("textbox", name="Password").fill(PASSWORD)
        page.get_by_role("button", name="LOG IN").click()
        page.wait_for_url(TEAM_URL, wait_until="domcontentloaded")
        page.goto(TEAM_URL, wait_until="domcontentloaded")
        page.get_by_role("link", name="squad").click()
    
        for email in emails:
            first, last = [f"foo {email}", "bar"]
            print(f"βž• Adding {first} {last} ({email})")
    
            page.get_by_role("button", name="ADD NEW").click()
            page.get_by_role("button", name="Add Single Member").click()
            page.locator(".ts-card .ts-list-item").first.wait_for()
            first_name_input = page.locator("div.ts-text-input input").first
            first_name_input.fill(first)
            last_name_input = page.locator("div.ts-text-input input").nth(1)
            last_name_input.fill(last)
            email_input = page.locator("div.ts-text-input input").nth(4)
            email_input.fill(email)
            page.get_by_role("button", name="SAVE").click()
    
            try:
                page.get_by_text("NO", exact=True).click(force=True, timeout=5_000)
            except:
                pass
    
        print("πŸŽ‰ Finished adding all players.")
        browser.close()
    

    The most critical parts are:

    1. page.locator(".ts-card .ts-list-item").first.wait_for() which waits for the player list to load. For whatever reason, inputs don't seem to work until this list is populated.
    2. email_input = page.locator("div.ts-text-input input").nth(4) the email is the 5th, not the 3rd element in the input list.

    A potentially better approach is to send direct HTTP requests.

    Once you have the cookie, then you can often skip browser automation entirely and just make a fetch request to create the data from Node or Python, or from the browser console (with evaluate).

    If you open the network tab, fill out a new team member manually as a human, then find the POST request that was sent, you can copy the payload:

    screenshot of dev tools showing POST to create a player

    screenshot of dev tools showing how to copy the fetch call

    This gives:

    await fetch("https://api.teamstats.net/api/member/setMember.asp", {
      "headers": {
        "accept": "application/json, text/plain, */*",
        "accept-language": "en-US,en;q=0.9",
        "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
        "priority": "u=1, i",
        "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"macOS\"",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-site",
        "cookie": "_scor_uid=3f65e4a414ca420d8cb09150b34efaeb; _gid=GA1.2.908336851.1756579405; _fbp=fb.1.1756579404993.119640189542663532; crisp-client%2Fsession%2F5af2bc25-165c-4cb1-b9e9-a56f55a70791=session_116014da-1f89-4d26-8e6d-e1f415d390b3; pb_ga=GA1.2.1069470341.1756579405; pb_ga_gid=GA1.2.838530501.1756579416; serverName=https%3A%2F%2Fwww%2Eteamstats%2Enet; _ga=GA1.2.1069470341.1756579405; pb_ga_ga_2ZPQ07VLSX=GS2.2.s1756579416$o1$g1$t1756579777$j60$l0$h0; ASPSESSIONIDCEQRABSS=DNOELLFBOIMNBKFJFDJIMJNI; cto_bundle=lI3fGF9yJTJGd3FmYmJOdFBmd3JkUnUzb3pBMkJJVWw3TUFHTkw2emdVVEkxTU41bE1KYXM2Q2dsUEZJaVFDOHRQSCUyQmxtVWYwVWl5ZkV4Q1VGTEFhUkdvTmRvVTE1VmprR1RGQTNZa1BnS0tRVERDSkpMYTR0cE1qajRnY2pwWllZWnVZZlhTTyUyQnlPcnhUYmxBWDJwTm9GaGJUcU43cWljSUpqN1Q3U3JGVHc4dmVvRkxSY1FoRHlPeHI4dDJtdHBnclhYNERwUWpIa0laSiUyQnAlMkJPdDh3eDNRMWV6dyUzRCUzRA; _gat_UA-85619626-2=1; visitorHash=; _ga_YCF3Y4G5KN=GS2.1.s1756579404$o1$g1$t1756579868$j42$l0$h0; amp_eb3a2f=L_J-EHB6EYh4PKZj_xwY3G...1j3u50ls0.1j3u53bhe.4.0.4",
        "Referer": "https://www.teamstats.net/"
      },
      "body": "sid=D86DD940-D285-F011-BB93-14187749D29F&path=%2Fenyimbafcaba%2Fadmin%2Fmembers%2Fedit%2F0%2Fnew%2Fdetails&mobile=false&lastAccessedDate=20250830%2011%3A51%3A33&stateName=admin.members.edit.details&stateUniqueId=0&stateUrlTitle=new&adsDataLastUpdated=30-Aug-2025%2019%3A33%3A12&tsVersionNum=3.4.05&tsPlatform=web&appDataLastUpdated=29-Aug-2025%2009%3A16%3A12&teamDataLastUpdated=30-Aug-2025%2019%3A33%3A12&teamId=1679386&clubId=48521&timeZoneOffset=1&teamName=Enyimba%20FC%20Aba&teamSiteUrl=enyimbafcaba&timeZoneCountryCode=NG&sportId=1&isClub=false&userUsername=uiwehee&userId=719499&userDataLastUpdated=30-Aug-2025%2019%3A50%3A51&isTeamMember=true&isTeamAdmin=true&isLoggedIn=true&jsonData=%7B%22UserID%22%3A0%2C%22UserTitle%22%3A%22%22%2C%22UserFirstName%22%3A%22xyx%22%2C%22UserSurname%22%3A%22bar%22%2C%22UserUsername%22%3A%22%22%2C%22UserPassword%22%3A%22%22%2C%22UserLastLoggedIn%22%3Anull%2C%22UserEmail%22%3A%22baz%40gmail.com%22%2C%22UserMobileNumber%22%3A%22%22%2C%22UserEmailSecondary%22%3A%22%22%2C%22UserMobileNumberSecondary%22%3A%22%22%2C%22UserDOB%22%3A%22%22%2C%22UserAddress%22%3A%22%22%2C%22UserPostCode%22%3A%22%22%2C%22UserNotes%22%3A%22%22%2C%22UserTwitterUsername%22%3A%22%22%2C%22TeamMemberID%22%3A0%2C%22TeamMemberIntroText%22%3A%22%22%2C%22TeamMemberPhotoMediaID%22%3Anull%2C%22TeamMemberCoverMediaID%22%3Anull%2C%22TeamMemberPhotoFileName%22%3Anull%2C%22TeamMemberCoverPhotoFileName%22%3Anull%2C%22TeamMemberPhotoPath%22%3A%22%2Fteamstats-media%2Ficons%2Favatar-circle.png%22%2C%22TeamMemberCoverPhotoPath%22%3A%22%2Fteamstats-media%2Fimages%2Fteam-member-cover.jpg%22%2C%22TeamMemberNickName%22%3A%22%22%2C%22TeamMemberPlayerPositionID%22%3A5%2C%22TeamMemberTeamRoleID%22%3A1%2C%22TeamMemberRoleText%22%3A%22%22%2C%22UploadQueueID%22%3Anull%2C%22TeamAdminUserID%22%3A719499%2C%22TeamAdminFullName%22%3A%22Uche%20Iwehee%22%2C%22TeamAdminFullNameShort%22%3A%22U.%20Iwehee%22%2C%22TeamAdminEmail%22%3A%22uiwehee%40gmail.com%22%2C%22TeamAdminMobile%22%3A%22%22%2C%22MobileSendMethodID%22%3A3%2C%22DefaultDOB%22%3A%2201%20Jan%202000%22%7D",
      "method": "POST"
    });
    

    From this, I was able to make players from the Node repl:

    Response {
      status: 200,
      statusText: 'OK',
      headers: Headers {
        'cache-control': 'private',
        'content-type': 'text/html; Charset=utf-8',
        server: 'TeamStats',
        'set-cookie': 'serverName=https%3A%2F%2Fwww%2Eteamstats%2Enet; path=/, ASPSESSIONIDCEQRABSS=EJMILLFBIFPLFLKFADLMLFAI; secure; path=/',
        'access-control-allow-headers': 'Origin, X-Requested-With, Content-Type, Accept',
        'access-control-allow-methods': 'OPTIONS, GET, POST, PUT, DELETE',
        'access-control-allow-credentials': 'true',
        'access-control-allow-origin': 'http://localhost:51926',
        'content-security-policy': "frame-ancestors 'self' https://www.teamstats.net https://api.teamstats.net https://staging.teamstats.net",
        date: 'Sun, 31 Aug 2025 03:22:53 GMT',
        'content-length': '59646'
      },
      body: ReadableStream { locked: false, state: 'readable', supportsBYOB: true },
      bodyUsed: false,
      ok: true,
      redirected: false,
      type: 'basic',
      url: 'https://api.teamstats.net/api/member/setMember.asp'
    }
    

    Next step is to URL encode the parameters and reverse engineer any other dynamic values to pull from the browser, although the latter may not be necessary depending on your use case (this may be a one-off operation).

    Here's a first step towards this:

    import json
    import requests
    
    url = "https://api.teamstats.net/api/member/setMember.asp"
    data = {
        "UserID": 0,
        "UserTitle": "",
        "UserFirstName": "aa new made 2",
        "UserSurname": "asdf",
        "UserUsername": "",
        "UserPassword": "",
        "UserLastLoggedIn": None,
        "UserEmail": "sds@asdg.com",
        "UserMobileNumber": "",
        "UserEmailSecondary": "",
        "UserMobileNumberSecondary": "",
        "UserDOB": "",
        "UserAddress": "",
        "UserPostCode": "",
        "UserNotes": "",
        "UserTwitterUsername": "",
        "TeamMemberID": 0,
        "TeamMemberIntroText": "",
        "TeamMemberPhotoMediaID": None,
        "TeamMemberCoverMediaID": None,
        "TeamMemberPhotoFileName": None,
        "TeamMemberCoverPhotoFileName": None,
        "TeamMemberPhotoPath": "/teamstats-media/icons/avatar-circle.png",
        "TeamMemberCoverPhotoPath": "/teamstats-media/images/team-member-cover.jpg",
        "TeamMemberNickName": "",
        "TeamMemberPlayerPositionID": 5,
        "TeamMemberTeamRoleID": 1,
        "TeamMemberRoleText": "",
        "UploadQueueID": None,
        "TeamAdminUserID": 719499,
        "TeamAdminFullName": "Uche Iwehee",
        "TeamAdminFullNameShort": "U. Iwehee",
        "TeamAdminEmail": "uiwehee@gmail.com",
        "TeamAdminMobile": "",
        "MobileSendMethodID": 3,
        "DefaultDOB": "01 Jan 2000"
    }
    params = {
        "sid": "D7546C14-0386-F011-BB93-14187749D29F",
        "path": "/enyimbafcaba/admin/members/edit/0/new/details",
        "mobile": "false",
        "lastAccessedDate": "20250830 20:34:59",
        "stateName": "admin.members.edit.details",
        "stateUniqueId": "0",
        "stateUrlTitle": "new",
        "adsDataLastUpdated": "31-Aug-2025 04:34:13",
        "tsVersionNum": "3.4.05",
        "tsPlatform": "web",
        "appDataLastUpdated": "29-Aug-2025 09:16:12",
        "teamDataLastUpdated": "31-Aug-2025 04:34:13",
        "teamId": "1679386",
        "clubId": "48521",
        "timeZoneOffset": "1",
        "teamName": "Enyimba FC Aba",
        "teamSiteUrl": "enyimbafcaba",
        "timeZoneCountryCode": "NG",
        "sportId": "1",
        "isClub": "false",
        "userUsername": "uiwehee",
        "userId": "719499",
        "userDataLastUpdated": "31-Aug-2025 01:40:22",
        "isTeamMember": "true",
        "isTeamAdmin": "true",
        "isLoggedIn": "true",
        "jsonData": json.dumps(data)
    }
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0",
        "Accept": "application/json, text/plain, */*",
        "Accept-Language": "en-US,en;q=0.5",
        "Sec-Fetch-Dest": "empty",
        "Sec-Fetch-Mode": "no-cors",
        "Sec-Fetch-Site": "same-site",
        "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
        "Priority": "u=0",
        "Pragma": "no-cache",
        "Cache-Control": "no-cache",
        "Referer": "https://www.teamstats.net/",
    }
    response = requests.post(url, data=params, headers=headers)
    print(response.status_code)
    print(response.text[:500])
    

    Here's a request to edit an existing member using the same endpoint:

    import json
    import requests
    
    url = "https://api.teamstats.net/api/member/setMember.asp"
    data = {
        "UserID": 724222,
        "UserTitle": None,
        "UserFirstName": "updated via request",
        "UserSurname": "hello",
        "UserUsername": "xa1679386",
        "UserUsernameOriginal": "xa1679386",
        "UserPassword": "",
        "UserLastLoggedIn": None,
        "UserEmail": "",
        "UserEmailOriginal": "",
        "UserEmailSecondary": "",
        "UserMobileNumber": "",
        "UserMobileOriginal": "",
        "UserMobileNumberSecondary": "",
        "UserDOB": None,
        "UserAddress": "",
        "UserPostCode": "",
        "UserNotes": "",
        "UserFacebookID": None,
        "UserFacebookEmail": "",
        "UserFacebookUsername": None,
        "UserFacebookLastLoggedIn": None,
        "UserFacebookAddedByAdmin": None,
        "UserTwitterID": None,
        "UserTwitterUsername": "",
        "UserTwitterEmail": "",
        "UserTwitterLastLoggedIn": None,
        "UserGoogleID": None,
        "UserGoogleUsername": None,
        "UserGoogleEmail": "",
        "UserGoogleLastLoggedIn": None,
        "UserMobileAppLastLoggedIn": None,
        "UserWhatsAppAccountID": None,
        "UserWhatsAppAccountSynced": False,
        "UserWhatsAppAccountLinkClicked": False,
        "TeamAdminUserID": 719499,
        "TeamAdminFullName": "Uche Iwehee",
        "TeamAdminFullNameShort": "U. Iwehee",
        "TeamAdminEmail": "uiwehee@gmail.com",
        "TeamAdminMobile": "",
        "MobileSendMethodID": 3,
        "TeamMemberID": 726569,
        "Age": None,
        "TeamMemberIntroText": "",
        "TeamMemberPhotoMediaID": None,
        "TeamMemberCoverMediaID": None,
        "TeamMemberPhotoPath": "/teamstats-media/icons/avatar-circle.png",
        "TeamMemberPhotoFileName": None,
        "TeamMemberCoverPhotoPath": "/teamstats-media/images/team-member-cover.jpg",
        "TeamMemberCoverPhotoFileName": None,
        "UploadQueueID": "",
        "TeamMemberNickName": "",
        "Position": "Not Specified",
        "TeamMemberPlayerPositionID": 5,
        "TeamMemberTeamRoleID": 1,
        "TeamRole": "Player Only",
        "TeamMemberRoleText": "Not Specified",
        "DefaultDOB": "01 Jan 2000"
    }
    params = {
        "sid": "D7546C14-0386-F011-BB93-14187749D29F",
        "path": "/enyimbafcaba/admin/members/edit/726569/x-a/details",
        "mobile": "false",
        "lastAccessedDate": "20250830 20:09:19",
        "stateName": "admin.members.edit.details",
        "stateUniqueId": "726569",
        "stateUrlTitle": "x-a",
        "adsDataLastUpdated": "31-Aug-2025 03:46:49",
        "tsVersionNum": "3.4.05",
        "tsPlatform": "web",
        "appDataLastUpdated": "29-Aug-2025 09:16:12",
        "teamDataLastUpdated": "31-Aug-2025 03:46:48",
        "teamId": "1679386",
        "clubId": "48521",
        "timeZoneOffset": "1",
        "teamName": "Enyimba FC Aba",
        "teamSiteUrl": "enyimbafcaba",
        "timeZoneCountryCode": "NG",
        "sportId": "1",
        "isClub": "false",
        "userUsername": "uiwehee",
        "userId": "719499",
        "userDataLastUpdated": "31-Aug-2025 01:40:22",
        "isTeamMember": "true",
        "isTeamAdmin": "true",
        "isLoggedIn": "true",
        "jsonData": json.dumps(data)
    }
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/201001 Firefox/142.0",
        "Accept": "application/json, text/plain, */*",
        "Accept-Language": "en-US,en;q=0.5",
        "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
        "Sec-Fetch-Dest": "empty",
        "Sec-Fetch-Mode": "cors",
        "Sec-Fetch-Site": "same-site",
        "Priority": "u=0",
        "Referer": "https://www.teamstats.net/",
    }
    response = requests.post(url, data=params, headers=headers)
    print(response.status_code)
    

    I should re-emphasize that some of these tokens may go stale, so you'll need to re-copy the fetch from your browser session, pull relevant tokens and/or update the cookie after it expires. All of this can be done programmatically, but I'll leave the rest as an exercise for the reader for now. Hopefully this communicates the main idea though.