diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..2b603f4 --- /dev/null +++ b/.distignore @@ -0,0 +1,30 @@ +# Runtime package is built by bin/build.sh; this file documents the intended +# WordPress.org exclusions for any tooling that packages from the repo root. +/.claude +/.distignore +/.git +/.github +/.gitignore +/.planning +/.wordpress-org/source +/.wp-env.json +/build +/docs +/node_modules +/test-results +/tests +/vendor + +*.cache +.DS_Store +composer.json +composer.lock +package.json +package-lock.json +phpcs.xml.dist +phpstan.neon.dist +phpunit-*.xml.dist +playwright.config.ts +SPEC.md +TESTING.md +bin diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..61a1047 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default owner for all changes. +* @dknauss diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml new file mode 100644 index 0000000..a827107 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-bug-report.yml @@ -0,0 +1,60 @@ +name: Bug report +description: Report a reproducible Maestro bug +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug. Do not include security vulnerabilities here; use SECURITY.md instead. + - type: textarea + id: summary + attributes: + label: Summary + description: What went wrong? + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. Activate Maestro. + 2. Click Edit Menu. + 3. ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected result + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual result + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: WordPress version, PHP version, browser, active plugins/themes if relevant. + placeholder: | + WordPress: + PHP: + Browser: + Maestro: + Other admin-menu/capability plugins: + validations: + required: true + - type: checkboxes + id: checks + attributes: + label: Pre-submit checks + options: + - label: I confirmed this is not a security report. + required: true + - label: I tested with Maestro's latest release or current main branch. + required: false diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml new file mode 100644 index 0000000..d6002ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-feature-request.yml @@ -0,0 +1,39 @@ +name: Feature request +description: Suggest an improvement for Maestro +title: "[Feature]: " +labels: ["enhancement", "triage"] +body: + - type: textarea + id: problem + attributes: + label: Problem or use case + description: What are you trying to do, and why is the current behavior insufficient? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: Describe the simplest behavior that would solve the problem. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Existing plugins, workarounds, or simpler options. + - type: dropdown + id: area + attributes: + label: Area + options: + - Admin menu editing + - Icons + - Role visibility + - Accessibility + - Localization + - Testing / QA + - Documentation + - Release / WordPress.org + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e08ce5b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security reports + url: https://github.com/dknauss/Maestro/security/advisories/new + about: Please report vulnerabilities privately, not in public issues. + - name: WordPress.org support + url: https://wordpress.org/support/plugin/maestro-menu-editor/ + about: Get support for the released plugin. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..9a120ce --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ +## Summary + +- + +## Type of change + +- [ ] Bug fix +- [ ] Feature +- [ ] Documentation +- [ ] Tooling / CI +- [ ] Release / packaging + +## Verification + +- [ ] `composer test:unit` +- [ ] `composer lint` +- [ ] `composer analyse:phpstan` +- [ ] `npm run test:js` +- [ ] `npm run check:doc-links` +- [ ] `npm run test:php` +- [ ] `npm run test:e2e` +- [ ] `bash bin/build.sh` + +## Notes + +- Related issue/phase: +- Screenshots/video for UI changes: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c91d452 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,37 @@ +version: 2 +updates: + - package-ecosystem: composer + directory: "/" + schedule: + interval: weekly + day: monday + time: "10:00" + timezone: America/Edmonton + open-pull-requests-limit: 5 + labels: + - dependencies + - php + + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + day: monday + time: "10:30" + timezone: America/Edmonton + open-pull-requests-limit: 5 + labels: + - dependencies + - javascript + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: monday + time: "11:00" + timezone: America/Edmonton + open-pull-requests-limit: 5 + labels: + - dependencies + - github-actions diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a0c0d7..3322f6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,58 @@ concurrency: cancel-in-progress: true jobs: + dependency-health: + name: Dependency metadata + audits + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + + - name: Validate Composer metadata + run: composer validate --strict + + - name: Install Composer dependencies + run: composer install --no-progress --prefer-dist --no-interaction + + - name: Composer security audit + run: composer audit + + - name: Install npm dependencies + run: npm ci + + - name: npm security audit with repo allowlist + run: npm run audit:npm + + php-lint: + name: PHP syntax (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2', '8.3'] + steps: + - uses: actions/checkout@v4 + + - name: Set up PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Lint PHP files + run: find maestro-menu-editor.php uninstall.php includes tests -name '*.php' -print0 | xargs -0 -n1 php -l + lint: name: Coding standards (WPCS) runs-on: ubuntu-latest @@ -34,7 +86,26 @@ jobs: run: composer install --no-progress --prefer-dist --no-interaction - name: Run phpcs (WordPress standard + PHP compatibility) - run: vendor/bin/phpcs + run: composer lint + + phpstan: + name: Static analysis (PHPStan) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer + coverage: none + + - name: Install Composer dependencies + run: composer install --no-progress --prefer-dist --no-interaction + + - name: Run PHPStan + run: composer analyse:phpstan unit: name: Unit tests (PHP ${{ matrix.php }}) @@ -58,6 +129,27 @@ jobs: - name: Run pure unit tests run: composer test:unit + node-quality: + name: JavaScript unit tests + docs links + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + + - name: Install npm dependencies + run: npm ci + + - name: JavaScript unit tests + run: npm run test:js + + - name: Documentation link hygiene + run: npm run check:doc-links + integration-e2e: name: Integration + E2E (wp-env, WP 7.0) runs-on: ubuntu-latest @@ -99,3 +191,41 @@ jobs: - name: Stop wp-env if: always() run: npm run env:stop + + package-check: + name: Runtime package + Plugin Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + + - name: Install npm dependencies + run: npm ci + + - name: Build runtime ZIP + run: bash bin/build.sh + + - name: Upload runtime ZIP artifact + uses: actions/upload-artifact@v4 + with: + name: maestro-menu-editor + path: build/maestro-menu-editor.zip + if-no-files-found: error + + - name: Start wp-env (Docker) + run: npm run env:start + + - name: Install Plugin Check + run: npx wp-env run cli wp plugin install plugin-check --activate + + - name: Run Plugin Check on runtime tree + run: npx wp-env run cli wp plugin check /var/www/html/wp-content/plugins/maestro-menu-editor/build/maestro-menu-editor --format=json + + - name: Stop wp-env + if: always() + run: npm run env:stop diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c050b7..a1e6097 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,10 @@ on: permissions: contents: write - actions: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false jobs: release: @@ -16,7 +19,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v4 + + - name: Verify tag matches plugin versions + run: | + VERSION="${GITHUB_REF_NAME#v}" + HEADER_VERSION="$(grep -m1 -E '^ \* Version:' maestro-menu-editor.php | sed -E 's/.*Version:[[:space:]]*//')" + STABLE_TAG="$(grep -m1 -E '^Stable tag:' readme.txt | sed -E 's/^Stable tag:[[:space:]]*//')" + test "$VERSION" = "$HEADER_VERSION" + test "$VERSION" = "$STABLE_TAG" - name: Build release zip run: bash bin/build.sh @@ -27,9 +38,3 @@ jobs: files: build/maestro-menu-editor.zip generate_release_notes: true fail_on_unmatched_files: true - - # Hand off to the SVN deploy workflow for the same tag. - - name: Dispatch WordPress.org deploy - env: - GH_TOKEN: ${{ github.token }} - run: gh workflow run wp-deploy.yml --ref "$GITHUB_REF_NAME" diff --git a/.github/workflows/wp-deploy.yml b/.github/workflows/wp-deploy.yml index d9b9bbf..7d3d645 100644 --- a/.github/workflows/wp-deploy.yml +++ b/.github/workflows/wp-deploy.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: tag: - description: "Git tag to deploy (e.g. v1.0.0). Leave blank when triggered by a release." + description: "Git tag to deploy (e.g. v1.1.1). Leave blank when triggered by a release." required: false default: "" release: @@ -13,6 +13,10 @@ on: permissions: contents: read +concurrency: + group: wporg-deploy-${{ github.event.inputs.tag || github.ref_name }} + cancel-in-progress: false + jobs: deploy: name: Deploy to WordPress.org SVN @@ -32,10 +36,18 @@ jobs: echo "version=${REF#v}" >> "$GITHUB_OUTPUT" - name: Checkout ${{ steps.meta.outputs.ref }} - uses: actions/checkout@v5 + uses: actions/checkout@v4 with: ref: ${{ steps.meta.outputs.ref }} + - name: Verify deploy version + run: | + VERSION="${{ steps.meta.outputs.version }}" + HEADER_VERSION="$(grep -m1 -E '^ \* Version:' maestro-menu-editor.php | sed -E 's/.*Version:[[:space:]]*//')" + STABLE_TAG="$(grep -m1 -E '^Stable tag:' readme.txt | sed -E 's/^Stable tag:[[:space:]]*//')" + test "$VERSION" = "$HEADER_VERSION" + test "$VERSION" = "$STABLE_TAG" + # No build toolchain: the runtime is plain PHP/JS/CSS. bin/build.sh # assembles the exact shippable tree under build/maestro-menu-editor. - name: Build runtime tree diff --git a/.planning/phases/07-visual-polish-icons/screenshots/icons-bootstrap-tab.png b/.planning/phases/07-visual-polish-icons/screenshots/icons-bootstrap-tab.png index d283ecd..e98ec5c 100644 Binary files a/.planning/phases/07-visual-polish-icons/screenshots/icons-bootstrap-tab.png and b/.planning/phases/07-visual-polish-icons/screenshots/icons-bootstrap-tab.png differ diff --git a/.planning/phases/07-visual-polish-icons/screenshots/icons-dashicons-tab.png b/.planning/phases/07-visual-polish-icons/screenshots/icons-dashicons-tab.png index c1ab47c..ed447e1 100644 Binary files a/.planning/phases/07-visual-polish-icons/screenshots/icons-dashicons-tab.png and b/.planning/phases/07-visual-polish-icons/screenshots/icons-dashicons-tab.png differ diff --git a/.planning/phases/07-visual-polish-icons/screenshots/icons-side-by-side.png b/.planning/phases/07-visual-polish-icons/screenshots/icons-side-by-side.png index 88d7e60..714af71 100644 Binary files a/.planning/phases/07-visual-polish-icons/screenshots/icons-side-by-side.png and b/.planning/phases/07-visual-polish-icons/screenshots/icons-side-by-side.png differ diff --git a/.planning/phases/07-visual-polish-icons/screenshots/toolbar-1200.png b/.planning/phases/07-visual-polish-icons/screenshots/toolbar-1200.png index ee35c04..40d30d0 100644 Binary files a/.planning/phases/07-visual-polish-icons/screenshots/toolbar-1200.png and b/.planning/phases/07-visual-polish-icons/screenshots/toolbar-1200.png differ diff --git a/.planning/phases/07-visual-polish-icons/screenshots/toolbar-700.png b/.planning/phases/07-visual-polish-icons/screenshots/toolbar-700.png index 78eadc4..101f9f8 100644 Binary files a/.planning/phases/07-visual-polish-icons/screenshots/toolbar-700.png and b/.planning/phases/07-visual-polish-icons/screenshots/toolbar-700.png differ diff --git a/.planning/reviews/code-review-followup-2026-06-20.md b/.planning/reviews/code-review-followup-2026-06-20.md new file mode 100644 index 0000000..8987007 --- /dev/null +++ b/.planning/reviews/code-review-followup-2026-06-20.md @@ -0,0 +1,86 @@ +# Code review follow-up handoff — 2026-06-20 + +## Context + +A comprehensive code/process review identified seven priority follow-ups. This handoff documents the work done in this pass and the residual items Claude should assess for the current milestone plan. + +## Completed in this pass + +1. **Autosave/reset/exit race hardening** + - Reused the existing single-flight save chain in `assets/maestro.js`. + - Added `waitForSaveIdle()` so Exit waits for queued or in-flight saves, not just debounce timers. + - Added `cancelQueuedAutosave()` and made Reset All wait for any in-flight save before issuing DELETE. + - Reset All now checks `response.ok`, disables/re-enables its button, and only reloads after a successful DELETE. + +2. **Uninstall cleanup** + - Added `uninstall.php` guarded by `WP_UNINSTALL_PLUGIN`. + - Deletes only `maestro_config` on uninstall; deactivation remains non-destructive. + - Updated `bin/build.sh` so uninstall cleanup ships in the runtime ZIP. + +3. **Dependency/tooling drift** + - Regenerated local Composer lock state and added PHPStan dependencies/config. + - Bumped private package metadata to `1.1.1` and updated `@wordpress/env` to `^11.8.1`. + - Added `bin/audit-npm.mjs` plus `npm run audit:npm` to document/allowlist the current dev-only `@wordpress/env` → `js-yaml` advisory. + +4. **Playwright setup robustness** + - Changed E2E global setup to use `waitUntil: 'domcontentloaded'` and explicitly wait for `#user_login`. + - Login submit now races click and URL wait through `Promise.all()`. + +5. **CI/release QA gates** + - Expanded `.github/workflows/ci.yml` with dependency validation/audits, PHP syntax matrix, PHPCS, PHPStan, JS unit/docs checks, integration/E2E, runtime ZIP artifact, and Plugin Check against the built runtime tree. + - Added `.distignore` as a second packaging-policy guard. + - Simplified release deployment flow to avoid duplicate WordPress.org deploys: tag push creates the GitHub release; the existing release-published event drives `wp-deploy.yml`. + - Added release/deploy version checks and deploy concurrency. + +6. **Repository health files** + - Added repo-local `CONTRIBUTING.md`, `SECURITY.md`, `SUPPORT.md`, `CODE_OF_CONDUCT.md`, `.github/PULL_REQUEST_TEMPLATE.md`, issue forms, `dependabot.yml`, and `CODEOWNERS`. + +7. **Docs/version drift** + - Updated README status from v1.1.0 to v1.1.1. + - Corrected build ZIP name to `build/maestro-menu-editor.zip`. + - Documented `uninstall.php` as a runtime file. + - Updated TESTING guidance to include PHPStan, npm audit wrapper, Plugin Check, and current E2E scope without brittle exact E2E counts. + +## Recommended backlog / planning candidates + +### Runtime behavior + +- **Add E2E coverage for save races**: slow REST response + Exit; pending rename + Reset All; in-flight save + Reset All. The implementation was hardened, but automated regression coverage should be explicit. +- **Bound config payload size**: add server-side limits for title length, number of items/order entries, role list length, and data-URI length. Current sanitization is strong but does not cap payload size. +- **Scope `custom_menu_order` enablement**: consider enabling `custom_menu_order` only when a stored top-level order exists, to avoid activating unrelated menu-order filters unnecessarily. +- **Duplicate submenu slug strategy**: document or improve handling for duplicate submenu slugs under one parent. Current slug-only identity is consistent with v1 but can collapse ambiguous duplicates. + +### QA / tooling + +- **Add JS/CSS linting**: CI now runs JS unit tests but not ESLint/Stylelint. Decide whether to add `@wordpress/scripts` or standalone ESLint/Stylelint rules for no-build runtime JS/CSS. +- **Add axe accessibility checks**: Playwright has strong UI coverage; automated axe checks would complement keyboard and sizing assertions. +- **Add multisite test path**: current integration tests are single-site in wp-env. Decide whether v1.2 requires multisite assertions or documentation that multisite is not yet a supported configuration. +- **Plugin Check output policy**: CI runs Plugin Check against the runtime tree. Consider parsing JSON and storing the report as an artifact for release audits. + +### Release / repository operations + +- **Update GitHub About homepage**: repository metadata currently points to a stale `dknauss/admin-menu-maestro` Playground URL; update it to the active `dknauss/Maestro` URL. +- **Branch cleanup**: remote branch list is clean except `ci/deploy-dispatch`, but local stale branches remain. Prune/delete local-only branches after confirming no work is needed. +- **Decide Composer lock policy**: `.gitignore` says `composer.lock` is intentionally not committed. If CI keeps `composer validate --strict`, this is fine; if release reproducibility becomes more important than library-style dependency freshness, revisit the policy. +- **Remove npm audit allowlist when upstream fixes `@wordpress/env`**: the advisory is dev-only and documented in `bin/audit-npm.mjs`; Dependabot/npm updates should revisit it. + +## Verification notes + +Verification should be run after these edits with: + +```bash +composer validate --strict +composer test:unit +composer lint +composer analyse:phpstan +npm run test:js +npm run audit:npm +npm run check:doc-links +bash bin/build.sh +npm run env:start +npm run test:php +npm run test:e2e +npx wp-env run cli wp plugin install plugin-check --activate +npx wp-env run cli wp plugin check /var/www/html/wp-content/plugins/maestro-menu-editor/build/maestro-menu-editor --format=json +npm run env:stop +``` diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5ad3c2d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,22 @@ +# Code of Conduct + +This project follows the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/), version 2.1. + +## Our pledge + +We pledge to make participation in this project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +## Expected behavior + +- Be respectful and constructive. +- Assume good intent, but accept corrections gracefully. +- Focus criticism on ideas, code, documentation, and process. +- Help keep discussions welcoming to users and contributors with different levels of WordPress experience. + +## Unacceptable behavior + +Harassment, insults, threats, sexualized language or imagery, sustained disruption, doxxing, and other conduct that would reasonably make participation unsafe or unwelcome are not acceptable. + +## Enforcement + +Project maintainers may remove comments, close issues, block users, or take other action they deem appropriate. Report unacceptable behavior privately to the maintainer through the contact options on the maintainer's GitHub profile. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1822d9d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing to Maestro + +Thanks for helping improve Maestro. This project is a WordPress plugin with a plain PHP/JS/CSS runtime and dev tooling for PHPUnit, Playwright, wp-env, WordPress Coding Standards, and WordPress.org packaging. + +## Development setup + +```bash +composer install +npm ci +npm run env:start +``` + +The plugin is mounted into the wp-env site as `maestro-menu-editor`. + +## Quality gates + +Run the smallest useful checks first, then the full suite before opening a PR: + +```bash +composer test:unit +composer lint +composer analyse:phpstan +npm run test:js +npm run check:doc-links +npm run audit:npm +bash bin/build.sh +npm run test:php +npm run test:e2e +``` + +`npm run test:php` and `npm run test:e2e` require Docker through `@wordpress/env`. + +## Coding standards + +- Target WordPress 6.4+ and PHP 7.4+ unless the plugin headers/readme change deliberately. +- Keep runtime files limited to `maestro-menu-editor.php`, `includes/`, `assets/`, `languages/`, `uninstall.php`, and `readme.txt`. +- Use capability checks with nonces for state-changing behavior. +- Sanitize on input and escape on output. +- Treat menu visibility as cosmetic only; do not describe it as access control. +- Keep user-facing strings translatable with the `maestro-menu-editor` text domain. + +## Pull requests + +- Use a focused branch and a concise title. +- Explain the user-facing change and the verification you ran. +- Include screenshots or video for UI changes when practical. +- Update `README.md`, `readme.txt`, `SPEC.md`, or `TESTING.md` when behavior, requirements, or QA expectations change. + +## Security + +Do not report vulnerabilities in public issues or pull requests. See `SECURITY.md`. diff --git a/README.md b/README.md index 92d65c8..b701483 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ For the longer walkthrough, see [`docs/user-guide.md`](docs/user-guide.md). ## Status -**v1.1.0 is live on the WordPress.org plugin directory** ([maestro-menu-editor](https://wordpress.org/plugins/maestro-menu-editor/)) — it adds keyboard-accessible reordering, a live "modified" indicator, solid-fill bundled icons, native dashicon save-status, and edit-mode polish over the 1.0.0 base (roadmap in [`.planning/ROADMAP.md`](.planning/ROADMAP.md)). The server core (replay engine, REST API, sanitization) and the editor are done, and all test layers are green (unit 44, integration 29, E2E 16; phpcs clean; Plugin Check reports no errors on the extracted build zip). The editor uses the click-to-select model with debounced autosave: +**v1.1.1 is live on the WordPress.org plugin directory** ([maestro-menu-editor](https://wordpress.org/plugins/maestro-menu-editor/)) — it includes the v1.1 polish release plus follow-up toolbar label refinements over the 1.0.0 base (roadmap in [`.planning/ROADMAP.md`](.planning/ROADMAP.md)). The server core (replay engine, REST API, sanitization) and the editor are done, and all test layers are expected to stay green (unit 44, integration 29, E2E coverage, JavaScript unit tests, phpcs, PHPStan, and Plugin Check on the runtime build). The editor uses the click-to-select model with debounced autosave: - **Debounced autosave (~500 ms)** on reorder, rename, icon pick, visibility toggle, and per-item reset — no manual Save button; a "Saving… / Saved" status indicator (dashicon + text) instead. Saves are serialized (single-flight) so a slow request can't overwrite newer edits. Reload only on Exit (which flushes any pending save) and on Reset all. - **Click-to-select with one shared controls panel.** No edit chrome until an item is selected: each row shows only a hover/focus-revealed drag handle. Selecting an item opens the shared panel (rename, icon picker for top-level items, per-role visibility, reset-this-item). @@ -78,7 +78,7 @@ The plugin uses the `maestro-menu-editor` text domain and ships a translation te Build a runtime-only zip and upload it under Plugins → Add New → Upload: ```bash -bin/build.sh # writes build/maestro.zip (runtime files only) +bin/build.sh # writes build/maestro-menu-editor.zip (runtime files only) ``` Activate it; **Edit Menu** appears in the admin bar. Never ship the dev tooling inside the installed plugin. @@ -118,6 +118,10 @@ in the editor, then use **Switch To** (admin bar) to view the menu as that user. > > Both use a `git:directory` resource; the local [`playground/blueprint.json`](playground/blueprint.json) mounts the working tree instead. +## Contributing + +See [`CONTRIBUTING.md`](CONTRIBUTING.md) for local setup, quality gates, and pull request expectations. For security reports, use the private process in [`SECURITY.md`](SECURITY.md); do not open public security issues. + ## License GPL-2.0-or-later. See [`LICENSE`](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d9bb372 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Security Policy + +## Supported versions + +Security fixes are targeted at the latest released version of Maestro. Older releases may receive fixes when the issue is severe and a safe backport is practical. + +## Reporting a vulnerability + +Please do **not** open a public GitHub issue or pull request for a suspected vulnerability. + +Use GitHub's private vulnerability reporting / Security Advisory flow for this repository. If that is unavailable, contact the maintainer privately through the contact options on the maintainer's GitHub profile and include only the minimum detail needed to establish a private channel. + +A good initial report includes: + +- affected version or commit; +- high-level impact; +- WordPress/PHP/browser environment; +- whether authentication or a specific role is required. + +Avoid publishing exploit details until a fix is available and coordinated disclosure is agreed. + +## Expected response + +- Initial acknowledgement target: within 7 days. +- Triage/update target: within 14 days after acknowledgement. +- Fix timing depends on severity, reproducibility, and release risk. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..6fd3e75 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,11 @@ +# Support + +For general usage help, start with: + +- `README.md` for overview and quick start; +- `docs/user-guide.md` for the longer walkthrough; +- the WordPress.org plugin support forum for installed-release support. + +Use GitHub issues for reproducible bugs, feature requests, documentation corrections, and development tasks. + +Do not use public issues for security reports. See `SECURITY.md`. diff --git a/TESTING.md b/TESTING.md index 948f9a7..184e7c9 100644 --- a/TESTING.md +++ b/TESTING.md @@ -2,9 +2,7 @@ Three layers, smallest and fastest first. -> **Current local status:** all three layers pass: unit 44/44, integration 29/29 -> with 81 assertions, and E2E 9/9. The E2E suite includes Phase 3 coverage for -> reset-this-item and per-role visibility. +> **Current expected status:** unit 44/44, integration 29/29 with 81 assertions, JavaScript unit tests, phpcs, PHPStan, Plugin Check, and the Playwright E2E suite should pass before release. E2E coverage includes reset-this-item, per-role visibility, icon persistence, keyboard reordering, first-run cues, and toolbar accessibility checks. ## Gotchas (first run) @@ -81,3 +79,27 @@ stores the session. The DOM-join (locating submenu items by index within `.wp-submenu`) is only exercised by the E2E layer — that is the layer to watch when testing against a real-world menu with third-party plugins registered. + + +## 4. Static and package QA + +Additional release gates: + +```bash +composer lint +composer analyse:phpstan +npm run test:js +npm run check:doc-links +npm run audit:npm +bash bin/build.sh +``` + +To run Plugin Check locally against the runtime tree: + +```bash +npm run env:start +npx wp-env run cli wp plugin install plugin-check --activate +npx wp-env run cli wp plugin check /var/www/html/wp-content/plugins/maestro-menu-editor/build/maestro-menu-editor --format=json +``` + +`npm run audit:npm` wraps `npm audit` with a narrow allowlist for the current dev-only `@wordpress/env` → `js-yaml` advisory. Remove that allowlist when upstream ships a clean dependency path. diff --git a/assets/maestro.js b/assets/maestro.js index af298c7..e5afb86 100644 --- a/assets/maestro.js +++ b/assets/maestro.js @@ -1038,26 +1038,57 @@ return doAutosave(); // captures edits made while the last POST was in flight } setStatus( ok ? 'saved' : 'error' ); + inFlight = null; return null; } + function waitForSaveIdle() { + if ( saveTimer ) { + return flushAutosave(); + } + return inFlight || Promise.resolve(); + } + + function cancelQueuedAutosave() { + if ( saveTimer ) { + clearTimeout( saveTimer ); + saveTimer = null; + } + savePending = false; + } + function doResetAll( e ) { e.preventDefault(); if ( ! window.confirm( I.confirmAll ) ) { return; } - fetch( D.restUrl, { - method: 'DELETE', - headers: { 'X-WP-Nonce': D.nonce }, - credentials: 'same-origin' - } ) - .then( function () { window.location.reload(); } ) - .catch( function () { setStatus( 'error' ); } ); + + var button = e.currentTarget; + if ( button ) { button.disabled = true; } + setStatus( 'saving' ); + cancelQueuedAutosave(); + + ( inFlight || Promise.resolve() ) + .then( function () { + return fetch( D.restUrl, { + method: 'DELETE', + headers: { 'X-WP-Nonce': D.nonce }, + credentials: 'same-origin' + } ); + } ) + .then( function ( r ) { + if ( ! r.ok ) { throw new Error( 'HTTP ' + r.status ); } + window.location.reload(); + } ) + .catch( function () { + if ( button ) { button.disabled = false; } + setStatus( 'error' ); + } ); } function onExit( e ) { - // If there's pending work, flush it before navigating so nothing is lost. - if ( saveTimer ) { + // If there's pending or active work, flush/wait before navigating so nothing is lost. + if ( saveTimer || inFlight ) { e.preventDefault(); - flushAutosave().then( function () { + waitForSaveIdle().then( function () { window.location.href = D.exitUrl; } ); } diff --git a/bin/audit-npm.mjs b/bin/audit-npm.mjs new file mode 100755 index 0000000..66532a1 --- /dev/null +++ b/bin/audit-npm.mjs @@ -0,0 +1,48 @@ +#!/usr/bin/env node +/** + * npm audit wrapper with a narrow, documented dev-tooling exception. + * + * @wordpress/env currently depends on js-yaml 3.x and npm reports + * GHSA-h67p-54hq-rp68. This repository uses @wordpress/env only in local/CI + * development to start disposable WordPress test containers; it is never shipped + * in the runtime plugin zip. Keep this allowlist small and remove it as soon as + * upstream publishes a non-vulnerable dependency path. + */ +import { spawnSync } from 'node:child_process'; + +const allowed = new Set( [ 'GHSA-h67p-54hq-rp68' ] ); +const result = spawnSync( 'npm', [ 'audit', '--json' ], { encoding: 'utf8' } ); +const stdout = result.stdout || '{}'; +let report; +try { + report = JSON.parse( stdout ); +} catch ( error ) { + process.stdout.write( stdout ); + process.stderr.write( result.stderr || '' ); + process.exit( result.status || 1 ); +} + +const findings = []; +for ( const vulnerability of Object.values( report.vulnerabilities || {} ) ) { + for ( const via of vulnerability.via || [] ) { + if ( typeof via === 'string' ) { + continue; + } + const url = via.url || ''; + const id = url.split( '/' ).pop(); + if ( ! allowed.has( id ) ) { + findings.push( { name: vulnerability.name, title: via.title, severity: via.severity, url } ); + } + } +} + +if ( findings.length ) { + console.error( JSON.stringify( findings, null, 2 ) ); + process.exit( 1 ); +} + +if ( ( report.metadata?.vulnerabilities?.total || 0 ) > 0 ) { + console.log( 'npm audit: only allowlisted dev-tooling advisories found.' ); +} else { + console.log( 'npm audit: no vulnerabilities found.' ); +} diff --git a/bin/build.sh b/bin/build.sh index 3332a80..66f21ea 100755 --- a/bin/build.sh +++ b/bin/build.sh @@ -14,6 +14,7 @@ rm -rf "$STAGE" "$OUT/$SLUG.zip" mkdir -p "$STAGE" cp "$ROOT/$SLUG.php" "$STAGE/" +cp "$ROOT/uninstall.php" "$STAGE/" cp -r "$ROOT/includes" "$STAGE/" cp -r "$ROOT/assets" "$STAGE/" if [ -d "$ROOT/languages" ]; then diff --git a/composer.json b/composer.json index 6965c81..9455355 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "danknauss/maestro", - "description": "In-place editing of the WordPress admin menu — rename, reorder, icons, per-role visibility.", + "description": "In-place editing of the WordPress admin menu \u2014 rename, reorder, icons, per-role visibility.", "type": "wordpress-plugin", "license": "GPL-2.0-or-later", "require": { @@ -12,7 +12,9 @@ "wp-phpunit/wp-phpunit": "^6.4", "wp-coding-standards/wpcs": "^3.0", "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "phpcompatibility/phpcompatibility-wp": "^2.1" + "phpcompatibility/phpcompatibility-wp": "^2.1", + "phpstan/phpstan": "^1.12", + "szepeviktor/phpstan-wordpress": "^1.3" }, "autoload-dev": { "psr-4": { @@ -23,7 +25,8 @@ "scripts": { "test:unit": "phpunit -c phpunit-unit.xml.dist", "test:integration": "phpunit -c phpunit-integration.xml.dist", - "lint": "phpcs --standard=phpcs.xml.dist maestro-menu-editor.php includes/" + "lint": "phpcs --standard=phpcs.xml.dist maestro-menu-editor.php uninstall.php includes/", + "analyse:phpstan": "phpstan analyse --configuration=phpstan.neon.dist --memory-limit=512M --no-progress" }, "config": { "allow-plugins": { diff --git a/includes/class-rest.php b/includes/class-rest.php index 3f72c7a..4aa96fb 100644 --- a/includes/class-rest.php +++ b/includes/class-rest.php @@ -105,7 +105,7 @@ public function get_config() { * Full-replace save. * * @param \WP_REST_Request $request Request. - * @return \WP_REST_Response + * @return \WP_REST_Response|\WP_Error */ public function save_config( \WP_REST_Request $request ) { $incoming = $request->get_param( 'config' ); diff --git a/package-lock.json b/package-lock.json index b403fc8..33870dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "maestro", - "version": "1.0.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "maestro", - "version": "1.0.0", + "version": "1.1.1", "devDependencies": { "@playwright/test": "^1.45.0", - "@wordpress/env": "^10.6.0", + "@wordpress/env": "^11.8.1", "bootstrap-icons": "^1.13.1" } }, @@ -1487,25 +1487,24 @@ } }, "node_modules/@wordpress/env": { - "version": "10.39.0", - "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-10.39.0.tgz", - "integrity": "sha512-Hgl2RQAAzXFMqkpegGWT1/KkX88OVikRroPidWkij1WtU8p+AZniTcncWmlWqbdLdfGbPqQS5ZkqDZCzrQjgnA==", + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-11.8.1.tgz", + "integrity": "sha512-wboYnQNPfMfcgYgiNDbrGGZj8RkdeZtaz2UvmAVUrhOgAvHFpZXsKuXuLdCCSv7JdcHGG5xLWMYhvRLTGOcQ1g==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { "@inquirer/prompts": "^7.2.0", - "@wp-playground/cli": "^3.0.0", - "chalk": "^4.0.0", + "@wp-playground/cli": "^3.0.48", + "adm-zip": "^0.5.9", + "chalk": "^4.1.1", "copy-dir": "^1.3.0", "cross-spawn": "^7.0.6", "docker-compose": "^0.24.3", - "extract-zip": "^1.6.7", "got": "^11.8.5", "js-yaml": "^3.13.1", "ora": "^4.0.2", "rimraf": "^5.0.10", - "simple-git": "^3.5.0", - "terminal-link": "^2.0.0", + "simple-git": "^3.24.0", "yargs": "^17.3.0" }, "bin": { @@ -1668,6 +1667,16 @@ "node": ">= 0.6" } }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -1699,35 +1708,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1938,16 +1918,6 @@ "dev": true, "license": "MIT" }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1955,13 +1925,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2204,22 +2167,6 @@ "dev": true, "license": "MIT" }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2267,13 +2214,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -2662,39 +2602,6 @@ "dev": true, "license": "MIT" }, - "node_modules/extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - } - }, - "node_modules/extract-zip/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/extract-zip/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2759,16 +2666,6 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -3249,13 +3146,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3646,19 +3536,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4006,13 +3883,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -4090,13 +3960,6 @@ "node": ">= 0.6.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4190,29 +4053,6 @@ "node": ">=0.10.0" } }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4677,23 +4517,6 @@ "node": ">= 0.8" } }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4795,37 +4618,6 @@ "node": ">=8" } }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminal-link/node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", @@ -4907,13 +4699,6 @@ "node": ">= 0.4" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true, - "license": "MIT" - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -4959,13 +4744,6 @@ "node": ">= 0.8" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5199,17 +4977,6 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yoctocolors-cjs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", diff --git a/package.json b/package.json index f7d3c57..32025d3 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "maestro", - "version": "1.0.0", + "version": "1.1.1", "private": true, "description": "Tooling for Maestro: The Inline Admin Menu Editor: wp-env, PHP integration tests, Playwright E2E.", "scripts": { "check:doc-links": "node bin/check-doc-links.mjs", "test:js": "node --test tests/js/*.mjs", + "audit:npm": "node bin/audit-npm.mjs", "env:start": "wp-env start", "env:stop": "wp-env stop", "env:clean": "wp-env clean all", @@ -19,7 +20,7 @@ }, "devDependencies": { "@playwright/test": "^1.45.0", - "@wordpress/env": "^10.6.0", + "@wordpress/env": "^11.8.1", "bootstrap-icons": "^1.13.1" } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..c364517 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,15 @@ +includes: + - vendor/szepeviktor/phpstan-wordpress/extension.neon + +parameters: + level: 3 + paths: + - maestro-menu-editor.php + - includes + - uninstall.php + excludePaths: + analyse: + - includes/icons-bootstrap.php + bootstrapFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + - tests/phpstan-bootstrap.php diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts index 37b10d7..02bea40 100644 --- a/tests/e2e/global-setup.ts +++ b/tests/e2e/global-setup.ts @@ -45,11 +45,14 @@ async function globalSetup( config: FullConfig ) { const browser = await chromium.launch(); const page = await browser.newPage(); - await page.goto( 'http://localhost:8889/wp-login.php' ); + await page.goto( 'http://localhost:8889/wp-login.php', { waitUntil: 'domcontentloaded' } ); + await page.waitForSelector( '#user_login', { state: 'visible' } ); await page.fill( '#user_login', 'admin' ); await page.fill( '#user_pass', 'password' ); - await page.click( '#wp-submit' ); - await page.waitForURL( /wp-admin/ ); + await Promise.all( [ + page.waitForURL( /wp-admin/, { waitUntil: 'domcontentloaded' } ), + page.click( '#wp-submit' ), + ] ); await page.context().storageState( { path: statePath } ); await browser.close(); diff --git a/tests/phpstan-bootstrap.php b/tests/phpstan-bootstrap.php new file mode 100644 index 0000000..6f69c80 --- /dev/null +++ b/tests/phpstan-bootstrap.php @@ -0,0 +1,22 @@ +