Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4182e74
chore(phpunit): instrument CI with JUnit + coverage artifacts, record…
jakejackson1 May 25, 2026
0e71520
chore(phpunit): port coverage-baseline script from Python to PHP
jakejackson1 May 25, 2026
e1ce7a6
docs(phpunit): refresh Playwright baseline after building dist/ + vendor
jakejackson1 May 25, 2026
e7bee35
chore(phpunit): add shared TestCase + traits, pilot on Statics
jakejackson1 May 25, 2026
f971436
chore(phpunit): drop premature AjaxTestCase + unused traits
jakejackson1 May 25, 2026
22e946b
chore(phpunit): drain unit-tests/ into integration/ mirroring src/
jakejackson1 May 25, 2026
4294be8
chore(phpunit): adopt GF_UnitTest_Factory, drain GFAPI::add_* calls
jakejackson1 May 25, 2026
accbe3c
chore(phpunit): drop redundant @return in UsesFactory
jakejackson1 May 25, 2026
340235d
ci(phpunit): enforce Phase 0 coverage floor on every PR
jakejackson1 May 25, 2026
dd73a59
chore(phpunit): drop unused min-percent override in coverage-gate
jakejackson1 May 25, 2026
8578e18
test(phpunit): fill Exceptions hierarchy + Statics coverage gaps
jakejackson1 May 25, 2026
ca38d9e
test(phpunit): characterize 11 previously untested controllers
jakejackson1 May 25, 2026
703b125
chore(phpunit): trim slop from controller characterization tests
jakejackson1 May 25, 2026
1405795
test(phpunit): fill Model coverage gaps in Install/Uninstall/Template…
jakejackson1 May 25, 2026
6a454d1
chore(phpunit): drop before/after dance from multisite early-return test
jakejackson1 May 25, 2026
41a2939
chore(phpunit): drop assertTrue(true) coverage padding from character…
jakejackson1 May 25, 2026
a7e9364
test(phpunit): characterize 49 previously untested Helper classes
jakejackson1 May 25, 2026
fb64c79
chore(phpunit): drop license-header slop from 11 Helper test files
jakejackson1 May 25, 2026
ad127b4
test(phpunit): characterize previously untested Views
jakejackson1 May 25, 2026
b2cce9e
test(phpunit): mirror Helper/Log + Helper/Mpdf subdirs in tests
jakejackson1 May 25, 2026
db2f9ff
fix(phpunit): grant super-admin so multisite check_install_status tes…
jakejackson1 May 25, 2026
238bbc4
ci(phpunit): ratchet coverage floor from 76.33% to 79.67%
jakejackson1 May 25, 2026
bc3d8f1
test(phpunit): close Helper/Licensing, Helper_Abstract_Addon, Model_P…
jakejackson1 May 25, 2026
15370cb
ci(phpunit): ratchet coverage floor from 79.67% to 80.06%
jakejackson1 May 25, 2026
74cd897
test(phpunit): close Helper/Licensing wp_die + cache-key branches
jakejackson1 May 25, 2026
224c0e9
test(phpunit): close Model_PDF, Logger, and bootstrap coverage gaps
jakejackson1 May 25, 2026
99a7549
ci(phpunit): union single-site + multisite clovers in coverage gate
jakejackson1 May 25, 2026
f3a6257
refactor(phpunit): add class-scoped fixture loader (Phase A of fixtur…
jakejackson1 May 25, 2026
5076d29
test(phpunit): migrate Test_Field_Section to class-scoped fixture (Ph…
jakejackson1 May 25, 2026
350dc89
test(phpunit): migrate View/ to class-scoped fixtures (Phase C, subdi…
jakejackson1 May 25, 2026
a80bbea
test(phpunit): migrate Controller/ to class-scoped fixtures (Phase C,…
jakejackson1 May 25, 2026
5586953
test(phpunit): migrate Model/ to class-scoped fixtures (Phase C, subd…
jakejackson1 May 26, 2026
434e342
test(phpunit): migrate Helper/Fields/ to class-scoped fixtures (Phase…
jakejackson1 May 26, 2026
b771ad8
test(phpunit): migrate Helper/ (non-Fields) to class-scoped fixtures …
jakejackson1 May 26, 2026
8edee42
test(phpunit): migrate root files to class-scoped fixtures (Phase C, …
jakejackson1 May 26, 2026
c062ae8
style(phpunit): trim Test_Form_Data holdout comment
jakejackson1 May 26, 2026
284346b
test(phpunit): migrate Test_Form_Data via upload-path rewrite (Phase …
jakejackson1 May 26, 2026
a78538a
test(phpunit): remove legacy fixture global + add CI grep gate (Phase…
jakejackson1 May 26, 2026
1850b98
style(phpunit): trim slop comments in Phase D additions
jakejackson1 May 26, 2026
0870f40
test(phpunit): retire slow-pdf-processes group annotation
jakejackson1 May 26, 2026
adbbd71
test(phpunit): fix order-coupled failures under --order-by=random
jakejackson1 May 26, 2026
15925ae
test(phpunit): stop two pre-existing flakes uncovered by random order
jakejackson1 May 26, 2026
63279ca
style(phpunit): trim slop from order-coupling fixes
jakejackson1 May 26, 2026
28fc981
test(phpunit): fix cross-invocation filesystem leaks in template tests
jakejackson1 May 26, 2026
2c9cba0
test(phpunit): force plain permalinks in test_get_pdf_url_no_perma
jakejackson1 May 26, 2026
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
67 changes: 61 additions & 6 deletions .github/workflows/phpunit.tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6

# Fixtures must go through static::load_fixtures() on HasGfpdfFixtures;
# the shared $GLOBALS['GFPDF_Test'] global and direct GFAPI form/entry
# creates are gone, and this gate prevents them from coming back.
- name: Fail on banned fixture patterns
run: |
if grep -rnE "\\\$GLOBALS\\[['\\\"]GFPDF_Test['\\\"]\\]|GFAPI::add_(form|entry)\\b" tests/phpunit/integration/ --include="*.php"; then
echo "::error::Banned fixture pattern found in tests/phpunit/integration/. Use static::load_fixtures() instead."
exit 1
fi

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
Expand Down Expand Up @@ -110,24 +120,69 @@ jobs:

- name: Install / Setup Gravity PDF + WordPress
if: ${{ matrix.report }}
run: yarn wp-env:integration start --xdebug=debug
run: yarn wp-env:integration start --xdebug=coverage

- name: Run PHPUnit tests
if: ${{ ! matrix.report }}
run: |
yarn test:php --do-not-cache-result --verbose
yarn test:php --do-not-cache-result --verbose --log-junit=/var/www/html/wp-content/plugins/gravity-pdf/tmp/junit/phpunit-integration-php${{ matrix.php }}.xml

# Multisite suite reuses the already-running php 8.3 container instead of
# spinning up its own matrix cell + cold wp-env start.
- name: Run Multisite PHPUnit tests
if: ${{ matrix.php == '8.3' && ! matrix.report }}
run: |
yarn test:php:multisite --verbose
yarn test:php:multisite --verbose --log-junit=/var/www/html/wp-content/plugins/gravity-pdf/tmp/junit/phpunit-multisite-php${{ matrix.php }}.xml

- name: Generate Code Coverage Report for PHP
# The yarn test:php wrapper fails under xdebug coverage with
# "RecursiveDirectoryIterator on src/templates" (PHPUnit 9.6 + Xdebug 3
# working-dir resolution). Invoke phpunit directly inside the container.
- name: Generate Code Coverage Report for PHP (single-site)
if: ${{ matrix.report }}
run: |
yarn wp-env:integration run wordpress bash -c '
cd /var/www/html/wp-content/plugins/gravity-pdf &&
vendor/bin/phpunit \
-c tools/phpunit/config.xml \
--do-not-cache-result \
--verbose \
--log-junit=tmp/junit/phpunit-coverage-php${{ matrix.php }}.xml \
--coverage-clover=tmp/coverage/report-xml/php-coverage1.xml
'

- name: Generate Code Coverage Report for PHP (multisite)
if: ${{ matrix.report }}
run: |
yarn wp-env:integration run wordpress bash -c '
cd /var/www/html/wp-content/plugins/gravity-pdf &&
vendor/bin/phpunit \
-c tools/phpunit/config-multisite.xml \
--do-not-cache-result \
--verbose \
--log-junit=tmp/junit/phpunit-coverage-multisite-php${{ matrix.php }}.xml \
--coverage-clover=tmp/coverage/report-xml/php-coverage-multisite.xml
'

- name: Enforce coverage floor
if: ${{ matrix.report }}
run: |
yarn test:php --do-not-cache-result --verbose --coverage-clover=/var/www/html/wp-content/plugins/gravity-pdf/tmp/coverage/report-xml/php-coverage1.xml
run: |
php tools/phpunit/coverage-gate.php \
tmp/coverage/report-xml/php-coverage1.xml \
tmp/coverage/report-xml/php-coverage-multisite.xml

- name: Upload PHPUnit JUnit timings
uses: actions/upload-artifact@v6
if: always()
with:
name: phpunit-junit-php${{ matrix.php }}${{ matrix.report && '-coverage' || '' }}
path: tmp/junit/*.xml

- name: Upload PHPUnit coverage XML
uses: actions/upload-artifact@v6
if: ${{ matrix.report && always() }}
with:
name: phpunit-coverage-clover
path: tmp/coverage/report-xml/*.xml

- name: Code Coverage Upload
uses: codecov/codecov-action@v6
Expand Down
348 changes: 348 additions & 0 deletions tests/phpunit/COVERAGE_BASELINE.md

Large diffs are not rendered by default.

157 changes: 157 additions & 0 deletions tests/phpunit/Concerns/HasGfpdfFixtures.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

declare(strict_types=1);

namespace GFPDF\Tests\Concerns;

/**
* Class-scoped form/entry fixtures plus ergonomic accessors.
*
* Each test class declares the fixtures it needs in set_up_before_class() via
* load_fixtures(), reads them via $this->form() / $this->entry() / $this->entries(),
* and inherits cleanup via tear_down_after_class() (wired in TestCase / AjaxTestCase).
*/
trait HasGfpdfFixtures {

/**
* Form key → entry-fixture filename. The original bootstrap mixed
* -entries.json and -entry.json suffixes, so a map is the source of truth.
*/
private static $entry_filenames = [
'all-form-fields' => 'all-form-fields-entries.json',
'gravityform-1' => 'gravityform-1-entries.json',
'repeater-empty-form' => 'repeater-empty-entry.json',
'repeater-consent-form' => 'repeater-consent-entry.json',
'non-group-products-form' => 'non-group-products-form-entries.json',
];

/**
* Per-class fixture cache, keyed by class name (late static binding).
*
* Shape: [ 'Test_Foo' => [ 'forms' => [ key => array ], 'entries' => [ key => array[] ] ] ].
*
* Protected (not private) so subclasses can patch entries after load_fixtures
* — e.g. Test_Form_Data rewrites file-upload URLs to match the per-class form's
* upload directory before tests run.
*/
protected static $fixture_caches = [];

/**
* Class-scoped fixture loader. Call from set_up_before_class().
*
* @param string[] $forms Form keys; each loads tools/phpunit/data/forms/<key>.json.
* @param string[] $entries Entry-set keys; the parent form must be in $forms
* (entries are created against the just-loaded form's ID).
*/
protected static function load_fixtures( array $forms = [], array $entries = [] ) {
$factory = new \GF_UnitTest_Factory();
$class = static::class;
$cache = self::$fixture_caches[ $class ] ?? [ 'forms' => [], 'entries' => [] ];

foreach ( $forms as $key ) {
$cache['forms'][ $key ] = $factory->form->import_fixture_and_get( "$key.json" );
}

foreach ( $entries as $key ) {
if ( ! isset( $cache['forms'][ $key ] ) ) {
throw new \LogicException( "Cannot load entry set '$key' before its parent form." );
}
if ( ! isset( self::$entry_filenames[ $key ] ) ) {
throw new \LogicException( "No entry fixture mapping for '$key'." );
}

$cache['entries'][ $key ] = $factory->entry->import_many_and_get(
self::$entry_filenames[ $key ],
$cache['forms'][ $key ]['id']
);
}

self::$fixture_caches[ $class ] = $cache;
}

/**
* Deletes class-scoped fixtures from the database. Call from tear_down_after_class().
*
* Forms+entries created by load_fixtures() outlive WP's per-test transaction
* (GFAPI writes go to non-transactional tables), so without this each class
* leaks its fixtures into subsequent classes.
*/
protected static function cleanup_class_fixtures() {
$class = static::class;
if ( ! isset( self::$fixture_caches[ $class ] ) ) {
return;
}
$cache = self::$fixture_caches[ $class ];

foreach ( $cache['entries'] as $entries ) {
foreach ( $entries as $entry ) {
\GFAPI::delete_entry( $entry['id'] );
}
}
foreach ( $cache['forms'] as $form ) {
\GFAPI::delete_form( $form['id'] );
}
unset( self::$fixture_caches[ $class ] );
}

/**
* Returns the form fixture stored under $key (declared via load_fixtures).
*
* @param string $key Form key.
*
* @return array
*/
protected function form( $key ) {
$cache = self::$fixture_caches[ static::class ]['forms'] ?? [];
if ( ! isset( $cache[ $key ] ) ) {
$available = implode( ', ', array_keys( $cache ) ) ?: '(none)';
$this->fail( "Form fixture '$key' is not loaded. Available in " . static::class . ": $available" );
}

return $cache[ $key ];
}

/**
* Returns one of the entry fixtures stored under $key.
*
* @param string $key Entry-set key (same key as the parent form).
* @param int $index Zero-based index into the entry list.
*
* @return array
*/
protected function entry( $key, $index = 0 ) {
$cache = self::$fixture_caches[ static::class ]['entries'] ?? [];
if ( ! isset( $cache[ $key ][ $index ] ) ) {
$this->fail( "Entry fixture '$key'[$index] is not loaded in " . static::class . '.' );
}

return $cache[ $key ][ $index ];
}

/**
* Returns the full entry list for $key (for foreach/array_column use).
*
* @param string $key Entry-set key.
*
* @return array[]
*/
protected function entries( $key ) {
$cache = self::$fixture_caches[ static::class ]['entries'] ?? [];
if ( ! isset( $cache[ $key ] ) ) {
$this->fail( "Entry fixture set '$key' is not loaded in " . static::class . '.' );
}

return $cache[ $key ];
}

/**
* Returns the Gravity PDF Router (DI container).
*
* @return \GFPDF\Router
*/
protected function gfpdf() {
global $gfpdf;

return $gfpdf;
}
}
25 changes: 25 additions & 0 deletions tests/phpunit/Concerns/UsesFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace GFPDF\Tests\Concerns;

use GF_UnitTest_Factory;

/**
* Exposes the existing GF_UnitTest_Factory (tools/phpunit/gravityforms-factory.php).
* Used in place of direct GFAPI::add_form() / add_entry() calls in test bodies.
*/
trait UsesFactory {

/** @var GF_UnitTest_Factory|null */
private $gfpdf_factory;

protected function gf_factory(): GF_UnitTest_Factory {
if ( null === $this->gfpdf_factory ) {
$this->gfpdf_factory = new GF_UnitTest_Factory();
}

return $this->gfpdf_factory;
}
}
Loading