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
82 changes: 82 additions & 0 deletions .github/workflows/tests-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# cspell:ignore shivammathur
name: "E2E Tests"

on:
push:
branches:
- main
pull_request:

jobs:
test:
runs-on: ubuntu-latest

name: "E2E: Software Manager"

steps:
- name: Checkout the repository
uses: actions/checkout@v6

# -----------------------------------------------------------------------
# PHP + Composer (vendor/ is gitignored; needed for Harbor class loading)
# -----------------------------------------------------------------------
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.1"
tools: composer:v2
coverage: none

- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist

# -----------------------------------------------------------------------
# Node + JS dependencies
# -----------------------------------------------------------------------
- uses: oven-sh/setup-bun@v2

- name: Install JS dependencies
run: bun install

# -----------------------------------------------------------------------
# wp-env (Docker-based WordPress)
# -----------------------------------------------------------------------
- name: Start WordPress environment
id: wp-env
run: |
OUTPUT=$(bunx wp-env start 2>&1)
echo "$OUTPUT"
WP_URL=$(echo "$OUTPUT" | grep -oE 'http://localhost:[0-9]+' | head -1)
echo "url=${WP_URL:-http://localhost:8888}" >> "$GITHUB_OUTPUT"
env:
WP_ENV_HOME: ${{ github.workspace }}/.wp-env-home

# -----------------------------------------------------------------------
# Playwright
# -----------------------------------------------------------------------
- name: Install Playwright browsers
run: bunx playwright install chromium --with-deps

- name: Run E2E tests
run: bun run test:e2e
env:
WP_BASE_URL: ${{ steps.wp-env.outputs.url }}

# -----------------------------------------------------------------------
# Artifacts
# -----------------------------------------------------------------------
- name: Upload test artifacts on failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-artifacts
path: |
artifacts/
test-results/
retention-days: 7

- name: Stop WordPress environment
if: always()
run: bunx wp-env stop
env:
WP_ENV_HOME: ${{ github.workspace }}/.wp-env-home
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ local-scripts/
# OS
.DS_Store

# Playwright
artifacts/
test-results/

# PHPStan cache
phpstan-cache

