Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .distignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ assets/vue
yarn.lock
.eslintrc
CONTRIBUTING.md
AGENTS.md
phpstan.neon
phpstan-baseline.neon
.wp-env.json
.wp-env.override.json
.phpunit.result.cache
artifacts
playwright-report
test-results
4 changes: 2 additions & 2 deletions .github/workflows/build-dev-artifact.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
node-version: [20.x]
outputs:
branch-name: ${{ steps.retrieve-branch-name.outputs.branch_name }}
git-sha-8: ${{ steps.retrieve-git-sha-8.outputs.sha8 }}
Expand Down Expand Up @@ -50,7 +50,7 @@ jobs:
- name: Install composer deps
run: composer install --no-dev --prefer-dist --no-progress --no-suggest
- name: Install yarn deps
run: yarn install --frozen-lockfile
run: yarn install --frozen-lockfile --ignore-engines
- name: Build files
run: yarn run build
- name: Create zip
Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: "Copilot Setup Steps"

on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml

jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read

# These steps run before the Copilot coding agent starts, so the agent
# boots into a checkout with a running wp-env instance and both test
# suites (npm run test:unit:php:base / npm run test:e2e) ready to use.
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: "yarn"
- name: Install JS deps
run: |
yarn install --frozen-lockfile --ignore-engines
- name: Install Playwright
run: |
npx playwright install --with-deps chromium
- name: Setup PHP with tools
uses: shivammathur/setup-php@v2
with:
php-version: "7.4"
extensions: simplexml, mysql, mbstring, curl
tools: wp-cli
- name: Install composer deps
run: composer install --no-progress
- name: Start the WordPress environment via wp-env
run: |
yarn run env:start
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
node-version: [20.x]
steps:
- uses: actions/checkout@master
- name: Build files using ${{ matrix.node-version }}
Expand All @@ -19,7 +19,7 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Build
run: |
yarn install --frozen-lockfile
yarn install --frozen-lockfile --ignore-engines
yarn run build
composer install --no-dev --prefer-dist --no-progress --no-suggest
- name: WordPress Plugin Deploy
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Test E2E

on:
pull_request:
types: [opened, synchronize, ready_for_review]

concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
e2e:
name: Playwright
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: "yarn"
- name: Setup PHP version
uses: shivammathur/setup-php@v2
with:
php-version: "7.4"
- name: Install composer
run: composer install --prefer-dist --no-progress
- name: Install JS deps
run: |
yarn install --frozen-lockfile --ignore-engines
- name: Install Playwright
run: |
npx playwright install --with-deps chromium
- name: Setup WP Env
run: |
yarn run env:start
- name: Run Playwright tests
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
run: |
yarn run test:e2e
- name: Upload tests artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: artifacts-e2e
path: ./artifacts
retention-days: 3
23 changes: 20 additions & 3 deletions .github/workflows/test-php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ jobs:
- name: Setup PHP version
uses: shivammathur/setup-php@v2
with:
php-version: '7.2'
# the modern sniffer stack needs PHP 7.4+ and a current composer
php-version: '7.4'
extensions: simplexml
tools: composer:v2.1
- name: Checkout source code
uses: actions/checkout@v2
- name: Get Composer Cache Directory
Expand All @@ -36,6 +36,22 @@ jobs:
- name: Run PHPCS
run: composer run lint

phpstan:
name: PHPStan
runs-on: ubuntu-latest
steps:
- name: Setup PHP version
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
extensions: simplexml
- name: Checkout source code
uses: actions/checkout@v6
- name: Install composer
run: composer install --prefer-dist --no-progress
- name: PHPStan Static Analysis
run: composer run phpstan

