How to Automate Logins and Forms

Updated June 2026
Automating logins and form submissions is one of the most common browser automation tasks. This guide covers the complete process: identifying form elements, filling text inputs and passwords, handling dropdowns and checkboxes, submitting forms, saving authentication state for reuse, and dealing with multi-factor authentication and CAPTCHAs.

Login and form automation forms the foundation of nearly every browser automation workflow. Whether you are building automated tests that verify login functionality, scraping data from authenticated pages, or automating business processes that require filling out web forms, the techniques in this guide apply across all major frameworks. The examples here use Playwright conventions, but the concepts transfer directly to Selenium, Puppeteer, and other tools.

Step 1: Identify Form Elements and Selectors

Before writing any automation code, you need to identify the HTML elements that make up the form. Open the target page in Chrome, right-click on each form field, and select "Inspect" to view the element in the DevTools Elements panel. Note the element's tag name, ID, name attribute, class, and any data attributes that could serve as reliable selectors.

For a typical login form, you are looking for the username or email input field, the password input field, and the submit button. Good selectors include element IDs (which should be unique on the page), name attributes (commonly used in form inputs), and data-testid attributes (added by developers specifically for automation). Avoid using class names that appear to be dynamically generated, such as strings of random characters, because these often change between deployments.

Check for hidden fields that the form might include. Many web frameworks add CSRF (Cross-Site Request Forgery) tokens as hidden inputs. Your browser automation framework handles these automatically because it fills and submits the form through the actual browser, which includes the hidden fields in the submission just as it would for a human user. You do not need to extract or manage CSRF tokens manually.

Also note the form's action URL and method. While browser automation submits the form through the browser interface rather than constructing HTTP requests directly, knowing whether the form uses POST or GET and where it submits to helps you understand the expected behavior and verify that the submission reaches the right endpoint.

Step 2: Fill Text Inputs and Password Fields

Once you have identified the form elements, use your framework's fill method to enter text into each field. In Playwright, use page.fill("#email", "user@example.com") to clear any existing content in the email field and type the new value. For password fields, the same method works: page.fill("#password", "your-secure-password").

The fill method is preferred over type for form fields because fill clears the field first, ensuring you start with a clean slate. The type method appends characters to whatever is already in the field, which can cause problems if the field has a default value or placeholder that has not been cleared. Use type when you need to simulate realistic keystroke-by-keystroke input, such as when a form's JavaScript watches for individual keystrokes to provide autocomplete suggestions or real-time validation.

For forms with multiple text inputs beyond username and password, fill them in sequence from top to bottom. Some forms validate fields when you move focus to the next field (on blur validation), so filling in the natural tab order ensures validation runs correctly. If a form has date inputs, use the fill method with the date in the format the input expects, or use the input's date picker if it requires clicking through a calendar widget.

Store credentials securely rather than hardcoding them in your scripts. Use environment variables, a .env file that is excluded from version control, or a secrets manager. Never commit passwords, API keys, or authentication tokens to a code repository. Your automation script should read credentials from a secure source at runtime.

Step 3: Handle Dropdowns, Checkboxes, and Radio Buttons

Web forms use several input types beyond text fields. Each requires a different interaction method in browser automation.

Dropdown menus built with the HTML select element are handled with the select_option method. In Playwright, use page.select_option("#country", "US") to select by value, page.select_option("#country", label="United States") to select by visible text, or page.select_option("#country", index=5) to select by position. Custom dropdown components built with div and span elements instead of the native select element require clicking to open the dropdown, waiting for the options to appear, and then clicking the desired option.

Checkboxes can be toggled with the check and uncheck methods. Use page.check("#agree-terms") to check a box and page.uncheck("#newsletter") to uncheck one. These methods are idempotent, meaning check will do nothing if the box is already checked, and uncheck will do nothing if it is already unchecked. This is safer than using click, which toggles the state regardless of the current value.

Radio buttons are selected by clicking the specific option you want. Use page.click("input[name='plan'][value='premium']") to select the premium plan radio button. Since only one radio button in a group can be selected at a time, clicking one automatically deselects the others.

File uploads use the set_input_files method. Use page.set_input_files("#file-upload", "/path/to/document.pdf") to attach a file to an upload input. For multiple file uploads, pass an array of file paths. This method works with both visible and hidden file input elements, bypassing the system file dialog that would normally require manual interaction.

