microsoft/playwright · error · NonRecoverableDOMError
Clicking the checkbox did not change its state
Error message
Clicking the checkbox did not change its state
What it means
After _setChecked performs a click to toggle a checkbox, it re-reads the state; if the state did not flip to the requested value it throws NonRecoverableDOMError('Clicking the checkbox did not change its state'). This indicates the click landed but did not toggle — typically because a custom widget, an intercepting label, or a JS event handler prevented the native change.
Source
Thrown at packages/playwright-core/src/server/dom.ts:844
const result = await progress.race(this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {}));
if (result === 'error:notconnected' || result.received === 'error:notconnected')
throwElementIsNotAttached();
return { matches: result.matches, isRadio: result.isRadio };
};
await this._markAsTargetElement(progress);
const checkedState = await isChecked(progress);
if (checkedState.matches === state)
return 'done';
if (!state && checkedState.isRadio)
throw new NonRecoverableDOMError('Cannot uncheck radio button. Radio buttons can only be unchecked by selecting another radio button in the same group.');
const result = await this._click(progress, { ...options, waitAfter: 'disabled' });
if (result !== 'done')
return result;
if (options.trial)
return 'done';
const finalState = await isChecked(progress);
if (finalState.matches !== state)
throw new NonRecoverableDOMError('Clicking the checkbox did not change its state');
return 'done';
}
async boundingBox(progress: Progress): Promise<types.Rect | null> {
return await progress.race(this._page.delegate.getBoundingBox(this));
}
async screenshot(progress: Progress, options: ScreenshotOptions): Promise<Buffer> {
return await this._page.screenshotter.screenshotElement(progress, this, options);
}
async querySelector(progress: Progress, selector: string, options: types.StrictOptions): Promise<ElementHandle | null> {
return progress.race(this._querySelector(selector, options));
}
private async _querySelector(selector: string, options: types.StrictOptions): Promise<ElementHandle | null> {
return this._frame.selectors.query(selector, options, this);
}View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Verify the target is a real <input type=checkbox>; if it is a custom widget, click the underlying input or use a role-based locator that resolves to it.
- Ensure no overlay intercepts the click (remove force, let auto-wait clear overlays).
- If a JS handler must run, click the element that the app's own toggle binds to (often a label).
- As a last resort, set the state via page.evaluate and dispatch the change event the app expects.
Example fix
// before
await page.locator('.fancy-checkbox').check(); // click doesn't flip → throws
// after — target the real input
await page.locator('input[type=checkbox]').check(); Defensive patterns
Strategy: validation
Validate before calling
// Target the real native checkbox the app toggles.
const isNative = await page.locator('input[type=checkbox]').count() > 0;
if (isNative) await page.locator('input[type=checkbox]').check(); Type guard
// Resolve to a native checkbox rather than a styled wrapper.
async function isNativeCheckbox(locator: Locator): Promise<boolean> {
return await locator.evaluate(el => el.tagName === 'INPUT' && (el as HTMLInputElement).type === 'checkbox');
} Prevention
- Locate the underlying <input type=checkbox>, not its visual wrapper div.
- Remove force so auto-wait clears any intercepting overlay before the toggle click.
- If the widget toggles via JS, click the element the app binds its handler to.
When it happens
Trigger: Calling check()/uncheck() on a custom checkbox widget that toggles via JS rather than the native click→checked flip; a label/div overlapping the input so the click does not reach the real checkbox; the element looks like a checkbox but is a styled div.
Common situations: Component libraries (e.g. Material/Chakra) that render a hidden native input plus a visible div and toggle checked in a click handler that the synthetic click sequence does not trigger identically; double-click handlers that flip state twice back to the original.
Related errors
- Cannot uncheck radio button. Radio buttons can only be unche
- Debugger is already paused
- Debugger is not paused
- Element is not ${result.missingState}
- state: expected one of (attached|detached|visible|hidden)
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/98ad4be35c426931.
Report an issue: GitHub.