{"id":4448,"date":"2026-08-20T03:39:17","date_gmt":"2026-08-20T03:39:17","guid":{"rendered":"https:\/\/tucumandevelopers.com\/index.php\/2026\/08\/20\/the-excluded-plugin-setting-that-playwright-ignored-fixing-browser-mode-updates-and-false-residual-warnings\/"},"modified":"2026-08-20T03:39:17","modified_gmt":"2026-08-20T03:39:17","slug":"the-excluded-plugin-setting-that-playwright-ignored-fixing-browser-mode-updates-and-false-residual-warnings","status":"publish","type":"post","link":"https:\/\/tucumandevelopers.com\/index.php\/2026\/08\/20\/the-excluded-plugin-setting-that-playwright-ignored-fixing-browser-mode-updates-and-false-residual-warnings\/","title":{"rendered":"The excluded-plugin setting that Playwright ignored \u2014 fixing browser-mode updates and false residual warnings"},"content":{"rendered":"<div>\n<div><\/div>\n<div data-article-id=\"4439660\" id=\"article-body\">\n<h2> <a name=\"the-symptom\" href=\"#the-symptom\"> <\/a> The symptom <\/h2>\n<p>In browser-mode maintenance (Playwright, no SSH), plugins marked as &#8220;excluded from update checks&#8221; were still being updated. After the run, a &#8220;plugin updates remaining&#8221; WARNING email arrived every time.<\/p>\n<p>The excluded plugins were intentionally left behind, but the residual check treated them as unfinished updates and fired a warning \u2014 a two-part problem: wrong behavior and a misleading alert.<\/p>\n<h2> <a name=\"ssh-path-vs-playwright-path\" href=\"#ssh-path-vs-playwright-path\"> <\/a> SSH path vs. Playwright path <\/h2>\n<p>On SSH-capable sites, WP-CLI&#8217;s <code>--skip-plugins<\/code> flag carries the <code>ignored_plugins<\/code> list into the update command. That path already excluded them correctly.<\/p>\n<p>The Playwright path was different. <code>browser_update_remaining_plugins()<\/code> worked by clicking the &#8220;select all&#8221; checkbox on <code>update-core.php<\/code> and submitting the form \u2014 no filtering at all. <\/p>\n<div>\n<pre><code><span># Before: select-all, ignored_plugins never consulted <\/span><span>page<\/span><span>.<\/span><span>check<\/span><span>(<\/span><span>'<\/span><span>input[name=<\/span><span>\"<\/span><span>action<\/span><span>\"<\/span><span>][value=<\/span><span>\"<\/span><span>update-selected-plugins<\/span><span>\"<\/span><span>]<\/span><span>'<\/span><span>)<\/span> <span>for<\/span> <span>cb<\/span> <span>in<\/span> <span>page<\/span><span>.<\/span><span>query_selector_all<\/span><span>(<\/span><span>'<\/span><span>input[name=<\/span><span>\"<\/span><span>checked[]<\/span><span>\"<\/span><span>]<\/span><span>'<\/span><span>):<\/span> <span>cb<\/span><span>.<\/span><span>check<\/span><span>()<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>This function is the chokepoint for two flows: pure browser-mode updates (<code>run_browser_update_flow<\/code>) and the browser residual pass that runs after SSH updates (<code>run_browser_residual_update<\/code>, on by default). So even SSH sites could have excluded plugins updated by the residual pass. Browser-driven bulk updates are also outside the scope of pinpoint rollback, meaning there is no automatic recovery if a wrong update goes through.<\/p>\n<h2> <a name=\"fix-1-raw-ignoredpluginslugs-endraw-and-raw-pluginslugfromcheckboxvalue-endraw-\" href=\"#fix-1-raw-ignoredpluginslugs-endraw-and-raw-pluginslugfromcheckboxvalue-endraw-\"> <\/a> Fix 1 \u2014 <code>_ignored_plugin_slugs()<\/code> and <code>_plugin_slug_from_checkbox_value()<\/code> <\/h2>\n<p>When excluded plugins are configured, the fix replaces the select-all approach with per-checkbox evaluation. <\/p>\n<div>\n<pre><code><span>def<\/span> <span>_ignored_plugin_slugs<\/span><span>(<\/span><span>site<\/span><span>:<\/span> <span>dict<\/span><span>)<\/span> <span>-&gt;<\/span> <span>set<\/span><span>[<\/span><span>str<\/span><span>]:<\/span> <span>raw<\/span> <span>=<\/span> <span>site<\/span><span>.<\/span><span>get<\/span><span>(<\/span><span>\"<\/span><span>ignored_plugins<\/span><span>\"<\/span><span>,<\/span> <span>\"\"<\/span><span>)<\/span> <span>return<\/span> <span>{<\/span><span>s<\/span><span>.<\/span><span>strip<\/span><span>().<\/span><span>lower<\/span><span>()<\/span> <span>for<\/span> <span>s<\/span> <span>in<\/span> <span>raw<\/span><span>.<\/span><span>split<\/span><span>(<\/span><span>\"<\/span><span>,<\/span><span>\"<\/span><span>)<\/span> <span>if<\/span> <span>s<\/span><span>.<\/span><span>strip<\/span><span>()}<\/span> <span>def<\/span> <span>_plugin_slug_from_checkbox_value<\/span><span>(<\/span><span>value<\/span><span>:<\/span> <span>str<\/span><span>)<\/span> <span>-&gt;<\/span> <span>str<\/span><span>:<\/span> <span>return<\/span> <span>value<\/span><span>.<\/span><span>split<\/span><span>(<\/span><span>\"<\/span><span>\/<\/span><span>\"<\/span><span>)[<\/span><span>0<\/span><span>].<\/span><span>strip<\/span><span>().<\/span><span>lower<\/span><span>()<\/span> <span>if<\/span> <span>\"<\/span><span>\/<\/span><span>\"<\/span> <span>in<\/span> <span>value<\/span> <span>else<\/span> <span>value<\/span><span>.<\/span><span>strip<\/span><span>().<\/span><span>lower<\/span><span>()<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Plugin checkboxes on <code>update-core.php<\/code> use a <code>value<\/code> like <code>contact-form-7\/wp-contact-form-7.php<\/code>. The slug is the part before the slash. Normalizing to lowercase covers casing differences in user input.<\/p>\n<p>With excluded plugins configured: <\/p>\n<div>\n<pre><code><span>ignored<\/span> <span>=<\/span> <span>_ignored_plugin_slugs<\/span><span>(<\/span><span>site<\/span><span>)<\/span> <span>if<\/span> <span>ignored<\/span><span>:<\/span> <span>for<\/span> <span>cb<\/span> <span>in<\/span> <span>page<\/span><span>.<\/span><span>query_selector_all<\/span><span>(<\/span><span>'<\/span><span>input[name=<\/span><span>\"<\/span><span>checked[]<\/span><span>\"<\/span><span>]<\/span><span>'<\/span><span>):<\/span> <span>val<\/span> <span>=<\/span> <span>cb<\/span><span>.<\/span><span>get_attribute<\/span><span>(<\/span><span>\"<\/span><span>value<\/span><span>\"<\/span><span>)<\/span> <span>or<\/span> <span>\"\"<\/span> <span>slug<\/span> <span>=<\/span> <span>_plugin_slug_from_checkbox_value<\/span><span>(<\/span><span>val<\/span><span>)<\/span> <span>if<\/span> <span>slug<\/span> <span>and<\/span> <span>slug<\/span> <span>not<\/span> <span>in<\/span> <span>ignored<\/span><span>:<\/span> <span>cb<\/span><span>.<\/span><span>check<\/span><span>()<\/span> <span># if value is missing, keep it checked (safe side: update rather than skip) <\/span><\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Sites with no excluded plugins keep the original behavior (select-all with individual fallback) exactly as before.<\/p>\n<h2> <a name=\"fix-2-raw-browserpluginresidualstate-endraw-stops-false-residual-warnings\" href=\"#fix-2-raw-browserpluginresidualstate-endraw-stops-false-residual-warnings\"> <\/a> Fix 2 \u2014 <code>_browser_plugin_residual_state()<\/code> stops false residual warnings <\/h2>\n<p>The residual check had the same gap. Before the fix, any remaining update form on <code>update-core.php<\/code> counted as &#8220;updates pending.&#8221; An excluded plugin left intentionally on the page was enough to trigger a WARNING. <\/p>\n<div>\n<pre><code><span>def<\/span> <span>_browser_plugin_residual_state<\/span><span>(<\/span><span>page<\/span><span>,<\/span> <span>site<\/span><span>:<\/span> <span>dict<\/span><span>)<\/span> <span>-&gt;<\/span> <span>bool<\/span><span>:<\/span> <span>ignored<\/span> <span>=<\/span> <span>_ignored_plugin_slugs<\/span><span>(<\/span><span>site<\/span><span>)<\/span> <span>checkboxes<\/span> <span>=<\/span> <span>page<\/span><span>.<\/span><span>query_selector_all<\/span><span>(<\/span><span>'<\/span><span>input[name=<\/span><span>\"<\/span><span>checked[]<\/span><span>\"<\/span><span>]<\/span><span>'<\/span><span>)<\/span> <span>if<\/span> <span>not<\/span> <span>checkboxes<\/span><span>:<\/span> <span>return<\/span> <span>False<\/span> <span># no form = nothing pending <\/span> <span>if<\/span> <span>not<\/span> <span>ignored<\/span><span>:<\/span> <span>return<\/span> <span>True<\/span> <span># no exclusions = form present = pending (original behavior) <\/span> <span>for<\/span> <span>cb<\/span> <span>in<\/span> <span>checkboxes<\/span><span>:<\/span> <span>val<\/span> <span>=<\/span> <span>cb<\/span><span>.<\/span><span>get_attribute<\/span><span>(<\/span><span>\"<\/span><span>value<\/span><span>\"<\/span><span>)<\/span> <span>or<\/span> <span>\"\"<\/span> <span>slug<\/span> <span>=<\/span> <span>_plugin_slug_from_checkbox_value<\/span><span>(<\/span><span>val<\/span><span>)<\/span> <span>if<\/span> <span>slug<\/span> <span>not<\/span> <span>in<\/span> <span>ignored<\/span><span>:<\/span> <span>return<\/span> <span>True<\/span> <span># at least one non-excluded plugin still pending <\/span> <span>return<\/span> <span>False<\/span> <span># only excluded plugins remain \u2014 not pending <\/span><\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>After this change, a site where only excluded plugins remain on <code>update-core.php<\/code> resolves to <code>False<\/code> (not pending), and no WARNING email is sent.<\/p>\n<h2> <a name=\"test-coverage\" href=\"#test-coverage\"> <\/a> Test coverage <\/h2>\n<p><code>tests\/test_browser_ignored_plugins.py<\/code> \u2014 21 new tests:<\/p>\n<ul>\n<li> <strong>Slug normalization (7)<\/strong>: <code>slug\/file.php<\/code> format, single-file format, case and whitespace variation, missing value<\/li>\n<li> <strong>Exclusion behavior (9)<\/strong>: single exclusion, multiple exclusions, all excluded (zero updates), no exclusions (all updated), nonexistent slug in exclusion list does not over-exclude<\/li>\n<li> <strong>Residual check (5)<\/strong>: only excluded remain \u2192 not pending, excluded + non-excluded remain \u2192 pending, no exclusions (original behavior), no form \u2192 not pending<\/li>\n<\/ul>\n<hr>\n<p><em>Related: <a href=\"https:\/\/en.wpmm.jp\/blog\/select-all-checkbox-verification-fallback\/\" target=\"_blank\" rel=\"noopener noreferrer\">The select-all checkbox that Playwright checked but the page ignored<\/a> \/ <a href=\"https:\/\/en.wpmm.jp\/blog\/playwright-friendly-error-translation\/\" target=\"_blank\" rel=\"noopener noreferrer\">Translating Playwright&#8217;s raw error messages into readable text<\/a><\/em><\/p>\n<\/p><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p>Fuente: <a href=\"https:\/\/dev.to\/susumun\/the-excluded-plugin-setting-that-playwright-ignored-fixing-browser-mode-updates-and-false-3ceo\">Art\u00edculo original<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The symptom In browser-mode maintenance (Playwright, no SSH), plugins marked as &#8220;excluded from update checks&#8221; were still being updated. After the run, a &#8220;plugin updates remaining&#8221; WARNING email arrived every time. The excluded plugins were intentionally left behind, but the residual check treated them as unfinished updates and fired a warning \u2014 a two-part problem: [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":4447,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"webixso_pending_account_ids":""},"categories":[41],"tags":[],"class_list":["post-4448","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devto"],"jetpack_publicize_connections":[],"_links":{"self":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/4448","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/comments?post=4448"}],"version-history":[{"count":0,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/4448\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media\/4447"}],"wp:attachment":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media?parent=4448"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/categories?post=4448"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/tags?post=4448"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}