Step 4: Submit the Form and Verify Success

After filling all form fields, submit the form by clicking the submit button. Use page.click("button[type='submit']") or page.click("#login-button") depending on how the button is implemented. Some forms submit when you press Enter in a text field. You can simulate this with page.press("#password", "Enter").

After submission, wait for a clear indicator that the form was processed successfully. For login forms, this typically means waiting for navigation to a dashboard or home page. Use page.wait_for_url("**/dashboard") to wait until the URL changes to the expected destination. Alternatively, wait for a specific element that only appears on the post-login page: page.wait_for_selector(".user-menu").

Handle submission failures gracefully. Check for error messages that might appear on the form. You can test for the presence of an error element: if page.is_visible(".login-error") returns true, the login failed and you should read the error message with page.text_content(".login-error") to understand why. Common failure reasons include incorrect credentials, expired sessions, rate limiting, and account lockouts.

For multi-step forms that span several pages, repeat the fill-and-submit pattern for each step. Wait for each subsequent page to load fully before filling its fields. Some multi-step forms use JavaScript to show and hide sections within a single page rather than navigating to separate URLs. In that case, wait for the new section's fields to become visible before filling them.

Step 5: Save and Reuse Authentication State

Logging in on every automation run wastes time and creates unnecessary load on the target server. Browser automation frameworks can save the authenticated browser state, including cookies and local storage, to a file and restore it on subsequent runs.

In Playwright, save the authentication state after a successful login with context.storage_state(path="auth_state.json"). This writes all cookies and local storage entries to a JSON file. On the next run, create a new browser context with the saved state: browser.new_context(storage_state="auth_state.json"). The browser loads with the saved cookies and local storage, effectively skipping the login process.

Session tokens eventually expire, so your script should handle expired sessions gracefully. Check whether the saved state is still valid by navigating to a page that requires authentication and verifying that the expected content appears. If the session has expired, fall back to the full login flow, save the new state, and continue.

A common pattern is to implement a login function that first attempts to use saved state. If the state file exists and the session is valid, it proceeds without logging in. If the state file does not exist or the session has expired, it performs the full login, saves the new state, and then proceeds. This approach minimizes login frequency while handling expiration automatically.

Keep authentication state files secure. They contain session tokens that grant access to the authenticated account. Store them outside your code repository, exclude them from version control with .gitignore entries, and treat them with the same security standards you apply to passwords and API keys.

Step 6: Handle Multi-Factor Authentication and CAPTCHAs

Multi-factor authentication and CAPTCHAs are designed to verify that a human is performing the action, which makes them inherently challenging for automation. There are practical approaches for each.

For MFA with TOTP codes (time-based one-time passwords from apps like Google Authenticator), you can generate the codes programmatically. Libraries like pyotp in Python accept the TOTP secret key and generate valid codes. Store the TOTP secret securely and use it to generate a fresh code during the login flow: fill the MFA input field with the generated code and submit. This fully automates MFA login without manual intervention.

For SMS or email-based MFA, full automation is more difficult because you need access to the SMS or email inbox programmatically. Some approaches use email APIs (like Gmail's API) to read the verification code from the inbox. Others use a manual-on-first-run approach: pause the script during the first login to let a human enter the MFA code, then save the authenticated session state so subsequent runs skip the MFA step entirely. The session persistence technique from the previous step makes this practical.

For CAPTCHAs, the options are limited by design. Third-party CAPTCHA solving services like 2Captcha and Anti-Captcha use human workers or AI models to solve CAPTCHAs for a per-solve fee. Your script sends the CAPTCHA image or site key to the service, waits for a solution, and enters it into the form. This works but adds cost and latency to the automation. For your own applications, the better approach is to disable CAPTCHAs in test environments or whitelist your automation's IP addresses.

When automating against your own applications for testing purposes, consider adding test-mode flags that bypass MFA and CAPTCHAs. This is standard practice in QA environments and eliminates the need for workarounds that add complexity and fragility to test scripts.

Key Takeaway

Form automation follows a consistent pattern: inspect the form to identify selectors, fill fields using the framework's dedicated methods for each input type, submit and verify success, then save authentication state to avoid repeated logins. Handle MFA with TOTP libraries when possible, and use session persistence to minimize the impact of authentication steps on automation performance.