WordPress’s plugin and theme update screens both have a “select all” checkbox. Calling check() on it with Playwright succeeds — no error, no exception. But look at the individual checkboxes afterward, and sometimes none of them are actually checked.
Note: Playwright’s
check()ticks a checkbox. The click itself can succeed even if the page’s JavaScript handler never fires, leaving what the form actually submits out of sync with what the screen visually shows.
What actually happens
The “select all” checkbox is usually wired up with a JavaScript handler: clicking it is supposed to check every individual checkbox underneath it. Playwright’s check(force=True) can force the DOM state of that one checkbox — but that only changes that checkbox’s own state. It doesn’t guarantee the JavaScript handler that’s supposed to propagate the change to the individual checkboxes actually fires.
# Looks like it worked, but the individual checkboxes are still unchecked
select_all.first.check(force=True)
page.click('input[type="submit"][name="upgrade"]')
Clicking the update button submits whatever the form’s actual state is — which is “nothing checked.” Nothing updates. No error is thrown, so on the surface it looks like the run completed normally.
The fix — verify right after checking, every time
Right after checking “select all,” confirm that the individual checkboxes underneath are actually checked. If they aren’t, fall back to checking each one individually.
sel_all_sel = 'input[type="checkbox"][id^="plugins-select-all"]'
select_all = plugin_form.locator(sel_all_sel)
if select_all.count() > 0:
select_all.first.check(force=True)
page.wait_for_timeout(500)
# Verification step — confirm checkboxes are actually checked
chk_sel_check = 'input[type="checkbox"][name="checked[]"]:checked'
any_checked = plugin_form.locator(chk_sel_check).count() > 0
if not any_checked:
# Select-all had no effect; switch to individual selection
select_all = None
if not select_all or select_all.count() == 0:
checkboxes = plugin_form.locator('input[type="checkbox"][name="checked[]"]')
count = checkboxes.count()
for i in range(count):
checkboxes.nth(i).check(force=True)
The key point: the code checks the actual fact of how many checkboxes ended up checked, instead of assuming that “select all” worked. Right before clicking the update button, it also logs the checked count against the total, as a final guard against silently proceeding with zero selections.
checked_count = plugin_form.locator(chk_sel).count()
total_checkboxes = plugin_form.locator(all_chk_sel).count()
if checked_count == 0:
# Nothing to update — stop here
return result
Apply the same pattern to both plugins and themes
The plugin update form and theme update form are different screens, but they share the same structure: a select-all checkbox sitting above individual checkboxes. Adding this verification-and-fallback to one screen but leaving the other with a bare check() just means the theme update path will hit the same failure later. Once a pattern like this turns up in one place, it has to be carried over to every other screen with the same structure.
Summary
| Aspect | Detail |
|---|---|
| Symptom | “Select all” appears checked, but individual checkboxes underneath stay unchecked |
| Cause | check(force=True) changes the checkbox’s own state but doesn’t guarantee the linked JavaScript handler fires |
| Fix | Verify the individual checkboxes right after checking “select all”; fall back to individual checks if nothing propagated |
| Carry-over | Apply the same verify-and-fallback pattern to every screen sharing the same structure (plugins, themes) |
Like the version-scraping false positive in update-core.php, this comes down to the gap between “the operation appeared to succeed” and “the intended state actually happened.” A click that didn’t throw an exception doesn’t mean it accomplished what it was supposed to. The question of what to trust when something does fail carries over into not showing users a raw Playwright error.