From d434d0df76ae9f7cffef2ad541d28311a82ffb88 Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Fri, 5 Jun 2026 16:01:47 +0600 Subject: [PATCH 1/8] feat(checks): add Collapse All and Expand All controls to results page Add Collapse All and Expand All buttons above results. Hide buttons until scan completes via is-hidden class. Use custom card UI with rotating chevron, file icon, and brand color. All cards start collapsed by default. Fixes #1334 --- .gitignore | 1 + assets/css/plugin-check-admin.css | 68 +++++++++++++++++++ assets/js/plugin-check-admin.js | 104 ++++++++++++++++++++++++++++++ templates/admin-page.php | 10 +++ templates/results-table.php | 66 +++++++++++-------- 5 files changed, 221 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index bdaf98ed0..52194e67f 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ temp/ .wp-env.override.json .wp-env.tests.override.json +/build-temp diff --git a/assets/css/plugin-check-admin.css b/assets/css/plugin-check-admin.css index 115a348f7..19d207cf6 100644 --- a/assets/css/plugin-check-admin.css +++ b/assets/css/plugin-check-admin.css @@ -126,6 +126,74 @@ cursor: pointer; } +.plugin-check__collapse-expand-controls { + margin: 16px 0; + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.plugin-check__collapse-expand-controls.is-hidden { + display: none; +} + +.plugin-check__file-section { + margin: 1em 0; + border: 1px solid #dcdcde; + background: #fff; +} + +.plugin-check__file-section-header { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 8px 12px; + font-weight: 600; + cursor: pointer; + user-select: none; + background: transparent; + border: 0; + text-align: left; + color: inherit; + font-size: inherit; +} + +.plugin-check__file-section-header:focus { + outline: 2px solid #2271b1; + outline-offset: -2px; +} + +.plugin-check__file-section-chevron { + transition: transform 0.15s ease-in-out; + font-size: 18px; + line-height: 1; +} + +.plugin-check__file-section-title { + color: var(--wp-admin-theme-color); + display: inline-flex; + align-items: center; + gap: 6px; +} + +.plugin-check__file-section-icon { + font-size: 18px; + line-height: 1; +} + +.plugin-check__file-section-header[aria-expanded="true"] .plugin-check__file-section-chevron { + transform: rotate(180deg); +} + +.plugin-check__file-section-content { + padding: 0 14px 14px; +} + +.plugin-check__file-section--collapsed .plugin-check__file-section-content { + display: none; +} + .plugin-check__false-positive-results { padding: 0 14px 14px; } diff --git a/assets/js/plugin-check-admin.js b/assets/js/plugin-check-admin.js index 22227cc4e..5fe64124e 100644 --- a/assets/js/plugin-check-admin.js +++ b/assets/js/plugin-check-admin.js @@ -4,6 +4,9 @@ const exportContainer = document.getElementById( 'plugin-check__export-controls' ); + const collapseExpandContainer = document.getElementById( + 'plugin-check__collapse-expand-controls' + ); const spinner = document.getElementById( 'plugin-check__spinner' ); const pluginsList = document.getElementById( 'plugin-check__plugins-dropdown' @@ -20,6 +23,7 @@ ! pluginsList || ! resultsContainer || ! exportContainer || + ! collapseExpandContainer || ! spinner || ! categoriesList.length || ! typesList.length @@ -33,6 +37,7 @@ let checksCompleted = false; exportContainer.classList.add( 'is-hidden' ); exportContainer.addEventListener( 'click', onExportContainerClick ); + collapseExpandContainer.classList.add( 'is-hidden' ); const includeExperimental = document.getElementById( 'plugin-check__include-experimental' @@ -122,6 +127,7 @@ resultsContainer.innerText = ''; exportContainer.innerHTML = ''; exportContainer.classList.add( 'is-hidden' ); + collapseExpandContainer.classList.add( 'is-hidden' ); resetAggregatedResults(); checksCompleted = false; } @@ -424,10 +430,12 @@ exportContainer.innerHTML = ''; if ( ! checksCompleted ) { exportContainer.classList.add( 'is-hidden' ); + collapseExpandContainer.classList.add( 'is-hidden' ); return; } exportContainer.classList.remove( 'is-hidden' ); + collapseExpandContainer.classList.remove( 'is-hidden' ); const exportButtonConfigs = [ { @@ -1389,4 +1397,100 @@ const template = templates[ templateSlug ]; return template( data ); } + + /** + * Toggles the open/collapsed state of a single file section. + * + * @since 1.6.0 + * + * @param {HTMLElement} section The file section element. + */ + function toggleFileSection( section ) { + if ( ! section ) { + return; + } + const header = section.querySelector( + '.plugin-check__file-section-header' + ); + const isCollapsed = section.classList.contains( + 'plugin-check__file-section--collapsed' + ); + if ( isCollapsed ) { + section.classList.remove( 'plugin-check__file-section--collapsed' ); + if ( header ) { + header.setAttribute( 'aria-expanded', 'true' ); + } + } else { + section.classList.add( 'plugin-check__file-section--collapsed' ); + if ( header ) { + header.setAttribute( 'aria-expanded', 'false' ); + } + } + } + + /** + * Sets the open state of every file section in the results container. + * + * @since 1.6.0 + * + * @param {boolean} open Open state to apply. + */ + function setAllFileSectionsOpen( open ) { + const sections = resultsContainer.querySelectorAll( + '.plugin-check__file-section' + ); + sections.forEach( function ( section ) { + const header = section.querySelector( + '.plugin-check__file-section-header' + ); + if ( open ) { + section.classList.remove( + 'plugin-check__file-section--collapsed' + ); + if ( header ) { + header.setAttribute( 'aria-expanded', 'true' ); + } + } else { + section.classList.add( + 'plugin-check__file-section--collapsed' + ); + if ( header ) { + header.setAttribute( 'aria-expanded', 'false' ); + } + } + } ); + } + + // Delegated click handler for individual file section headers. + resultsContainer.addEventListener( 'click', function ( event ) { + const header = event.target.closest( + '.plugin-check__file-section-header' + ); + if ( ! header ) { + return; + } + event.preventDefault(); + const section = header.closest( '.plugin-check__file-section' ); + toggleFileSection( section ); + } ); + + // Wire up the Collapse All / Expand All buttons. + const expandAllButton = document.getElementById( + 'plugin-check__expand-all' + ); + const collapseAllButton = document.getElementById( + 'plugin-check__collapse-all' + ); + + if ( expandAllButton ) { + expandAllButton.addEventListener( 'click', function () { + setAllFileSectionsOpen( true ); + } ); + } + + if ( collapseAllButton ) { + collapseAllButton.addEventListener( 'click', function () { + setAllFileSectionsOpen( false ); + } ); + } } )( PLUGIN_CHECK ); diff --git a/templates/admin-page.php b/templates/admin-page.php index d8bfe935e..567ce1ead 100644 --- a/templates/admin-page.php +++ b/templates/admin-page.php @@ -104,6 +104,16 @@
+
diff --git a/templates/results-table.php b/templates/results-table.php index 2c10949b2..edaaf6365 100644 --- a/templates/results-table.php +++ b/templates/results-table.php @@ -1,29 +1,39 @@ -

