Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a specific selector, wait for the radio to be visible, click it, and verify the resulting state. In an Angular form, also verify the relevant FormControl, NgModel, or rendered result. A successful page.click() only proves that Pyppeteer performed a mouse action; it does not prove that Angular accepted the value or that a submission finished.

The reliable sequence

  1. Identify the exact radio with stable attributes such as name and value.
  2. Wait for that element to be present and visible.
  3. Click it with Pyppeteer.
  4. Wait for a meaningful postcondition, normally the native checked property.
  5. If Angular state is the real test target, verify the form value or a UI result that depends on it.
  6. If the click causes navigation, wait for navigation concurrently with the click.

This order avoids the most common failures: clicking a hidden duplicate, selecting the wrong radio group, racing Angular change detection, and missing a navigation event.

Before you write the test

Confirm the installed APIs

The Python examples here use the Pyppeteer API documented for version 0.0.25. That documentation is old, and Pyppeteer is an unofficial Python port; JavaScript Puppeteer examples are not guaranteed to have identical Python signatures. Check the version installed in your project and use its API reference when adapting these snippets.

Inspect the actual Angular markup

Do not guess the selector from a component label. In browser developer tools, inspect the rendered <input type="radio">. Confirm its name, value, whether it is disabled, and whether a custom component hides the native input. A typical group looks like this:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<input type="radio" name="delivery" value="standard">
<input type="radio" name="delivery" value="express">

The name groups mutually exclusive choices. The value distinguishes the option. Prefer attributes that are stable across styling changes instead of generated classes or DOM positions.

A minimal Pyppeteer implementation

This complete example opens a page, waits for the express option to be visible, clicks it, and verifies the browser’s native state.

import asyncio
from pyppeteer import launch

async def select_delivery():
    browser = await launch()
    page = await browser.newPage()
    await page.goto("https://example.com/checkout")

    selector = 'input[type="radio"][name="delivery"][value="express"]'

    await page.waitForSelector(selector, {"visible": True})
    await page.click(selector)
    await page.waitForFunction(
        "selector => document.querySelector(selector)?.checked === true",
        {},
        selector,
    )

    await browser.close()

asyncio.get_event_loop().run_until_complete(select_delivery())

waitForSelector waits for DOM presence by default. Passing visible=True adds a visibility requirement. waitForFunction evaluates a browser-side function until it returns a truthy value, so the assertion is tied to an observable state rather than an arbitrary sleep.

Make the selector narrower when necessary

If the page contains several groups, scope the selector to a form or section:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
selector = '#shipping-options input[type="radio"][name="delivery"][value="express"]'

When IDs are unstable, use a stable ancestor attribute supplied by the application. A broad selector such as input[type="radio"] can match the first option in an unrelated group and still produce a technically successful click.

Verify Angular’s form state, not only checked

Angular’s RadioControlValueAccessor is the bridge that writes radio values and listens for radio changes. It is used by FormControlDirective, FormControlName, and NgModel. Therefore, a checked DOM input is important evidence, but it is not proof that the application’s intended model or submit path has completed.

Verify a rendered application result

The most portable end-to-end assertion is an element that Angular renders from the selected value, such as a summary or validation message. For example, after clicking, wait for a summary containing the expected option:

await page.waitForFunction(
    "expected => document.querySelector('#delivery-summary')?.textContent.includes(expected)",
    {},
    "Express",
)

Use the application’s real result selector and text. Avoid asserting an implementation detail that users never see.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Verify a form control when your test exposes one

With reactive forms, assert the value through the application’s observable output or a test-only diagnostic surface rather than reaching into Angular internals from the browser. With template-driven forms, NgModel binds a FormControl to the element and emits ngModelChange. The default update mode is change; an application can instead configure updateOn as blur or submit.

That setting changes what “complete” means. For updateOn: 'blur', a click may update the native control while the model waits for a blur. For updateOn: 'submit', the model may not be committed until submission. In those cases, perform the user action that the application requires and assert the resulting form behavior, rather than adding a fixed delay.

When selecting the radio navigates

A change handler may submit a form or route to another page. Register the navigation wait at the same time as the click. If you await the click first, the navigation can begin and finish before the wait is registered.

import asyncio

selector = 'input[type="radio"][name="delivery"][value="express"]'
await page.waitForSelector(selector, {"visible": True})
await asyncio.gather(
    page.waitForNavigation(),
    page.click(selector),
)

After navigation, assert the destination or a page-specific element. If the action updates the current page without navigation, use a postcondition such as checked, a changed summary, or a form error disappearing.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choosing a dependable interaction strategy

Direct input selector

Use name plus value when the native input is present and those attributes are stable. This is usually the clearest and least ambiguous approach.

Label or accessible name

A visible label can be preferable when the input is visually hidden and the application exposes a proper label association. Verify that the label points to the intended input and that duplicate labels do not exist. If the markup does not provide a reliable association, improve the markup or use a stable input selector rather than guessing by text.

