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

  1. 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.
  2. Ensure no overlay intercepts the click (remove force, let auto-wait clear overlays).
  3. If a JS handler must run, click the element that the app's own toggle binds to (often a label).
  4. 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

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


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/98ad4be35c426931. Report an issue: GitHub.