{{ data.file }}

- - - - - - - - - <# if ( data.hasLinks ) { #> - - <# } #> - - - -
- - - - - - - - - - - -
+
+ +
+ + + + + + + + + <# if ( data.hasLinks ) { #> + + <# } #> + + + +
+ + + + + + + + + + + +
+
+

From 9ca7abf90c283fb8a71a26751e83cd59f4b06106 Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Mon, 8 Jun 2026 01:07:36 +0600 Subject: [PATCH 2/8] fix: address PR review feedback - Remove /build-temp from .gitignore - Remove trailing
after file section (double spacing with card margin) - Update @since tags to 2.1.0 for new toggleFileSection and setAllFileSectionsOpen Refs #1347 --- .gitignore | 1 - assets/js/plugin-check-admin.js | 4 ++-- templates/results-table.php | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 52194e67f..bdaf98ed0 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,3 @@ temp/ .wp-env.override.json .wp-env.tests.override.json -/build-temp diff --git a/assets/js/plugin-check-admin.js b/assets/js/plugin-check-admin.js index 5fe64124e..95b840213 100644 --- a/assets/js/plugin-check-admin.js +++ b/assets/js/plugin-check-admin.js @@ -1401,7 +1401,7 @@ /** * Toggles the open/collapsed state of a single file section. * - * @since 1.6.0 + * @since 2.1.0 * * @param {HTMLElement} section The file section element. */ @@ -1431,7 +1431,7 @@ /** * Sets the open state of every file section in the results container. * - * @since 1.6.0 + * @since 2.1.0 * * @param {boolean} open Open state to apply. */ diff --git a/templates/results-table.php b/templates/results-table.php index edaaf6365..208463e46 100644 --- a/templates/results-table.php +++ b/templates/results-table.php @@ -36,4 +36,4 @@ -
+ From 225b837e246eb89d2b61fe0615863327db41d16b Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Mon, 8 Jun 2026 01:23:55 +0600 Subject: [PATCH 3/8] ci: import Codecov GPG key before coverage upload The Codecov CLI v11.x GPG signature verification fails in GitHub Actions runners because the signing public key is missing from the default GPG keyring. Import it from keyserver.ubuntu.com (with a Keybase fallback) before the codecov-action runs. Add the import step to both behat-test.yml and php-test.yml since both use the same pinned codecov-action and hit the same issue. Refs #1347 --- .github/workflows/behat-test.yml | 6 ++++++ .github/workflows/php-test.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/behat-test.yml b/.github/workflows/behat-test.yml index 67110a79a..3a0bdc9a3 100644 --- a/.github/workflows/behat-test.yml +++ b/.github/workflows/behat-test.yml @@ -153,6 +153,12 @@ jobs: echo "Coverage files: $FILES" echo "files=$FILES" >> $GITHUB_OUTPUT + - name: Import Codecov GPG key + if: ${{ matrix.coverage }} + run: | + gpg --keyserver keyserver.ubuntu.com --recv-keys 27034E7FDB850E0BBC2C62FF806BB28AED779869 2>/dev/null || \ + curl -fsSL https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --import --no-default-keyring --keyring trustedkeys.gpg 2>/dev/null || true + - name: Upload code coverage report if: ${{ matrix.coverage }} uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 diff --git a/.github/workflows/php-test.yml b/.github/workflows/php-test.yml index 3ff6b5bca..f909e5ad3 100644 --- a/.github/workflows/php-test.yml +++ b/.github/workflows/php-test.yml @@ -108,6 +108,12 @@ jobs: npm run test-php-multisite fi + - name: Import Codecov GPG key + if: ${{ matrix.coverage }} + run: | + gpg --keyserver keyserver.ubuntu.com --recv-keys 27034E7FDB850E0BBC2C62FF806BB28AED779869 2>/dev/null || \ + curl -fsSL https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --import --no-default-keyring --keyring trustedkeys.gpg 2>/dev/null || true + - name: Upload code coverage report if: ${{ matrix.coverage }} uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 From b054ae28f7dd4c5dd91cdea635ae5a690b522dce Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Mon, 8 Jun 2026 01:34:14 +0600 Subject: [PATCH 4/8] ci: retry wp-env start on transient docker network failure Debian bullseye security repo (deb.debian.org) occasionally times out during Docker build for PHP 7.4 + WP 6.3 matrix job. Add retry loop around wp-env start step (3 attempts, 30s delay). Docker build cache preserves already-built layers between retries. Refs #1347 --- .github/workflows/php-test.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/php-test.yml b/.github/workflows/php-test.yml index f909e5ad3..6275fc430 100644 --- a/.github/workflows/php-test.yml +++ b/.github/workflows/php-test.yml @@ -92,11 +92,20 @@ jobs: - name: Start WordPress run: | - if [[ ${{ matrix.coverage == true }} == true ]]; then - npm run wp-env:start:tests -- --xdebug=coverage - else - npm run wp-env:start:tests - fi + set +e + for i in 1 2 3; do + if [[ ${{ matrix.coverage == true }} == true ]]; then + npm run wp-env:start:tests -- --xdebug=coverage + else + npm run wp-env:start:tests + fi + if [ $? -eq 0 ]; then + exit 0 + fi + echo "wp-env start failed (attempt $i), retrying in 30s..." + sleep 30 + done + exit 1 - name: Run tests run: | From 5a503ec46f48804a2d4c5e6db9b2df0260c1b741 Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Mon, 8 Jun 2026 01:39:26 +0600 Subject: [PATCH 5/8] ci: increase wp-env retry to 5 attempts with 60s backoff Previous 3-attempt 30s backoff insufficient for transient Docker Hub registry timeouts. Bump to 5 attempts with 60s backoff to give the network time to recover. Refs #1347 --- .github/workflows/php-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/php-test.yml b/.github/workflows/php-test.yml index 6275fc430..f74e5d77b 100644 --- a/.github/workflows/php-test.yml +++ b/.github/workflows/php-test.yml @@ -93,7 +93,7 @@ jobs: - name: Start WordPress run: | set +e - for i in 1 2 3; do + for i in 1 2 3 4 5; do if [[ ${{ matrix.coverage == true }} == true ]]; then npm run wp-env:start:tests -- --xdebug=coverage else @@ -102,8 +102,8 @@ jobs: if [ $? -eq 0 ]; then exit 0 fi - echo "wp-env start failed (attempt $i), retrying in 30s..." - sleep 30 + echo "wp-env start failed (attempt $i), retrying in 60s..." + sleep 60 done exit 1 From fd8da0463902549733756d1280e1b61f6979911e Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Sun, 26 Jul 2026 07:19:21 +0600 Subject: [PATCH 6/8] feat(checks): show error and warning counts per file Adds per-file error and warning count badges to the file section card header so reviewers see totals at a glance. * Reuses countResultTree() to compute severity counts. * Reuses existing pluginCheck L10n strings for singular/plural labels. * Adds pill styles using the admin color palette. * Updates both render call sites, main results and the false positives panel, to keep counts consistent. Addresses PR feedback. Refs #1347 --- assets/css/plugin-check-admin.css | 28 +++++++++++++++++ assets/js/plugin-check-admin.js | 50 ++++++++++++++++++++++++++++++- templates/results-table.php | 5 +++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/assets/css/plugin-check-admin.css b/assets/css/plugin-check-admin.css index 19d207cf6..46fbb166d 100644 --- a/assets/css/plugin-check-admin.css +++ b/assets/css/plugin-check-admin.css @@ -182,6 +182,34 @@ line-height: 1; } +.plugin-check__file-section-counts { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: 6px; +} + +.plugin-check__file-section-count { + display: inline-block; + padding: 1px 8px; + border-radius: 10px; + font-size: 12px; + font-weight: 500; + line-height: 1.4; +} + +.plugin-check__file-section-count--error { + background: #fcf0f1; + color: #8b1a1a; + border: 1px solid #d63638; +} + +.plugin-check__file-section-count--warning { + background: #fcf9e8; + color: #6b5b00; + border: 1px solid #dba617; +} + .plugin-check__file-section-header[aria-expanded="true"] .plugin-check__file-section-chevron { transform: rotate(180deg); } diff --git a/assets/js/plugin-check-admin.js b/assets/js/plugin-check-admin.js index 95b840213..8c6569660 100644 --- a/assets/js/plugin-check-admin.js +++ b/assets/js/plugin-check-admin.js @@ -1091,10 +1091,36 @@ const hasLinks = hasLinksInResults( errors ) || hasLinksInResults( warnings ); + const errorCount = countResultTree( { [ file ]: errors } ); + const warningCount = countResultTree( { [ file ]: warnings } ); + + const errorLabel = + errorCount > 0 + ? ( errorCount === 1 + ? pluginCheck.errorString + : pluginCheck.errorsString + ).replace( '%d', errorCount ) + : ''; + const warningLabel = + warningCount > 0 + ? ( warningCount === 1 + ? pluginCheck.warningString + : pluginCheck.warningsString + ).replace( '%d', warningCount ) + : ''; + // Render the file table. resultsContainer.innerHTML += renderTemplate( 'plugin-check-results-table', - { file, index, hasLinks } + { + file, + index, + hasLinks, + errorCount, + warningCount, + errorLabel, + warningLabel, + } ); const resultsTable = document.getElementById( 'plugin-check__results-body-' + index @@ -1232,10 +1258,32 @@ const hasLinks = hasLinksInResults( errors ) || hasLinksInResults( warnings ); + const errorCount = countResultTree( { [ file ]: errors } ); + const warningCount = countResultTree( { [ file ]: warnings } ); + + const errorLabel = + errorCount > 0 + ? ( errorCount === 1 + ? pluginCheck.errorString + : pluginCheck.errorsString + ).replace( '%d', errorCount ) + : ''; + const warningLabel = + warningCount > 0 + ? ( warningCount === 1 + ? pluginCheck.warningString + : pluginCheck.warningsString + ).replace( '%d', warningCount ) + : ''; + container.innerHTML += renderTemplate( 'plugin-check-results-table', { file, index, hasLinks, + errorCount, + warningCount, + errorLabel, + warningLabel, } ); const resultsTable = document.getElementById( diff --git a/templates/results-table.php b/templates/results-table.php index 208463e46..5938d209f 100644 --- a/templates/results-table.php +++ b/templates/results-table.php @@ -2,7 +2,10 @@ From 86b0c8748ea0b018699563d2306ae4c9eb674413 Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Sun, 26 Jul 2026 17:15:15 +0600 Subject: [PATCH 7/8] fix(admin): retarget spacing selectors to new file card markup Replace dead #plugin-check__results h4:first-child and .notice + h4 selectors with .plugin-check__file-section equivalents so the initial lift and post-notice spacing carry over after the per-file

was replaced by the card button in templates/results-table.php. Refs #1347 --- includes/Admin/Admin_Page.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/Admin/Admin_Page.php b/includes/Admin/Admin_Page.php index 5dd4881e8..9f84a3612 100644 --- a/includes/Admin/Admin_Page.php +++ b/includes/Admin/Admin_Page.php @@ -414,14 +414,14 @@ public function admin_footer() { ?>