diff --git a/.env.testing.example b/.env.testing.example
new file mode 100644
index 000000000..43d3f7642
--- /dev/null
+++ b/.env.testing.example
@@ -0,0 +1,17 @@
+# WP-Browser configuration
+# https://wpbrowser.wptestkit.dev/
+# Copy to .env.testing and adjust for your environment.
+# These defaults match the reusable codecoverage workflow (MySQL service on 33306, MYSQL_DATABASE=tests-wordpress).
+
+WP_ROOT_FOLDER="wordpress"
+
+TEST_DB_HOST="127.0.0.1"
+TEST_DB_PORT="33306"
+TEST_DB_USER="root"
+TEST_DB_PASSWORD="password"
+
+TEST_DB_NAME="tests-wordpress"
+TEST_TABLE_PREFIX="wp_"
+
+TEST_SITE_WP_DOMAIN="localhost:8888"
+TEST_SITE_ADMIN_EMAIL="admin@example.org"
diff --git a/.github/workflows/codecoverage-main.yml b/.github/workflows/codecoverage-main.yml
index 923069be9..6629fd72d 100644
--- a/.github/workflows/codecoverage-main.yml
+++ b/.github/workflows/codecoverage-main.yml
@@ -4,194 +4,38 @@ name: Codecoverage-Main
# GitHub Pages, generates a README badge with the coverage percentage.
on:
- push:
- branches:
- - trunk
- pull_request:
- types: [ opened, reopened, ready_for_review, synchronize ]
- branches:
- - trunk
- workflow_dispatch:
+ push:
+ branches:
+ - trunk
+ pull_request:
+ types: [ opened, reopened, ready_for_review, synchronize ]
+ branches:
+ - trunk
+ workflow_dispatch:
+
+permissions:
+ contents: read
jobs:
-
- codecoverage:
- runs-on: ubuntu-latest
-
- services:
- mysql:
- image: mysql:5.7 # Password auth did not work on 8.0 on PHP 7.3, it did seem to work for PHP 7.4+
- env: # These are the same username and password as the wp-env container defaults
- MYSQL_ROOT_PASSWORD: password
- MYSQL_DATABASE: tests-wordpress
- ports: # This mapping matches the .wp-env.json configuration
- - 33306:3306
- options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
-
- strategy:
- matrix: # Supported PHP versions
- php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ]
-
- steps:
- - name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- with:
- fetch-depth: 0 # attempting to get all branch names.
-
- # TODO: a DEPLOY_KEY is needed to enable gh-pages for the first time (the branch can be created and pushed but
- # it will not be reachable as a website).
- # Consider @see https://github.com/peaceiris/actions-gh-pages
- # - name: Check does gh-pages branch need to be created
- # run: |
- # git branch -l;
- # if [[ $(git branch -l gh-pages) == "" ]]; then
- # gh_pages_branch_needed=true;
- # echo "gh-pages branch is needed";
- # else
- # gh_pages_branch_needed=false
- # echo "gh-pages branch already exists";
- # fi
- # echo "GH_PAGES_BRANCH_NEEDED=$gh_pages_branch_needed" >> $GITHUB_ENV;
- # mkdir gh-pages
- #
- # - name: Maybe create gh-pages branch
- # if: ${{ env.GH_PAGES_BRANCH_NEEDED }}
- # uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
- # with:
- # github_token: ${{ secrets.GITHUB_TOKEN }}
- # publish_dir: ./gh-pages
- # force_orphan: true
- # allow_empty_commit: true
- # commit_message: "🤖 Creating gh-pages branch"
-
- - name: Checkout GitHub Pages branch for code coverage report
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- with:
- ref: gh-pages
- path: gh-pages
-
- - name: Install PHP
- uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
- with:
- php-version: ${{ matrix.php }}
- coverage: xdebug
- tools: composer, jaschilz/php-coverage-badger
- extensions: zip
-
- - name: Read .env.testing
- uses: c-py/action-dotenv-to-setenv@925b5d99a3f1e4bd7b4e9928be4e2491e29891d9 # v5
- with:
- env-file: .env.testing
-
- - name: Run composer install
- continue-on-error: true
- run: composer install -v
-
- - name: Allow writing to wp-content
- if: ${{ hashFiles('wp-content') != '' }} # TODO: This may be incorrect since it's a directory.
- run: sudo chmod -R a+w wp-content
-
- - name: Print refs
- run: |
- echo "ref: ${{ github.ref }}"
- echo "head_ref: ${{ github.head_ref }}"
- echo "base_ref: ${{ github.base_ref }}"
-
- # On trunk, there will be a previous coverage report. On PRs we create a new directory using the commit SHA.
- - name: Clear previous code coverage
- if: ${{ (matrix.php == '7.3') && (github.ref == 'refs/heads/trunk') }}
- run: |
- rm -rf gh-pages/phpunit || true;
- mkdir gh-pages/phpunit || true;
-
-# - name: Run unit tests
-# if: ${{ hashFiles('tests/phpunit/bootstrap.php') != '' }} # Only run unit tests if they are present.
-# run: XDEBUG_MODE=coverage vendor/bin/phpunit --bootstrap tests/phpunit/bootstrap.php --coverage-php tests/_output/unit.cov --debug
-
- - name: Run wpunit tests
- run: XDEBUG_MODE=coverage vendor/bin/codecept run wpunit --coverage tests/_output/wpunit.cov --debug
-
- # For PRs, we'll generate the coverage report on each pushed commit
- - name: Merge code coverage for PR
- if: ${{ matrix.php == '7.3' && github.event_name == 'pull_request' }}
- run: |
- vendor/bin/phpcov merge --clover clover.xml tests/_output/;
- vendor/bin/phpcov merge --clover gh-pages/${{ github.event.pull_request.head.sha }}/phpunit/clover.xml --php gh-pages/${{ github.event.pull_request.head.sha }}/phpunit/phpunit.cov --html gh-pages/${{ github.event.pull_request.head.sha }}/phpunit/html/ tests/_output/;
-
- # On main, we want it output to a different path
- - name: Merge code coverage for main
- if: ${{ (matrix.php == '7.3') && (github.ref == 'refs/heads/trunk') }}
- run: |
- vendor/bin/phpcov merge --clover clover.xml tests/_output/;
- vendor/bin/phpcov merge --clover gh-pages/phpunit/clover.xml --php gh-pages/phpunit/phpunit.cov --html gh-pages/phpunit/html/ tests/_output/;
-
- # This makes the coverage percentage available in `{{ steps.coverage-percentage.outputs.coverage-rounded }}`.
- - name: Check test coverage
- if: ${{ matrix.php == '7.3' }}
- uses: johanvanhelden/gha-clover-test-coverage-check@2543c79a701f179bd63aa14c16c6938c509b2cec # v1
- id: coverage-percentage
- with:
- percentage: 25
- exit: false
- filename: clover.xml
- rounded-precision: "0"
-
- # See: https://github.blog/2009-12-29-bypassing-jekyll-on-github-pages/
- - name: Add `.nojekyll` file so code coverage report successfully deploys to gh-pages
- if: ${{ (matrix.php == '7.3') }}
- working-directory: gh-pages
- run: |
- touch .nojekyll
- git add -- .nojekyll *
-
- - name: Update README coverage badge
- if: ${{ (matrix.php == '7.3') && (github.ref == 'refs/heads/trunk') }} # only commit on trunk, on the PHP version we're using in production.
- run: php-coverage-badger clover.xml gh-pages/phpunit/coverage.svg
-
- - name: Generate PR coverage badge
- if: ${{ (matrix.php == '7.3') && github.event_name == 'pull_request' }}
- run: php-coverage-badger clover.xml gh-pages/${{ github.event.pull_request.head.sha }}/phpunit/coverage.svg
-
- - name: Commit code coverage to gh-pages
- if: ${{ matrix.php == '7.3' }}
- uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
- with:
- repository: gh-pages
- branch: gh-pages
- commit_message: ${{ format('🤖 Save code coverage report to gh-pages {0}%', steps.coverage-percentage.outputs.coverage-rounded) }}
- commit_options: ""
- env:
- GITHUB_TOKEN: "${{ github.token }}"
-
- - name: Add coverage badge to PR comment
- if: ${{ matrix.php == '7.3' && github.event_name == 'pull_request' }}
- run: |
- echo "[](https://newfold-labs.github.io/wp-module-onboarding/${{ github.event.pull_request.head.sha }}/phpunit/html/)" >> coverage-comment.md
- echo "" >> coverage-comment.md
- echo "" >> coverage-comment.md
-
- - name: Add coverage report link to PR comment
- if: ${{ matrix.php == '7.3' && github.event_name == 'pull_request' }}
- run: |
- echo "${{ format('[project coverage report {0}%](https://newfold-labs.github.io/wp-module-onboarding/{1}/phpunit/html/) @ {2}', steps.coverage-percentage.outputs.coverage-rounded, github.event.pull_request.head.sha, github.event.pull_request.head.sha) }}" >> coverage-comment.md
- echo "" >> coverage-comment.md
- echo "" >> coverage-comment.md
-
- - name: Add phpcov uncovered lines report to PR comment
- if: ${{ matrix.php == '7.3' && github.event_name == 'pull_request' }}
- continue-on-error: true # phpcov can fail if there are no uncovered lines
- run: |
- BRANCHED_COMMIT=$(git rev-list origin..HEAD | tail -n 1);
- echo "BRANCHED_COMMIT=$BRANCHED_COMMIT"
- git diff $BRANCHED_COMMIT...${{ github.event.pull_request.head.sha }} > branch.diff;
- cat branch.diff;
- OUTPUT=${vendor/bin/phpcov patch-coverage --path-prefix $(pwd) ./gh-pages/${{ github.event.pull_request.head.sha }}/phpunit/phpunit.cov branch.diff || true}
- echo $OUTPUT;
- echo "$OUTPUT" >> coverage-comment.md
-
- - name: Add coverage PR comment
- uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
- if: ${{ matrix.php == '7.3' && github.event_name == 'pull_request' }}
- with:
- message-id: coverage-report
- message-path: coverage-comment.md
+ get-repo-name:
+ runs-on: ubuntu-latest
+ outputs:
+ repository-name: ${{ steps.repo-name.outputs.name }}
+ steps:
+ - name: Extract repository name
+ id: repo-name
+ run: echo "name=$(echo ${{ github.repository }} | cut -d'/' -f2)" >> $GITHUB_OUTPUT
+
+ codecoverage:
+ needs: get-repo-name
+ permissions:
+ contents: write
+ pull-requests: write
+ uses: newfold-labs/workflows/.github/workflows/reusable-codecoverage.yml@main
+ with:
+ php-versions: '["7.4", "8.0", "8.1", "8.2", "8.3", "8.4"]'
+ coverage-php-version: '7.4'
+ repository-name: ${{ needs.get-repo-name.outputs.repository-name }}
+ minimum-coverage: 25
+ secrets:
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index cdab30c2c..ea73427bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
.DS_Store
.vscode
.env
+.env.testing
/node_modules
/tests/_output
/tests/_support/_generated
diff --git a/codeception.dist.yml b/codeception.dist.yml
index 615b541f4..897635783 100644
--- a/codeception.dist.yml
+++ b/codeception.dist.yml
@@ -15,6 +15,9 @@ params:
coverage:
enabled: true
include:
- - /includes/*
-# - /upgrades/*
-bootstrap: bootstrap.php
+ - includes/*
+ exclude:
+ - tests/*
+ - vendor/*
+ - build/*
+ - src/*
diff --git a/phpunit.xml b/phpunit.xml
index 935c100a9..d91159405 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -8,7 +8,6 @@
>
-
diff --git a/tests/_data/.gitkeep b/tests/_data/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/phpunit/ColorTest.php b/tests/phpunit/ColorTest.php
new file mode 100644
index 000000000..d01f718ca
--- /dev/null
+++ b/tests/phpunit/ColorTest.php
@@ -0,0 +1,101 @@
+assertSame( 'Accent 1', $color->get_name() );
+ $this->assertSame( 'accent_1', $color->get_slug() );
+ $this->assertSame( '#F27121', $color->get_color() );
+ }
+
+ /**
+ * To_array returns expected keys.
+ *
+ * @return void
+ */
+ public function test_to_array() {
+ $color = new Color( 'Accent 1', 'accent_1', '#F27121' );
+ $arr = $color->to_array();
+ $this->assertSame(
+ array(
+ 'name' => 'Accent 1',
+ 'slug' => 'accent_1',
+ 'color' => '#F27121',
+ ),
+ $arr
+ );
+ }
+
+ /**
+ * From_array creates equivalent Color.
+ *
+ * @return void
+ */
+ public function test_from_array() {
+ $data = array(
+ 'name' => 'Accent 1',
+ 'slug' => 'accent_1',
+ 'color' => '#F27121',
+ );
+ $color = Color::from_array( $data );
+ $this->assertInstanceOf( Color::class, $color );
+ $this->assertSame( 'Accent 1', $color->get_name() );
+ $this->assertSame( 'accent_1', $color->get_slug() );
+ $this->assertSame( '#F27121', $color->get_color() );
+ }
+
+ /**
+ * From_array throws when keys are missing.
+ *
+ * @return void
+ */
+ public function test_from_array_throws_when_keys_missing() {
+ $this->expectException( \InvalidArgumentException::class );
+ $this->expectExceptionMessage( 'Array must contain name, slug, and color keys' );
+ Color::from_array(
+ array(
+ 'slug' => 'accent_1',
+ 'color' => '#F27121',
+ )
+ );
+ }
+
+ /**
+ * Constructor trims whitespace.
+ *
+ * @return void
+ */
+ public function test_trims_whitespace() {
+ $color = new Color( ' Accent 1 ', ' accent_1 ', ' #F27121 ' );
+ $this->assertSame( 'Accent 1', $color->get_name() );
+ $this->assertSame( 'accent_1', $color->get_slug() );
+ $this->assertSame( '#F27121', $color->get_color() );
+ }
+
+ /**
+ * Constructor throws when name is empty.
+ *
+ * @return void
+ */
+ public function test_throws_when_name_empty() {
+ $this->expectException( \InvalidArgumentException::class );
+ $this->expectExceptionMessage( 'Name cannot be empty' );
+ new Color( ' ', 'accent_1', '#F27121' );
+ }
+}
diff --git a/tests/phpunit/ExampleTest.php b/tests/phpunit/ExampleTest.php
deleted file mode 100644
index 8d03fb66e..000000000
--- a/tests/phpunit/ExampleTest.php
+++ /dev/null
@@ -1,15 +0,0 @@
-assertEquals( true, true );
- }
-
-}
diff --git a/tests/phpunit/ModuleLoadingTest.php b/tests/phpunit/ModuleLoadingTest.php
new file mode 100644
index 000000000..7f67eea4d
--- /dev/null
+++ b/tests/phpunit/ModuleLoadingTest.php
@@ -0,0 +1,25 @@
+assertTrue( class_exists( Types\Color::class ) );
+ $this->assertTrue( class_exists( Permissions::class ) );
+ $this->assertTrue( class_exists( Compatibility\Status::class ) );
+ $this->assertTrue( class_exists( RestApi\RestApi::class ) );
+ }
+}
diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php
index 963ed78a6..1cc43daa2 100644
--- a/tests/phpunit/bootstrap.php
+++ b/tests/phpunit/bootstrap.php
@@ -1,9 +1,13 @@
assertSame( 'Accent 1', $color->get_name() );
+ $this->assertSame( 'accent_1', $color->get_slug() );
+ $this->assertSame( '#F27121', $color->get_color() );
+ }
+
+ /**
+ * To_array returns expected keys.
+ *
+ * @return void
+ */
+ public function test_color_to_array() {
+ $color = new Color( 'Accent 1', 'accent_1', '#F27121' );
+ $arr = $color->to_array();
+ $this->assertSame(
+ array(
+ 'name' => 'Accent 1',
+ 'slug' => 'accent_1',
+ 'color' => '#F27121',
+ ),
+ $arr
+ );
+ }
+
+ /**
+ * From_array creates equivalent Color.
+ *
+ * @return void
+ */
+ public function test_color_from_array() {
+ $data = array(
+ 'name' => 'Accent 1',
+ 'slug' => 'accent_1',
+ 'color' => '#F27121',
+ );
+ $color = Color::from_array( $data );
+ $this->assertInstanceOf( Color::class, $color );
+ $this->assertSame( 'Accent 1', $color->get_name() );
+ $this->assertSame( 'accent_1', $color->get_slug() );
+ $this->assertSame( '#F27121', $color->get_color() );
+ }
+
+ /**
+ * From_array throws when name is missing.
+ *
+ * @return void
+ */
+ public function test_color_from_array_throws_when_keys_missing() {
+ $this->expectException( \InvalidArgumentException::class );
+ $this->expectExceptionMessage( 'Array must contain name, slug, and color keys' );
+ Color::from_array(
+ array(
+ 'slug' => 'accent_1',
+ 'color' => '#F27121',
+ )
+ );
+ }
+
+ /**
+ * Constructor trims whitespace.
+ *
+ * @return void
+ */
+ public function test_color_trims_whitespace() {
+ $color = new Color( ' Accent 1 ', ' accent_1 ', ' #F27121 ' );
+ $this->assertSame( 'Accent 1', $color->get_name() );
+ $this->assertSame( 'accent_1', $color->get_slug() );
+ $this->assertSame( '#F27121', $color->get_color() );
+ }
+
+ /**
+ * Constructor throws when name is empty.
+ *
+ * @return void
+ */
+ public function test_color_throws_when_name_empty() {
+ $this->expectException( \InvalidArgumentException::class );
+ $this->expectExceptionMessage( 'Name cannot be empty' );
+ new Color( ' ', 'accent_1', '#F27121' );
+ }
+}
diff --git a/tests/wpunit/CompatibilityStatusWPUnitTest.php b/tests/wpunit/CompatibilityStatusWPUnitTest.php
new file mode 100644
index 000000000..05c028359
--- /dev/null
+++ b/tests/wpunit/CompatibilityStatusWPUnitTest.php
@@ -0,0 +1,66 @@
+assertSame( 'unscanned', Status::$default );
+ }
+
+ /**
+ * Get() returns default when no option is set.
+ *
+ * @return void
+ */
+ public function test_get_returns_default_when_unset() {
+ Status::reset();
+ $this->assertSame( 'unscanned', Status::get() );
+ }
+
+ /**
+ * Get('all') returns stored data (default string when unset).
+ *
+ * @return void
+ */
+ public function test_get_all_returns_stored_data() {
+ Status::reset();
+ $data = Status::get( 'all' );
+ $this->assertSame( 'unscanned', $data );
+ }
+
+ /**
+ * Reset() removes stored option.
+ *
+ * @return void
+ */
+ public function test_reset_clears_option() {
+ $scan = new \stdClass();
+ $scan->result = 'compatible';
+ Status::set( $scan );
+ Status::reset();
+ $this->assertSame( 'unscanned', Status::get() );
+ }
+}
diff --git a/tests/wpunit/ExampleWPUnitTest.php b/tests/wpunit/ExampleWPUnitTest.php
deleted file mode 100644
index 08941487d..000000000
--- a/tests/wpunit/ExampleWPUnitTest.php
+++ /dev/null
@@ -1,12 +0,0 @@
-assertTrue( class_exists( Application::class ) );
+ $this->assertTrue( class_exists( ModuleController::class ) );
+ $this->assertTrue( class_exists( Permissions::class ) );
+ $this->assertTrue( class_exists( Status::class ) );
+ $this->assertTrue( class_exists( Scan::class ) );
+ $this->assertTrue( class_exists( ImageSideloadTaskManager::class ) );
+ $this->assertTrue( class_exists( ImageSideloadTask::class ) );
+ $this->assertTrue( class_exists( RestApi\RestApi::class ) );
+ }
+
+ /**
+ * Verify WordPress factory is available.
+ *
+ * @return void
+ */
+ public function test_wordpress_factory_available() {
+ $this->assertTrue( function_exists( 'get_option' ) );
+ $this->assertNotEmpty( get_option( 'blogname' ) );
+ }
+}
diff --git a/tests/wpunit/PermissionsWPUnitTest.php b/tests/wpunit/PermissionsWPUnitTest.php
new file mode 100644
index 000000000..29ead300e
--- /dev/null
+++ b/tests/wpunit/PermissionsWPUnitTest.php
@@ -0,0 +1,54 @@
+assertSame( 'manage_options', Permissions::ADMIN );
+ $this->assertSame( 'install_themes', Permissions::INSTALL_THEMES );
+ $this->assertSame( 'edit_themes', Permissions::EDIT_THEMES );
+ }
+
+ /**
+ * Rest_is_authorized_admin returns false when not logged in.
+ *
+ * @return void
+ */
+ public function test_rest_is_authorized_admin_when_logged_out() {
+ wp_set_current_user( 0 );
+ $this->assertFalse( Permissions::rest_is_authorized_admin() );
+ }
+
+ /**
+ * Rest_is_authorized_admin returns true for admin user.
+ *
+ * @return void
+ */
+ public function test_rest_is_authorized_admin_when_admin() {
+ $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
+ wp_set_current_user( $user_id );
+ $this->assertTrue( Permissions::rest_is_authorized_admin() );
+ }
+
+ /**
+ * Custom_post_authorized_admin requires edit_posts and manage_options.
+ *
+ * @return void
+ */
+ public function test_custom_post_authorized_admin_for_administrator() {
+ $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
+ wp_set_current_user( $user_id );
+ $this->assertTrue( Permissions::custom_post_authorized_admin() );
+ }
+}
diff --git a/tests/wpunit/RestApiWPUnitTest.php b/tests/wpunit/RestApiWPUnitTest.php
new file mode 100644
index 000000000..41609f5b3
--- /dev/null
+++ b/tests/wpunit/RestApiWPUnitTest.php
@@ -0,0 +1,32 @@
+get_routes();
+ $found = array_filter(
+ array_keys( $routes ),
+ function ( $route ) {
+ return strpos( $route, 'newfold-onboarding' ) !== false;
+ }
+ );
+ $this->assertNotEmpty( $found, 'Expected newfold-onboarding routes to be registered' );
+ }
+}
diff --git a/tests/wpunit/_bootstrap.php b/tests/wpunit/_bootstrap.php
index b3d9bbc7f..b4f91d94e 100644
--- a/tests/wpunit/_bootstrap.php
+++ b/tests/wpunit/_bootstrap.php
@@ -1 +1,16 @@