Expand Down
14 changes: 14 additions & 0 deletions .wp-env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"plugins": [ "./tests/_data/plugins/harbor-fixture" ],
"mappings": {
"wp-content/plugins/harbor": "."
},
"testsEnvironment": false,
"port": 8901,
"autoPort": true,
"config": {
"WP_DEBUG": false,
"WP_DEBUG_LOG": false,
"WP_DEBUG_DISPLAY": false
}
}
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![PHP Compatibility](https://github.com/stellarwp/harbor/actions/workflows/compatibility.yml/badge.svg)](https://github.com/stellarwp/harbor/actions/workflows/compatibility.yml)
[![PHP Tests](https://github.com/stellarwp/harbor/actions/workflows/tests-php.yml/badge.svg)](https://github.com/stellarwp/harbor/actions/workflows/tests-php.yml)
[![PHPStan](https://github.com/stellarwp/harbor/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/stellarwp/harbor/actions/workflows/static-analysis.yml)
[![E2E Tests](https://github.com/stellarwp/harbor/actions/workflows/tests-e2e.yml/badge.svg)](https://github.com/stellarwp/harbor/actions/workflows/tests-e2e.yml)

## Installation

Expand Down Expand Up @@ -132,4 +133,4 @@ Start with [Harbor Overview](/docs/harbor.md) for the full architecture.

- [Integration Guide](/docs/guides/integration.md) — How to integrate your plugin with Harbor.
- [CLI Commands](/docs/guides/cli.md) — WP-CLI commands for feature management.
- [Testing](/docs/guides/testing.md) — Running automated tests with Codeception and `slic`.
- [Testing](/docs/guides/testing.md) — PHP tests with Codeception/`slic`; E2E tests with Playwright/wp-env.
740 changes: 679 additions & 61 deletions bun.lock

Large diffs are not rendered by default.

73 changes: 72 additions & 1 deletion docs/guides/testing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,77 @@
# Automated tests

This repository uses Codeception for automated testing and leverages [`slic`](https://github.com/stellarwp/slic) for running the tests.
This repository has two test suites:

- **PHP tests** — Codeception unit/integration tests run via [`slic`](https://github.com/stellarwp/slic)
- **E2E tests** — Playwright browser tests run against a Docker WordPress environment via [`wp-env`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/)

---

## E2E tests (Playwright)

E2E tests live in `tests/e2e/` and exercise the Software Manager admin page end-to-end through a real browser against a real WordPress installation.

### How it works

A fixture WordPress plugin (`tests/_data/plugins/harbor-fixture/`) boots Harbor with local JSON fixture data instead of live API calls. Playwright logs in once via `global-setup.ts`, saves the session to `artifacts/storage-states/admin.json`, and reuses it across tests.

### Prerequisites

- Docker (for wp-env)
- [Bun](https://bun.sh)
- Composer

### Running locally

```bash
# Install dependencies (first time only)
bun install
composer install

# Start the WordPress environment (port 8901 by default)
bunx wp-env start

# Install Playwright browsers (first time only)
bunx playwright install chromium --with-deps

# Run all E2E tests (headless)
bun run test:e2e

# Stop the environment when done
bunx wp-env stop
```

### Watching tests run in a browser

```bash
# Opens a live Chrome window; each action is slowed to 800 ms
bun run test:e2e:headed
```

### Interactive UI mode (time-travel viewer)

```bash
bun run test:e2e:ui
```

UI mode lets you step through each action and view a screenshot at that point in time. The browser preview is blank between runs because `@wordpress/e2e-test-utils-playwright` closes the page after each test — click an action step in the timeline to see its screenshot.

### Port configuration

wp-env starts on port **8901** by default (configured in `.wp-env.json`). If that port is taken, `autoPort: true` picks the next available one. Override both wp-env and Playwright together with a single env var:

```bash
WP_ENV_PORT=9000 bunx wp-env start
WP_ENV_PORT=9000 bun run test:e2e
```

### CI

The GitHub Actions workflow (`.github/workflows/tests-e2e.yml`) captures the URL that wp-env prints on startup and passes it as `WP_BASE_URL` to the Playwright run, so the dynamic port is handled automatically.

---

## PHP tests (Codeception + slic)

## Pre-requisites

Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
"lint:md": "markdownlint '**/*.md' --ignore node_modules --ignore vendor",
"fix:md": "prettier --write '**/*.md' '!node_modules/**' '!vendor/**' && markdownlint '**/*.md' --ignore node_modules --ignore vendor --fix",
"test:js": "wp-scripts test-unit-js",
"test:shell": "bats tests/shell/"
"test:shell": "bats tests/shell/",
"test:e2e": "playwright test --project=chromium",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --project=headed"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
Expand All @@ -24,12 +27,15 @@
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@stellarwp/changelogger": "^0.10.0",
"@tailwindcss/postcss": "^4.1.17",
"@wordpress/api-fetch": "^7.40.0",
"@wordpress/components": "^32.3.0",
"@wordpress/data": "^10.40.0",
"@wordpress/i18n": "^6.13.0",
"@wordpress/e2e-test-utils-playwright": "^1.40.0",
"@wordpress/env": "^11.0.0",
"@wordpress/scripts": "^31.1.0",
"autoprefixer": "^10.4.22",
"bats": "^1.13.0",
Expand Down
33 changes: 33 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { defineConfig, devices } from '@playwright/test';
import { STORAGE_STATE_PATH } from './tests/e2e/global-setup';

// WP_ENV_PORT mirrors what wp-env uses (set via `WP_ENV_PORT=XXXX wp-env start`).
// WP_BASE_URL is the full override escape hatch.
const WP_ENV_PORT = process.env.WP_ENV_PORT ?? '8901';
const WP_BASE_URL = process.env.WP_BASE_URL ?? `http://localhost:${ WP_ENV_PORT }`;

export default defineConfig( {
testDir: './tests/e2e',
timeout: 60_000,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: process.env.CI ? 'github' : 'list',
globalSetup: './tests/e2e/global-setup.ts',
use: {
baseURL: WP_BASE_URL,
storageState: STORAGE_STATE_PATH,
video: 'retain-on-failure',
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices[ 'Desktop Chrome' ] },
},
{
name: 'headed',
use: { ...devices[ 'Desktop Chrome' ], headless: false, launchOptions: { slowMo: 800 } },
},
],
} );
73 changes: 73 additions & 0 deletions tests/_data/plugins/harbor-fixture/harbor-fixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
/**
* Plugin Name: Harbor E2E Fixture
* Plugin URI: https://github.com/stellarwp/harbor
* Description: Boots Harbor with fixture catalog and licensing data for E2E tests. Not for production use.
* Version: 1.0.0
* Author: Liquid Web
*/

defined( 'ABSPATH' ) || exit;

$harbor_autoloader = WP_PLUGIN_DIR . '/harbor/vendor/autoload.php';

if ( ! file_exists( $harbor_autoloader ) ) {
return;
}

require_once $harbor_autoloader;
require_once WP_PLUGIN_DIR . '/harbor/tests/_support/Helper/Licensing/Fixture_Client.php';

use lucatume\DI52\Container as DI52Container;
use LiquidWeb\Harbor\Config;
use LiquidWeb\Harbor\Harbor;
use LiquidWeb\Harbor\Portal\Clients\Portal_Client;
use LiquidWeb\Harbor\Portal\Clients\Fixture_Client as Portal_Fixture_Client;
use LiquidWeb\LicensingApiClient\Contracts\LicensingClientInterface;
use LiquidWeb\Harbor\Tests\Licensing\Fixture_Client as Licensing_Fixture_Client;
use StellarWP\ContainerContract\ContainerInterface;

// Satisfies both DI52 and StellarWP's ContainerInterface.
class Harbor_E2E_Container extends DI52Container implements ContainerInterface {}

add_action(
'plugins_loaded',
static function () {
$container = new Harbor_E2E_Container();
$container->singleton( ContainerInterface::class, $container );

Config::set_container( $container );
Config::set_plugin_basename( plugin_basename( __FILE__ ) );

Harbor::init();

$catalog_fixture = WP_PLUGIN_DIR . '/harbor/tests/_data/catalog/default.json';
$licensing_fixture_dir = WP_PLUGIN_DIR . '/harbor/tests/_data/licensing';

// Rebind after init to replace the real HTTP clients with fixture readers.
// DI52 singletons haven't been resolved yet at this point, so rebinding works.
$container->singleton(
Portal_Client::class,
static function () use ( $catalog_fixture ) {
return new Portal_Fixture_Client( $catalog_fixture );
}
);

$container->singleton(
LicensingClientInterface::class,
static function () use ( $licensing_fixture_dir ) {
return new Licensing_Fixture_Client( $licensing_fixture_dir );
}
);
},
5
);

// Seed the pro fixture license key so the UI renders with licensed product data.
// The key maps to tests/_data/licensing/lwsw-unified-pro-2026.json via strtolower().
add_action(
'init',
static function () {
update_option( 'lw_harbor_unified_license_key', 'LWSW-UNIFIED-PRO-2026', false );
}
);
20 changes: 20 additions & 0 deletions tests/e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import path from 'path';
import { RequestUtils } from '@wordpress/e2e-test-utils-playwright';

export const STORAGE_STATE_PATH =
process.env.STORAGE_STATE_PATH ||
path.join( process.cwd(), 'artifacts/storage-states/admin.json' );

const WP_ENV_PORT = process.env.WP_ENV_PORT ?? '8901';
const WP_BASE_URL = process.env.WP_BASE_URL ?? `http://localhost:${ WP_ENV_PORT }`;

async function globalSetup(): Promise<void> {
const requestUtils = await RequestUtils.setup( {
baseURL: WP_BASE_URL,
storageStatePath: STORAGE_STATE_PATH,
} );

await requestUtils.setupRest();
}

export default globalSetup;
Loading
Loading