Presence versus visibility

Presence only means an element exists in the DOM. It may still be covered, hidden, or a template duplicate. Request visibility when a real user must see and click it. If a component intentionally reveals the option after another action, wait for that reveal condition first, then wait for the radio itself.

Condition versus fixed sleep

Fixed sleeps make tests slower when the page is ready quickly and flaky when it is not ready yet. Prefer waitForFunction for a state transition you can name: checked becomes true, a summary appears, or a submit result is rendered. Angular performs asynchronous model/view synchronization and an additional change-detection pass in the NgModel flow, so an explicit condition is more meaningful than a guessed number of milliseconds.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Reusable helper with diagnostics

A small helper can standardize waiting, clicking, and verification while retaining useful error context:

async def choose_radio(page, selector, expected_text_selector=None, expected_text=None):
    await page.waitForSelector(selector, {"visible": True})
    await page.click(selector)
    await page.waitForFunction(
        "selector => document.querySelector(selector)?.checked === true",
        {},
        selector,
    )

    if expected_text_selector and expected_text is not None:
        await page.waitForFunction(
            "args => document.querySelector(args[0])?.textContent.includes(args[1])",
            {},
            [expected_text_selector, expected_text],
        )

Keep the selector and expected result in the test name or failure message. When a test fails, you should immediately know which group, option, and application outcome were involved.

Troubleshooting common failures

“No node found for selector”

Cause: The component has not rendered, the selector does not match the current markup, or the radio is inside a different document context.

Fix: Inspect the live DOM, wait for the component’s stable container, and then wait for the exact input. Recheck spelling, quoting, name, and value.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The click succeeds but the wrong option is selected

Cause: The selector matched another radio group or a duplicate template node.

Fix: Scope it to the relevant form or container and include both name and value. Verify the exact selector matched the intended element before clicking.

The element exists but the click has no effect

Cause: The element is hidden, disabled, covered by an overlay, or the visible control is a label/custom component.

Fix: Wait with visible=True, inspect disabled state and overlays, and confirm the label-to-input association. If the product intentionally uses a custom control, test the user-facing control while still verifying the resulting native or Angular state.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

checked is true but Angular still has the old value

Cause: The test asserted too early, or the form uses updateOn: 'blur' or submit.

Fix: Wait for the application’s rendered result, perform the required blur or submit action, and assert the form outcome. Do not treat a timeout extension as a substitute for identifying the configured update mode.

The test hangs around navigation

Cause: The navigation wait was started after the click, or the click does not actually navigate.

Fix: Use asyncio.gather(page.waitForNavigation(), page.click(selector)) for navigation-causing actions. For same-page updates, remove the navigation wait and wait for the relevant DOM or model postcondition.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Examples copied from JavaScript fail in Python

Cause: Pyppeteer is an unofficial Python port and its signatures and release history differ from current JavaScript Puppeteer.

Fix: Check the Pyppeteer version installed in the project and consult its Python API reference. Confirm the browser executable and Angular application versions used by your test environment.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Reliability, speed, and maintenance

  • Use stable contracts: Treat name, value, accessible labels, and test-specific attributes as part of the UI contract. Avoid positional selectors such as :nth-child(2).
  • Wait at the narrowest useful scope: Wait for the component or radio you need, not an unrelated page-wide delay.
  • Assert user-visible consequences: Native state catches click failures; an Angular summary, validation message, or submit result catches model and change-detection failures.
  • Keep diagnostics: On failure, capture the current URL, selector, and relevant text or HTML so markup changes are easy to diagnose.
  • Separate navigation and same-page flows: The correct wait depends on the application behavior; adding both indiscriminately can create unnecessary timeouts.

No performance winner is established between selector styles or wait strategies. Choose the one that expresses the application’s actual contract and remains stable as templates change.

Or skip the browser setup

If your goal is a clean screenshot rather than an interaction test, ScreenshotNeo returns a screenshot or PDF from one request. It accepts the cookie or consent banner like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the ScreenshotNeo API documentation for authentication and options. A minimal cURL request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request in Python:

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)

And Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account to try it.

FAQ

Should I click the label or the input?

Click the native input when it is visible and reliably identified. Click the associated label when the input is intentionally hidden and the label is the user-facing control, then verify the same checked and Angular outcomes.

Can I prove the form submitted by checking only checked?

No. checked proves the browser control state. Submission requires an application-level assertion such as a destination page, success message, or stored form result.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What if two options legitimately share a value?

Values should normally distinguish options within one radio group. If an application does not, scope the selector to the correct group container or use a stable label association so the test identifies one concrete control.

Frequently Asked Questions

Does Pyppeteer automatically wait for Angular change detection?

It waits only for the conditions you request. Add an explicit postcondition for the rendered result or form behavior that demonstrates Angular has processed the change.

How should I test a radio group that appears after an API response?

Wait for the group or its specific input to appear and become visible, then click and verify the resulting state; avoid a fixed delay based on an assumed response time.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.