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..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..95b840213 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 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/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..208463e46 100644
--- a/templates/results-table.php
+++ b/templates/results-table.php
@@ -1,29 +1,39 @@
- {{ data.file }}
-
-
-
- |
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
- <# if ( data.hasLinks ) { #>
-
-
- |
- <# } #>
-
-
-
-
-
+
+
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ <# if ( data.hasLinks ) { #>
+
+
+ |
+ <# } #>
+
+
+
+
+
+
+