phpunit:
name: PHPUnit
runs-on: ubuntu-latest
Expand All @@ -51,7 +67,8 @@ jobs:
- name: Setup PHP version
uses: shivammathur/setup-php@v2
with:
php-version: '7.2'
# the latest WordPress test suite requires PHP 7.4+
php-version: '7.4'
extensions: simplexml, mysql
tools: phpunit:7.5.20, phpunit-polyfills
- name: Checkout source code
Expand Down
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,14 @@ artifact
# build assets files
assets/js/*.min.js
assets/css/*.min.css

### yarn.lock is the tracked lockfile; npm's lockfile must never be committed
### (npm install also rewrites yarn.lock into a format yarn 1 cannot parse)
package-lock.json

### wp-env / tests
.wp-env.override.json
.phpunit.result.cache
artifacts/
playwright-report/
test-results/
16 changes: 16 additions & 0 deletions .wp-env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"core": null,
"phpVersion": "7.4",
"testsEnvironment": false,
"plugins": [ "." ],
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
"WP_DEBUG_DISPLAY": false,
"SCRIPT_DEBUG": true,
"FS_METHOD": "direct"
},
"lifecycleScripts": {
"afterStart": "bash bin/wp-env-setup.sh"
}
}
64 changes: 54 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,33 @@ WP Maintenance Mode (LightStart) is a WordPress plugin by Themeisle that display
## Commands

```bash
# Install dependencies
# Install dependencies (yarn.lock is the tracked lockfile; package-lock.json is
# ignored, and --ignore-engines is needed because the legacy eslint toolchain
# caps its node engines range below current node versions)
composer install
npm install
yarn install --frozen-lockfile --ignore-engines

# Lint (PHP_CodeSniffer with WordPress coding standards)
composer run lint
# Environment (wp-env, requires Docker)
npm run env:start # reuse a running instance, or pick a free port + start
npm run env:stop
npm run env:cleanup # remove THIS checkout's containers/volumes (run before deleting a worktree)

# Auto-fix coding standards
composer run format
# Lint / format (PHP_CodeSniffer with WordPress coding standards)
composer run lint # on the host
npm run lint:php # same, inside the wp-env container
npm run format:php

# Run PHPUnit tests
./vendor/bin/phpunit
# Static analysis (PHPStan level 6, WordPress stubs)
composer run phpstan
composer run phpstan:generate:baseline # refresh phpstan-baseline.neon after intentional changes

# Run a single test file
./vendor/bin/phpunit tests/generic-test.php
# PHP unit tests (run inside wp-env; no local MySQL/SVN setup needed)
npm run test:unit:php # start env + run the suite
npm run test:unit:php:base # run the suite against an already-running env

# E2E tests (Playwright; needs `npx playwright install chromium` once)
npm run test:e2e
npm run test:e2e:ui # interactive UI mode

# Build assets (minify JS + CSS via Grunt)
npm run build # or: npx grunt
Expand All @@ -30,6 +42,38 @@ npm run build # or: npx grunt
npx grunt watch
```

Configs: PHPUnit `phpunit.xml` (+ `tests/bootstrap.php`), Playwright `tests/e2e/playwright.config.js`, wp-env `.wp-env.json` + gitignored `.wp-env.override.json`, PHPStan `phpstan.neon` + `phpstan-baseline.neon` (stubs for optional integrations and dynamic constants live in `tests/static-analysis-stubs/`, loaded only during analysis).

## wp-env Instance

Single environment (`"testsEnvironment": false`): one `wordpress`/`cli`/`mysql` container set serves dev, PHPUnit, and E2E. Login: `admin`/`password`. Port precedence everywhere: `WP_BASE_URL` > `WP_ENV_PORT` > `.wp-env.override.json` > `8888` — `npm run env:start` (`bin/wp-env-up.js`) probes for a free port and pins it in the override file, which the Playwright config reads too.

- PHPUnit runs in the container with `WORDPRESS_TABLE_PREFIX=wptests_` (baked into `test:unit:php:base`). Never drop that prefix override: the container's tests config otherwise points at the dev site's own `wp_` tables and the suite install WIPES the site.
- `bin/wp-env-setup.sh` (wp-env `afterStart`) sets pretty permalinks, provisions the plugin's default settings, and disables the setup-wizard redirect plus the block-based "new look" (`wpmm_new_look=0`) so the classic settings screens are testable. While `wpmm_settings` is missing, the plugin re-flags the install as fresh on every request — provision settings before touching `wpmm_fresh_install`.
- The environment runs with `SCRIPT_DEBUG=true`, so unminified assets load and no Grunt build is needed for tests; `WPMM_ASSETS_SUFFIX` is empty in this mode.

## E2E Rules (Important)

- `workers: 1`, always. Maintenance mode is a site-wide option; virtually every spec flips global state. Do not raise the worker count or add `fullyParallel`.
- Specs must leave maintenance mode OFF when they finish, or every later spec sees the 503 page.
- Use the helpers in `tests/e2e/utils.js` (`setMaintenanceMode`, `activateSettingsTab`, `saveSettingsForm`, `openAsVisitor`) instead of hand-rolling admin flows. They encode three non-obvious constraints:
- After any settings save (form POST + redirect), headless Chromium stops producing animation frames for the page, so non-`force` Playwright interactions hang on the "element is stable" actionability check. Helpers use `force: true` plus explicit state assertions.
- The settings tabs are hash-driven; navigating to the hash-less settings URL is a same-document navigation, so the previously active tab persists. Always `activateSettingsTab` explicitly.
- The save redirect lands on `?updated=1#<tab>` and WordPress strips the query right after load — wait for the redirect response, not the URL.
- Visitor contexts: `browser.newContext()` inherits the admin `storageState`; `openAsVisitor` clears cookies to get a real logged-out visitor.

## Testing Practices (TDD)

- Red → green: write the failing test first, then only enough code to make it pass. One seam, one test, one minimal implementation per cycle — don't batch-write tests for imagined behavior (vertical slices, not horizontal). Refactoring is a separate review-stage step, not part of the loop.
- Test behavior through public seams, never internals: the admin-ajax endpoints (`wp_ajax_wpmm_*`), public helpers (`includes/functions/helpers.php`), rendered frontend output, and the settings screens. If a test breaks when you refactor but behavior hasn't changed, it's testing the wrong thing.
- Expected values must come from an independent source of truth (a known-good literal, the spec, a worked example) — never recompute them the way the code does.
- Plugin-specific seams and traps (see `tests/ajax-api-test.php`):
- Several AJAX handlers guard with plain `die( $msg )` (not `wp_die`); those guard paths CANNOT be exercised through `_handleAjax` — a plain `die()` kills the PHPUnit process with exit code 0.
- Handlers wrapped in `catch ( Exception )` swallow the suite's die-exception and answer twice; the test class installs a die handler that throws `WPMM_Ajax_Die_Signal extends Error` instead.
- `_handleAjax()` fires `admin_init` without defining `DOING_AJAX`; keep `maybe_redirect` unhooked in tests or a fresh-install flag turns it into a process-killing wizard redirect.
- The WP suite auto-excludes tests annotated `@group ajax` — don't add that annotation.
- wp-env's sender address `wordpress@localhost` fails PHPMailer validation; filter `wp_mail_from` in mail tests.

## Architecture

### Entry Point & Bootstrap
Expand Down
18 changes: 18 additions & 0 deletions bin/wp-env-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh

# Prepare the wp-env instance for development and tests (wp-env afterStart hook).

# Pretty permalinks: the Playwright global-setup talks to /wp-json/.
npx wp-env run cli wp rewrite structure '/%postname%/'

# Provision the plugin's default settings; while wpmm_settings is missing the
# plugin re-flags the install as fresh on every request.
npx wp-env run cli wp eval 'WP_Maintenance_Mode::single_activate();'

# Skip the setup wizard redirect so the settings screens are directly reachable.
npx wp-env run cli wp option update wpmm_fresh_install 0

# Use the classic (pre-Gutenberg) maintenance screen: the modules settings UI
# (subscribe, countdown, social) only renders in this mode, and the block-based
# flow needs Otter Blocks plus the wizard, which the E2E suite does not cover.
npx wp-env run cli wp option update wpmm_new_look 0
Loading
Loading