How to Select Radio Buttons Reliably with Pyppeteer and Angular
Select Angular radio buttons with Pyppeteer, verify DOM and form state, handle navigation races, and troubleshoot flaky automation.
Direct answer: wait for the exact radio input to appear, require it to be visible, click it, then verify its native checked property. If the test depends on Angular state, also verify the associated FormControl, NgModel value, or rendered result. When the click causes navigation, start waitForNavigation() at the same time as click() so the navigation event cannot be missed.
Pyppeteer is an unofficial Python port of Puppeteer. Its 0.0.25 API provides Page.click(), waitForSelector(), and waitForFunction(); use the Python API reference for exact signatures rather than assuming every current JavaScript Puppeteer example transfers unchanged. See the Pyppeteer API reference.
1. A reliable baseline
Use stable attributes that identify both the radio group and the option. A selector such as input[type="radio"][name="delivery"][value="express"] is safer than a page-wide input[type="radio"].
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(headless=True)
page = await browser.newPage()
await page.goto("http://localhost:4200/checkout", {"waitUntil": "networkidle0"})
selector = 'input[type="radio"][name="delivery"][value="express"]'
# DOM presence and visibility are separate checks.
await page.waitForSelector(selector, {"visible": True})
await page.click(selector)
# Verify the native browser state after the click.
await page.waitForFunction(
"selector => document.querySelector(selector)?.checked === true",
{},
selector,
)
print("Express delivery is selected")
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
waitForSelector waits for an element to appear. Passing visible: True also requires it to be visible. waitForFunction evaluates a browser-side condition until it becomes truthy. Those waits express the state your test needs instead of relying on an arbitrary sleep.
2. Build a selector that survives markup changes
Prefer attributes that describe the form contract:
| Selector strategy | Example | Use when |
|---|---|---|
| Group plus value | input[name="delivery"][value="express"] |
The application has stable name and value attributes. |
| Unique test hook | input[data-testid="delivery-express"] |
You control the Angular template and can add a dedicated automation attribute. |
| Label relationship | label[for="delivery-express"] |
The visible label is stable and clicking it is the intended user interaction. |
| Scoped selector | #shipping-options input[type="radio"][value="express"] |
Several forms contain radio groups with similar values. |
Avoid positional selectors such as input[type="radio"]:nth-of-type(2). Adding an option can silently make the test select a different choice. Also avoid selecting by visible text alone when duplicate labels or hidden responsive layouts exist.
Check that the selector identifies one option
count = await page.JJeval(
'input[type="radio"][name="delivery"][value="express"]',
'nodes => nodes.length',
)
if count != 1:
raise AssertionError(f"Expected one express radio, found {count}")
If your installed Pyppeteer exposes Jeval rather than the typo-prone method above, use:
count = await page.JJeval(
selector,
"nodes => nodes.length",
)
In practice, the simplest portable check is to wait for the selector and then query it with page.querySelector; keep the exact method names aligned with your installed Pyppeteer version.
3. Verify Angular form state, not only the DOM
Angular’s RadioControlValueAccessor bridges radio inputs and Angular forms. It is used by FormControlDirective, FormControlName, and NgModel. Angular’s forms documentation describes this value accessor as the bridge between a FormControl and a built-in DOM element. See the RadioControlValueAccessor API and Angular forms guide.
A checked input proves that the browser control is selected. It does not prove that the Angular model has updated, validation has passed, or a submit handler has completed. Add an application-visible assertion for the state that matters.
Verify a rendered Angular result
Expose a stable status element in the template, for example <output data-testid="delivery-summary">{{ delivery.value }}</output>, then wait for its expected text:
await page.waitForFunction(
"expected => document.querySelector('[data-testid=delivery-summary]')?.textContent.trim() === expected",
{},
"Express delivery",
)
Verify an NgModel-driven value
NgModel binds a FormControl to the element and emits ngModelChange. Its updateOn setting can be change, blur, or submit; change is the default. If the application uses blur or submit, a click alone may not produce the final model value. See the NgModel API.
For black-box browser tests, assert an output that the user can observe: an error message disappears, a dependent panel appears, a summary changes, or the submit button becomes enabled. Avoid reaching into Angular’s private component internals because those are implementation details and can change between Angular versions.
4. When selecting the radio triggers navigation
Register the navigation wait concurrently with the click. Waiting for the click first can miss a fast navigation event.
import asyncio
selector = 'input[type="radio"][name="plan"][value="pro"]'
await page.waitForSelector(selector, {"visible": True})
await asyncio.gather(
page.waitForNavigation({"waitUntil": "networkidle0"}),
page.click(selector),
)
await page.waitForSelector('[data-testid="plan-page"]', {"visible": True})
This pattern is documented in the Pyppeteer API examples. If the action updates the current page through XHR rather than navigation, wait for the resulting element, URL fragment, or application status instead of waitForNavigation.
5. Clicking the label when the input is visually hidden
Custom Angular controls often hide the native input and style a label or wrapper. A hidden input may fail a visibility wait even though a user can click the visible label. In that case, select the associated label and still verify the input afterward.
input_selector = '#delivery-express'
label_selector = 'label[for="delivery-express"]'
await page.waitForSelector(label_selector, {"visible": True})
await page.click(label_selector)
await page.waitForFunction(
"selector => document.querySelector(selector)?.checked === true",
{},
input_selector,
)
Use this only when the label is correctly associated with the input. If the application uses a custom click handler on a wrapper, target the element that represents the actual user interaction and then verify the native and Angular outcomes.
6. Complete example with diagnostics
import asyncio
from pyppeteer import launch
async def select_radio(page, selector, expected_text=None):
await page.waitForSelector(selector, {"visible": True})
# Confirm that the option is not disabled before attempting the click.
disabled = await page.JJeval(
selector,
"node => node.disabled || node.getAttribute('aria-disabled') === 'true'",
)
if disabled:
raise AssertionError(f"Radio is disabled: {selector}")
await page.click(selector)
await page.waitForFunction(
"selector => document.querySelector(selector)?.checked === true",
{},
selector,
)
if expected_text is not None:
await page.waitForFunction(
"expected => document.querySelector('[data-testid=selection-status]')?.textContent.trim() === expected",
{},
expected_text,
)
async def main():
browser = await launch(headless=True, args=["--no-sandbox"])
page = await browser.newPage()
await page.setViewport({"width": 1280, "height": 900})
await page.goto("http://localhost:4200/checkout", {"waitUntil": "networkidle0"})
try:
await select_radio(
page,
'input[type="radio"][name="delivery"][value="express"]',
expected_text="Express delivery",
)
except Exception:
await page.screenshot({"path": "radio-failure.png", "fullPage": True})
raise
finally:
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
Replace the example status selector and expected text with elements from your application. Keep diagnostics such as a failure screenshot and the current URL; they make selector and timing failures much faster to investigate.
7. Common failures and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
waitForSelector times out |
The route, selector, or render timing is wrong. | Check page.url, capture the HTML or screenshot, and wait for the parent component or route-specific marker first. |
| Selector finds the wrong radio | Selector is too broad or duplicated. | Scope by name, stable value, test ID, or form container; assert the expected match count. |
| Element exists but click fails | It is hidden, covered, disabled, or outside the current viewport. | Use visible: True, click the associated label, scroll the element into view, and check disabled or overlay state. |
Click returns but checked is false |
A custom handler prevented the default action or another radio in the group was selected. | Click the user-facing label or control, inspect console errors, and verify the group’s markup and event handlers. |
checked is true but Angular value is stale |
Change detection or the form’s updateOn mode has not completed. |
Wait for a rendered application result; trigger the interaction required by the configured mode, such as blur or submit. |
| Test hangs after clicking | The click does not navigate, but the test waits for navigation. | Wait for the XHR result, URL change, or a stable DOM postcondition instead. |
| Navigation assertion is flaky | waitForNavigation started after the click. |
Use asyncio.gather(page.waitForNavigation(...), page.click(...)). |
| Works locally, fails in CI | Different browser, viewport, fonts, network speed, or headless behavior. | Pin the browser used by the project, set a deterministic viewport, avoid fixed sleeps, and collect a screenshot and console output on failure. |
| Angular page remains blank | JavaScript error, failed bundle, wrong base URL, or blocked resource. | Listen for console and page errors, verify the route directly, and wait for an application bootstrap marker before selecting the radio. |
8. Waiting strategies that remain stable
- Wait for presence: use when the element can be hidden and you will interact with another visible control.
- Wait for visibility: use for a real pointer click on the input or label.
- Wait for a postcondition: use
waitForFunctionforchecked, a status element, a URL, or a validation result. - Wait for network idle: useful during initial navigation, but do not assume it means Angular’s model has reached the state your test needs.
Fixed delays can mask race conditions and make every run slower. If a third-party widget has no observable completion signal, use the smallest bounded delay necessary and follow it with a meaningful assertion.
9. Performance and reliability considerations
- Reuse one browser process and create a new page per test or scenario when isolation allows it; launching Chromium for every radio selection adds avoidable startup cost.
- Keep selectors specific so Pyppeteer does not search a large, ambiguous DOM.
- Use one targeted postcondition instead of repeated polling from Python.
- Set explicit navigation and selector timeouts appropriate for your CI environment, and fail with diagnostics when they expire.
- Pin the Pyppeteer and browser versions used in CI. The cited API reference is for Pyppeteer 0.0.25, and the research does not establish a tested combination with a particular current Angular release.
- Use deterministic viewport and timezone settings when responsive layouts or date-dependent Angular logic can change which control is visible.
10. A practical checklist
- Identify the group with a stable
nameand option with a stablevalueor test ID. - Scope the selector to the correct form or component.
- Wait for the input or its visible label.
- Check that it is enabled.
- Click the control users actually interact with.
- Verify native
checked === true. - Verify Angular’s rendered result or form outcome when that is the behavior under test.
- Coordinate click and navigation waits with
asyncio.gather. - Capture a screenshot, URL, console errors, and HTML context on failure.
11. Or skip the browser setup
If your goal is a clean screenshot of the Angular page after it has rendered, ScreenshotNeo provides a single HTTP request instead of maintaining a Chromium script. The API can wait for a selector or delay, run custom JavaScript, click an element before capture, choose a device or viewport, and capture a full page or one CSS-selected element. Read the ScreenshotNeo API documentation for the complete option list.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Before capture, cookie and consent banners, newsletter popups, and chat widgets are removed. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers report the page verdict and whether the request was billed. ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
12. FAQ
Should I click the input or its label?
Click the input when it is visible and represents the user action. Click the associated label when the input is intentionally hidden by a custom control, then verify the input and application state.
Does a checked radio prove Angular updated?
No. It proves the native DOM control is selected. Assert a form value or rendered application result when Angular state is the outcome under test.
Why does a radio click not submit my form?
Radio selection normally changes the control value; submission may require a separate button or an application handler. Check the form’s updateOn mode and wait for the actual submit result.
Can I use a fixed sleep?
Only as a last resort for a widget with no observable signal. Prefer selector, visibility, navigation, or application-state conditions.
Which Pyppeteer version does this guide describe?
The cited API details come from the Pyppeteer 0.0.25 documentation. Confirm the installed package and browser versions before relying on version-specific behavior.


