3

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()

7
  • Can you share a minimal reproducible example, something I can run to see the problem? If the thing you're automating is behind a login, and the problem is just inputting into angular forms generically, I suggest whipping up a simple angular app with a reproducible form you can't fill into so we can run it. I know there's this for React inputs, probably something similar for Angular. Commented Aug 30 at 18:19
  • Possibly the Angular version of this is Fill angular input using javascript but I'm skeptical that the answer there works until I can run it and see. Commented Aug 30 at 18:28
  • It's behind a login, the login fields works but it's the main player fields that is not working. Commented Aug 30 at 18:33
  • I have added the whole code so that you can run it and see the problem, just create a txt file named players.txt and put some dummy emails there, the code will pick the names to form first name and last name Commented Aug 30 at 18:38
  • Cool, very helpful. I think the first run failed on email_input, but subsequent cookie runs hit waiting for get_by_role("button", name="ADD NEW"). Commented Aug 30 at 18:48

1 Answer 1

1

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 = (
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
)

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": "[email protected]",
    "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": "[email protected]",
    "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": "[email protected]",
    "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.

Sign up to request clarification or add additional context in comments.

5 Comments

I have tried and still doesn't work, how did you upload the players, did you do it manually? Also can I use API to send the players?
I programmatically created users via their API running the request I posted here. You won't be able to run the same request, likely, so try following the steps I've shown to generate your own request, then reverse engineer any dynamic tokens. What do you mean by "send the players"? But yes, most operations on a website are done via an API call like this.
I've added browser automation code as well, although I do think reverse engineering the API is worth the time. It doesn't seem too complex, and the UI is not trivial to automate.
I’m a newbie at automation so all these are new to me, lemme try and see what I can do then I will revert back to you, I really appreciate your help.
I ran the code and it worked, thanks a lot, I appreciate your help, I will dedicate my time to learn this automation stuff very well, thanks once again!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.