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..f74e5d77b 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 4 5; 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 60s..." + sleep 60 + done + exit 1 - name: Run tests run: | @@ -108,6 +117,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 diff --git a/assets/css/plugin-check-admin.css b/assets/css/plugin-check-admin.css index 115a348f7..46fbb166d 100644 --- a/assets/css/plugin-check-admin.css +++ b/assets/css/plugin-check-admin.css @@ -126,6 +126,102 @@ 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-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); +} + +.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..8c6569660 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 = [ { @@ -1083,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 @@ -1224,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( @@ -1389,4 +1445,100 @@ const template = templates[ templateSlug ]; return template( data ); } + + /** + * Toggles the open/collapsed state of a single file section. + * + * @since 2.1.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 2.1.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/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() { ?>