From 4182e7444a68af19456a509f1a3faa62223fad1c Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 14:12:56 +1000 Subject: [PATCH 01/45] chore(phpunit): instrument CI with JUnit + coverage artifacts, record baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 of the PHPUnit test refactor — set up measurement infrastructure so subsequent phases (structure normalization, factory adoption, coverage 1:1 mirror) can be diffed against a real baseline rather than the stale .phpunit.result.cache. Workflow: every PHPUnit invocation now passes --log-junit, and two actions/upload-artifact@v6 steps upload the JUnit XML (every matrix cell) and the Clover XML (coverage cell). Existing codecov upload preserved. Baseline doc (tests/phpunit/COVERAGE_BASELINE.md) records live numbers from wp-env:integration: 1119 tests · 4088 assertions · 41.69s wall, 76.33% overall line coverage. Includes the per-src/-subdir breakdown, the "9 AJAX tests = 69% of total runtime" finding that informs phase 2, and a methodology section so future phases can reproduce the numbers. Tool (tools/phpunit/coverage-baseline.py) regenerates the per-subdir table from any Clover XML for regression checking. Two side-discoveries documented for a Phase 4 follow-up: - CI coverage cell uses --xdebug=debug, but coverage requires xdebug.mode=coverage — codecov upload has likely been receiving no data. - yarn test:php --coverage-clover fails under Xdebug 3 with a "RecursiveDirectoryIterator on src/templates" error; direct vendor/bin/phpunit works. Workaround documented. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/phpunit.tests.yml | 22 +++- tests/phpunit/COVERAGE_BASELINE.md | 194 ++++++++++++++++++++++++++++ tools/phpunit/coverage-baseline.py | 70 ++++++++++ 3 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 tests/phpunit/COVERAGE_BASELINE.md create mode 100644 tools/phpunit/coverage-baseline.py diff --git a/.github/workflows/phpunit.tests.yml b/.github/workflows/phpunit.tests.yml index 9424ecab3..3b7f3e045 100644 --- a/.github/workflows/phpunit.tests.yml +++ b/.github/workflows/phpunit.tests.yml @@ -115,19 +115,33 @@ jobs: - 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 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: | + yarn test:php --do-not-cache-result --verbose --log-junit=/var/www/html/wp-content/plugins/gravity-pdf/tmp/junit/phpunit-coverage-php${{ matrix.php }}.xml --coverage-clover=/var/www/html/wp-content/plugins/gravity-pdf/tmp/coverage/report-xml/php-coverage1.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 diff --git a/tests/phpunit/COVERAGE_BASELINE.md b/tests/phpunit/COVERAGE_BASELINE.md new file mode 100644 index 000000000..6a111d3dc --- /dev/null +++ b/tests/phpunit/COVERAGE_BASELINE.md @@ -0,0 +1,194 @@ +# PHPUnit Test Suite Baseline + +Captured: 2026-05-25 (Phase 0 of [`.claude/plans/2026-05-25-phpunit-tests-refactor.md`](../../.claude/plans/2026-05-25-phpunit-tests-refactor.md)). + +This file is the reference point every later phase compares against. Treat the numbers below as the line that must not regress (test count, coverage), and the runtime as the budget later phases should match within ±10%. + +## Runtime baseline (live wp-env:integration, no xdebug, no coverage) + +| Metric | Value | +| --- | --- | +| Test count | **1119** | +| Assertions | **4088** | +| Skipped | 8 (multisite-only tests, expected) | +| Sum of test-case times | **38.46s** | +| Wall-clock (incl. PHPUnit boot) | **41.69s** | +| Wrapper-inclusive (yarn + docker exec) | 42.63s | + +Slowest 15 tests (live JUnit, `tmp/junit/phpunit-integration.xml`): + +| Time | Test | +| ---: | :--- | +| 4.032s | `Test_PDF_Ajax::test_ajax_process_uploaded_template` | +| 3.570s | `Test_PDF_Ajax::test_render_template_fields` | +| 3.428s | `Test_PDF_Ajax::test_delete_gf_pdf_setting` | +| 3.161s | `Test_PDF_Ajax::test_ajax_process_license_deactivation` | +| 3.153s | `Test_PDF_Ajax::test_ajax_process_build_template_options_html` | +| 3.125s | `Test_PDF_Ajax::test_duplicate_gf_pdf_settings` | +| 3.120s | `Test_PDF_Ajax::test_ajax_save_core_font` | +| 3.098s | `Test_PDF_Ajax::test_ajax_process_delete_template` | +| 1.366s | `Test_EDD_SL_Plugin_Updater::test_check_update_already_exists` | +| 1.192s | `Test_Request::test_send_request_status_error` | +| 1.008s | `Test_Url_Signer::test_expiration_failure` (intentional `sleep(1)`) | +| 0.766s | `Test_Request::test_send_request_success` | +| 0.641s | `Test_Slow_PDF_Processes::test_process_legacy_pdf_endpoint` | +| 0.496s | `Test_Slow_PDF_Processes::test_process_pdf_endpoint` | +| 0.455s | `Test_Slow_PDF_Processes::test_generate_and_save_pdf` | + +**Headline finding**: the 9 `Test_PDF_Ajax` tests account for **26.70s — 69% of total suite runtime**. Phase 2's split of `test-ajax.php` into focused `*_Ajax.php` files (one per target Model) is the highest-leverage perf work in the refactor. + +## Per-namespace runtime breakdown + +| Namespace | Time | Tests | Avg/test | +| :--- | ---: | ---: | ---: | +| `GFPDF\Tests\` (root `test-*.php`) | 33.58s | 753 | 44.6ms | +| `GFPDF\Helper\` | 3.90s | 102 | 38.2ms | +| `GFPDF\Controller\` | 0.81s | 31 | 26.0ms | +| `GFPDF\Model\` | 0.15s | 138 | 1.1ms | +| `GFPDF\Statics\` | 0.03s | 90 | 0.3ms | +| `GFPDF\View\` | 0.00s | 5 | 0.3ms | + +`Helper/` subdivision: + +| Sub-namespace | Time | Tests | +| :--- | ---: | ---: | +| `Helper/Mpdf` | 2.13s | 8 | +| `Helper/Licensing` | 1.42s | 19 | +| `Helper/Fields` | 0.33s | 49 | +| `Helper/Log` | 0.01s | 20 | +| `Helper/(top-level)` | 0.01s | 5 | +| `Helper/Fonts` | 0.00s | 1 | + +Heaviest cross-cutting files (root `test-*.php`, sorted by total time): + +| File | Time | Tests | +| :--- | ---: | ---: | +| `Test_PDF_Ajax` | 26.70s | 9 | +| `Test_Slow_PDF_Processes` | 3.76s | 18 | +| `Test_Url_Signer` | 1.02s | 23 | +| `Test_Rest_Form_Settings` | 0.64s | 31 | +| `Test_PDF` | 0.24s | 75 | +| `Test_Options_API` | 0.11s | 81 | +| `Test_Helper_Misc` | 0.03s | 73 | + +(Full file-level breakdown reproducible from `tmp/junit/phpunit-integration.xml` via the methodology section below.) + +## Multisite runtime baseline + +Source: `tmp/junit/phpunit-multisite.xml`, captured via `yarn test:php:multisite`. + +| Metric | Value | +| --- | --- | +| Test count | **1119** (same surface, different bootstrap) | +| Assertions | 4119 | +| Skipped | 1 (non-multisite-only test, expected) | +| Sum of test-case times | **38.64s** | +| Wall-clock | 41.81s | + +## Coverage baseline (live wp-env:integration, xdebug coverage mode, PHP 8.5) + +Source: `tmp/coverage/report-xml/baseline.xml` (Clover format, 845 KB, 208 files). + +| `src/` subdirectory | Files | Statements covered / total | Line coverage | Phase 4 priority | +| :--- | ---: | :--- | ---: | :--- | +| `src/Rest/` | 2 | 546 / 588 | **92.86%** | Already strong — no new work | +| `src/Helper/` (top level) | 39 | 3368 / 4026 | **83.66%** | Mixed — abstracts critical, fill targeted gaps | +| `src/Statics/` | 4 | 287 / 347 | **82.71%** | Fill `Debug.php`, `Queue_Callbacks.php` | +| `src/Helper/Fonts/` | 5 | 31 / 38 | **81.58%** | Already strong | +| `src/Controller/` | 19 | 896 / 1100 | **81.45%** | High — 11 of 19 still without dedicated tests | +| `src/Model/` | 11 | 1938 / 2385 | **81.26%** | High — `Model_PDF` characterization (per plan §"Critical-class characterization tests") | +| `src/Helper/Mpdf/` | 3 | 33 / 44 | **75.00%** | Low — small surface | +| `src/Helper/Fields/` | 60 | 1296 / 1784 | **72.65%** | High — sparse coverage across ~60 field handlers | +| `src/Helper/Log/` | 3 | 118 / 170 | **69.41%** | Medium | +| `src/View/` | 35 | 646 / 1052 | **61.41%** | Out of scope — mostly HTML partials | +| `src/Helper/Licensing/` | 1 | 168 / 298 | **56.38%** | Medium — large untested branches | +| `src/` root (`bootstrap.php`, `autoload.php`, `deprecated.php`) | 3 | 290 / 557 | **52.06%** | Low — bootstrap has activation paths hard to cover | +| `src/templates/` | 9 | 97 / 307 | **31.60%** | Out of scope — PDF templates, not code-under-test | +| `src/Exceptions/` | 11 | 5 / 22 | **22.73%** | Per-plan single hierarchy smoke test | +| Plugin root (`pdf.php`, `api.php`, `gravity-pdf-updater.php`) | 3 | 209 / 289 | **72.32%** | Mixed | +| **OVERALL** | **208** | **9928 / 13007** | **76.33%** | — | + +The **76.33%** overall is the CI gate that Phase 4 introduces: `coverage ≥ 76.33%` per PR, ratcheted upward quarterly. + +Coverage runtime overhead is modest in xdebug `coverage` mode: 47s (vs 38s without) — only ~24% slower. + +> **Important methodology note** — `yarn test:php --coverage-clover=...` consistently fails on this codebase with `RecursiveDirectoryIterator::__construct(.../src/templates): Failed to open directory` when invoked through the yarn wrapper, even when `src/templates/` exists and is readable. **Invoking `vendor/bin/phpunit` directly inside the container works.** Suspected cause is a working-directory resolution quirk in PHPUnit 9.6 + Xdebug 3 coverage when the config path is absolute. This affects the CI workflow's coverage cell too (`.github/workflows/phpunit.tests.yml`); the Phase 4 CI coverage gate will need to switch the coverage step to the direct-phpunit form documented below. Filed as a follow-up for Phase 4. + +## Playwright (e2e) baseline + +| Metric | Value (local 2026-05-25) | +| --- | --- | +| Passed | 6 | +| Failed | 60 | +| Did not run | 21 | +| Wall-clock | 8.4m | + +The local run is **not green**, but the failures look environmental (post-`dist/` rebuild, fresh wp-env, etc.) rather than refactor-driven — the PHPUnit refactor has not touched any code yet. For the authoritative Playwright baseline, **compare future PRs against the GitHub Actions Playwright workflow artifacts** (`.github/workflows/playwright-e2e.yml`) on the same commit, not the local run. + +This is recorded only so a contributor on a fresh machine doesn't conclude their environment is broken when they see the same numbers. Phase 2 changes touch fixture bootstrap; before merging Phase 2 verify Playwright in CI matches the pre-refactor CI baseline. + +## Methodology — how to reproduce + +The numbers above are captured from a **live** wp-env run, not from `.phpunit.result.cache`. The cache file is stale almost the moment it lands; for regression detection, always re-run. + +Boot the environments once per session: + +```bash +yarn wp-env:integration start # port 8701, used by yarn test:php +yarn wp-env:e2e start # port 8702, used by yarn test:e2e +``` + +### Runtime + JUnit + +```bash +yarn test:php \ + --do-not-cache-result \ + --verbose \ + --log-junit=/var/www/html/wp-content/plugins/gravity-pdf/tmp/junit/phpunit-integration.xml +``` + +The Docker container mounts the plugin directory, so `tmp/junit/phpunit-integration.xml` appears on the host. Parse it with `xml.etree.ElementTree` to recompute the per-namespace and slowest-test tables above. + +### Multisite + +```bash +yarn test:php:multisite \ + --verbose \ + --log-junit=/var/www/html/wp-content/plugins/gravity-pdf/tmp/junit/phpunit-multisite.xml +``` + +### Coverage (requires xdebug `coverage` mode — restart wp-env first) + +```bash +# Note: --xdebug=debug is NOT enough; coverage needs xdebug.mode=coverage. +yarn wp-env:integration start --xdebug=coverage + +# The yarn wrapper produces a "RecursiveDirectoryIterator on src/templates" +# failure under xdebug 3 coverage — invoke phpunit directly: +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 \ + --coverage-clover=tmp/coverage/report-xml/baseline.xml \ + --log-junit=tmp/junit/phpunit-coverage.xml +' +``` + +Per-`src/`-subdir coverage is extracted from the Clover XML — every `` element has a `name` attribute (the full absolute path, despite the attribute name) and a `` child with `statements`/`coveredstatements`/`elements`/`coveredelements`. Group by the first path segment under `src/`, with `Helper/` broken out one level deeper. Run `tools/phpunit/coverage-baseline.py` to regenerate the per-subdir table above from the Clover XML. + +### Playwright + +```bash +yarn test:e2e +``` + +### Comparing a future run against this baseline + +After running any of the above, diff the new JUnit/clover against the artifacts uploaded by CI (workflow `.github/workflows/phpunit.tests.yml`, artifacts `phpunit-junit-php8.3` and `phpunit-coverage-clover`). A regression is: + +- Runtime: more than +10% on the sum of test-case times. +- Test count: any decrease (a moved test is still a test). +- Coverage: any decrease in overall line coverage, or any decrease in per-`src/`-subdir coverage by more than 1 percentage point. + +If a phase intentionally trades runtime for clarity (e.g., splitting `test-ajax.php` adds per-class setup overhead), record the new baseline here in a "phase N revision" section rather than relaxing the gate. diff --git a/tools/phpunit/coverage-baseline.py b/tools/phpunit/coverage-baseline.py new file mode 100644 index 000000000..502d8c41a --- /dev/null +++ b/tools/phpunit/coverage-baseline.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Print a per-`src/`-subdir coverage breakdown from a PHPUnit Clover XML. + +Usage: python3 tools/phpunit/coverage-baseline.py [path/to/clover.xml] + +Default path: tmp/coverage/report-xml/baseline.xml. See +tests/phpunit/COVERAGE_BASELINE.md for how to produce the input XML. +""" + +import sys +import xml.etree.ElementTree as ET +from collections import defaultdict + + +def bucket_for(path): + if path.endswith(('/pdf.php', '/api.php', '/gravity-pdf-updater.php')): + return 'Plugin root' + if '/src/' not in path: + return None + rel = path.split('/src/', 1)[1] + parts = rel.split('/') + if len(parts) == 1: + return 'src/ root' + if parts[0] == 'Helper' and len(parts) >= 3: + return f'Helper/{parts[1]}' + return parts[0] + + +def main(xml_path): + tree = ET.parse(xml_path) + buckets = defaultdict( + lambda: {'st': 0, 'covst': 0, 'el': 0, 'covel': 0, 'files': 0} + ) + overall = {'st': 0, 'covst': 0, 'el': 0, 'covel': 0} + + for f in tree.iter('file'): + bucket = bucket_for(f.get('name', '')) + if bucket is None: + continue + m = next((c for c in f if c.tag == 'metrics'), None) + if m is None: + continue + st, covst = int(m.get('statements', '0')), int(m.get('coveredstatements', '0')) + el, covel = int(m.get('elements', '0')), int(m.get('coveredelements', '0')) + b = buckets[bucket] + b['st'] += st; b['covst'] += covst; b['el'] += el; b['covel'] += covel + b['files'] += 1 + overall['st'] += st; overall['covst'] += covst + overall['el'] += el; overall['covel'] += covel + + def pct(c, t): + return (c / t * 100) if t else 0.0 + + print(f"{'Bucket':28s} {'Files':>5s} {'Stmts':>11s} {'Stmt %':>7s} {'Elem %':>7s}") + print('-' * 70) + for b in sorted(buckets): + d = buckets[b] + print(f"{b:28s} {d['files']:5d} {d['covst']:5d}/{d['st']:<5d} " + f"{pct(d['covst'], d['st']):6.2f}% {pct(d['covel'], d['el']):6.2f}%") + print('-' * 70) + files_total = sum(b['files'] for b in buckets.values()) + print(f"{'OVERALL':28s} {files_total:5d} " + f"{overall['covst']:5d}/{overall['st']:<5d} " + f"{pct(overall['covst'], overall['st']):6.2f}% " + f"{pct(overall['covel'], overall['el']):6.2f}%") + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else 'tmp/coverage/report-xml/baseline.xml')) From 0e715208b71f47c5df80087d77dafe6e01ae872c Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 14:21:17 +1000 Subject: [PATCH 02/45] chore(phpunit): port coverage-baseline script from Python to PHP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Phase 0 commit (4182e744) shipped the baseline regenerator as Python, which introduced a foreign runtime dependency for tooling that lives inside a PHP project — `tools/phpunit/` already houses PHP (factory, bootstrap, wp-tests-config). SimpleXML covers the parsing 1:1, so there's no reason to require python3 just to read a Clover XML. Output verified identical to the Python original against the same baseline.xml: 208 files, 9928/13007 statements, 76.33% — every per-bucket row matches the table already recorded in COVERAGE_BASELINE.md. Updated the methodology section to invoke `php tools/phpunit/coverage-baseline.php`. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/phpunit/COVERAGE_BASELINE.md | 2 +- tools/phpunit/coverage-baseline.php | 96 +++++++++++++++++++++++++++++ tools/phpunit/coverage-baseline.py | 70 --------------------- 3 files changed, 97 insertions(+), 71 deletions(-) create mode 100644 tools/phpunit/coverage-baseline.php delete mode 100644 tools/phpunit/coverage-baseline.py diff --git a/tests/phpunit/COVERAGE_BASELINE.md b/tests/phpunit/COVERAGE_BASELINE.md index 6a111d3dc..a53f55615 100644 --- a/tests/phpunit/COVERAGE_BASELINE.md +++ b/tests/phpunit/COVERAGE_BASELINE.md @@ -175,7 +175,7 @@ yarn wp-env:integration run wordpress bash -c ' ' ``` -Per-`src/`-subdir coverage is extracted from the Clover XML — every `` element has a `name` attribute (the full absolute path, despite the attribute name) and a `` child with `statements`/`coveredstatements`/`elements`/`coveredelements`. Group by the first path segment under `src/`, with `Helper/` broken out one level deeper. Run `tools/phpunit/coverage-baseline.py` to regenerate the per-subdir table above from the Clover XML. +Per-`src/`-subdir coverage is extracted from the Clover XML — every `` element has a `name` attribute (the full absolute path, despite the attribute name) and a `` child with `statements`/`coveredstatements`/`elements`/`coveredelements`. Group by the first path segment under `src/`, with `Helper/` broken out one level deeper. Run `php tools/phpunit/coverage-baseline.php` to regenerate the per-subdir table above from the Clover XML. ### Playwright diff --git a/tools/phpunit/coverage-baseline.php b/tools/phpunit/coverage-baseline.php new file mode 100644 index 000000000..1875b352b --- /dev/null +++ b/tools/phpunit/coverage-baseline.php @@ -0,0 +1,96 @@ += 3 ) { + return 'Helper/' . $parts[1]; + } + return $parts[0]; +} + +$xml_path = $argv[1] ?? 'tmp/coverage/report-xml/baseline.xml'; +$xml = simplexml_load_file( $xml_path ); +if ( false === $xml ) { + fwrite( STDERR, "Failed to parse $xml_path\n" ); + exit( 1 ); +} + +$buckets = []; +$overall = [ 'st' => 0, 'covst' => 0, 'el' => 0, 'covel' => 0 ]; + +foreach ( $xml->xpath( '//file' ) as $f ) { + $bucket = bucket_for( (string) $f['name'] ); + if ( null === $bucket || ! isset( $f->metrics ) ) { + continue; + } + $m = $f->metrics; + $st = (int) $m['statements']; + $covst = (int) $m['coveredstatements']; + $el = (int) $m['elements']; + $covel = (int) $m['coveredelements']; + + if ( ! isset( $buckets[ $bucket ] ) ) { + $buckets[ $bucket ] = [ 'st' => 0, 'covst' => 0, 'el' => 0, 'covel' => 0, 'files' => 0 ]; + } + $buckets[ $bucket ]['st'] += $st; + $buckets[ $bucket ]['covst'] += $covst; + $buckets[ $bucket ]['el'] += $el; + $buckets[ $bucket ]['covel'] += $covel; + $buckets[ $bucket ]['files']++; + + $overall['st'] += $st; + $overall['covst'] += $covst; + $overall['el'] += $el; + $overall['covel'] += $covel; +} + +$pct = function ( $c, $t ) { + return $t ? ( $c / $t * 100 ) : 0.0; +}; + +ksort( $buckets ); + +printf( "%-28s %5s %11s %7s %7s\n", 'Bucket', 'Files', 'Stmts', 'Stmt %', 'Elem %' ); +echo str_repeat( '-', 70 ) . "\n"; +foreach ( $buckets as $name => $d ) { + printf( + "%-28s %5d %5d/%-5d %6.2f%% %6.2f%%\n", + $name, + $d['files'], + $d['covst'], + $d['st'], + $pct( $d['covst'], $d['st'] ), + $pct( $d['covel'], $d['el'] ) + ); +} +echo str_repeat( '-', 70 ) . "\n"; +$files_total = array_sum( array_column( $buckets, 'files' ) ); +printf( + "%-28s %5d %5d/%-5d %6.2f%% %6.2f%%\n", + 'OVERALL', + $files_total, + $overall['covst'], + $overall['st'], + $pct( $overall['covst'], $overall['st'] ), + $pct( $overall['covel'], $overall['el'] ) +); diff --git a/tools/phpunit/coverage-baseline.py b/tools/phpunit/coverage-baseline.py deleted file mode 100644 index 502d8c41a..000000000 --- a/tools/phpunit/coverage-baseline.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -"""Print a per-`src/`-subdir coverage breakdown from a PHPUnit Clover XML. - -Usage: python3 tools/phpunit/coverage-baseline.py [path/to/clover.xml] - -Default path: tmp/coverage/report-xml/baseline.xml. See -tests/phpunit/COVERAGE_BASELINE.md for how to produce the input XML. -""" - -import sys -import xml.etree.ElementTree as ET -from collections import defaultdict - - -def bucket_for(path): - if path.endswith(('/pdf.php', '/api.php', '/gravity-pdf-updater.php')): - return 'Plugin root' - if '/src/' not in path: - return None - rel = path.split('/src/', 1)[1] - parts = rel.split('/') - if len(parts) == 1: - return 'src/ root' - if parts[0] == 'Helper' and len(parts) >= 3: - return f'Helper/{parts[1]}' - return parts[0] - - -def main(xml_path): - tree = ET.parse(xml_path) - buckets = defaultdict( - lambda: {'st': 0, 'covst': 0, 'el': 0, 'covel': 0, 'files': 0} - ) - overall = {'st': 0, 'covst': 0, 'el': 0, 'covel': 0} - - for f in tree.iter('file'): - bucket = bucket_for(f.get('name', '')) - if bucket is None: - continue - m = next((c for c in f if c.tag == 'metrics'), None) - if m is None: - continue - st, covst = int(m.get('statements', '0')), int(m.get('coveredstatements', '0')) - el, covel = int(m.get('elements', '0')), int(m.get('coveredelements', '0')) - b = buckets[bucket] - b['st'] += st; b['covst'] += covst; b['el'] += el; b['covel'] += covel - b['files'] += 1 - overall['st'] += st; overall['covst'] += covst - overall['el'] += el; overall['covel'] += covel - - def pct(c, t): - return (c / t * 100) if t else 0.0 - - print(f"{'Bucket':28s} {'Files':>5s} {'Stmts':>11s} {'Stmt %':>7s} {'Elem %':>7s}") - print('-' * 70) - for b in sorted(buckets): - d = buckets[b] - print(f"{b:28s} {d['files']:5d} {d['covst']:5d}/{d['st']:<5d} " - f"{pct(d['covst'], d['st']):6.2f}% {pct(d['covel'], d['el']):6.2f}%") - print('-' * 70) - files_total = sum(b['files'] for b in buckets.values()) - print(f"{'OVERALL':28s} {files_total:5d} " - f"{overall['covst']:5d}/{overall['st']:<5d} " - f"{pct(overall['covst'], overall['st']):6.2f}% " - f"{pct(overall['covel'], overall['el']):6.2f}%") - return 0 - - -if __name__ == '__main__': - sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else 'tmp/coverage/report-xml/baseline.xml')) From e1ce7a6c29b0fbc40d3d4fa343f08256d5eab275 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 14:26:09 +1000 Subject: [PATCH 03/45] docs(phpunit): refresh Playwright baseline after building dist/ + vendor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Phase 0 baseline captured Playwright in a 6/60/21 broken state because `dist/` and `vendor/` weren't built locally — without those Gravity PDF can't boot in wp-env, so most specs failed at fixture/login. Re-ran with both built: 87 passed / 0 failed / 0 didn't run in 2.2m (135s). Also noted that local wall-clock varies and CI's sharded workflow (`.github/workflows/playwright-e2e.yml`, `--shard=N/4`) is the right runtime comparator for Phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/phpunit/COVERAGE_BASELINE.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/phpunit/COVERAGE_BASELINE.md b/tests/phpunit/COVERAGE_BASELINE.md index a53f55615..7bf655c72 100644 --- a/tests/phpunit/COVERAGE_BASELINE.md +++ b/tests/phpunit/COVERAGE_BASELINE.md @@ -116,16 +116,16 @@ Coverage runtime overhead is modest in xdebug `coverage` mode: 47s (vs 38s witho ## Playwright (e2e) baseline -| Metric | Value (local 2026-05-25) | +| Metric | Value (local 2026-05-25, post `yarn build` + `composer install`) | | --- | --- | -| Passed | 6 | -| Failed | 60 | -| Did not run | 21 | -| Wall-clock | 8.4m | +| Passed | 87 | +| Failed | 0 | +| Did not run | 0 | +| Wall-clock | 2.2m (135s) | -The local run is **not green**, but the failures look environmental (post-`dist/` rebuild, fresh wp-env, etc.) rather than refactor-driven — the PHPUnit refactor has not touched any code yet. For the authoritative Playwright baseline, **compare future PRs against the GitHub Actions Playwright workflow artifacts** (`.github/workflows/playwright-e2e.yml`) on the same commit, not the local run. +Local run is fully green. The earlier captured run (6/60/21 in 8.4m) failed because `dist/` and `vendor/` were not built — Playwright requires both before the plugin will boot. -This is recorded only so a contributor on a fresh machine doesn't conclude their environment is broken when they see the same numbers. Phase 2 changes touch fixture bootstrap; before merging Phase 2 verify Playwright in CI matches the pre-refactor CI baseline. +For runtime regression detection prefer the sharded GitHub Actions Playwright workflow (`.github/workflows/playwright-e2e.yml`, 4-way `--shard`) — local wall-clock varies by machine. Phase 2 changes touch fixture bootstrap; before merging Phase 2 verify Playwright remains green in CI. ## Methodology — how to reproduce From e7bee353071a35900474ea5d0f1455a18c17ee64 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 14:50:56 +1000 Subject: [PATCH 04/45] chore(phpunit): add shared TestCase + traits, pilot on Statics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of the PHPUnit refactor (plan: .claude/plans/2026-05-25-phpunit-tests-refactor.md): introduce abstract TestCase / AjaxTestCase under tests/phpunit/integration/ plus three Concerns/ traits (HasGfpdfFixtures, CleansFilesystem, UsesFactory), wire them through tools/phpunit/bootstrap.php::load_test_infrastructure(), and migrate Statics as the pilot vertical slice. Both phpunit configs additively include the new path; legacy unit-tests/ continues to run side-by-side until Phase 2 drains it. Suite stays at 1119 tests / 8 skipped, 37.6s integration + 38.1s multisite (both within Phase 0 baseline's ±10% budget). Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/phpunit/Concerns/CleansFilesystem.php | 73 ++++++++++ tests/phpunit/Concerns/HasGfpdfFixtures.php | 101 ++++++++++++++ tests/phpunit/Concerns/UsesFactory.php | 34 +++++ tests/phpunit/README.md | 131 ++++++++++++++++++ tests/phpunit/integration/AjaxTestCase.php | 21 +++ .../Statics/Test_Cache.php | 11 +- .../Statics/Test_kses.php | 5 +- tests/phpunit/integration/TestCase.php | 23 +++ tools/phpunit/bootstrap.php | 26 +++- tools/phpunit/config-multisite.xml | 1 + tools/phpunit/config.xml | 1 + 11 files changed, 417 insertions(+), 10 deletions(-) create mode 100644 tests/phpunit/Concerns/CleansFilesystem.php create mode 100644 tests/phpunit/Concerns/HasGfpdfFixtures.php create mode 100644 tests/phpunit/Concerns/UsesFactory.php create mode 100644 tests/phpunit/README.md create mode 100644 tests/phpunit/integration/AjaxTestCase.php rename tests/phpunit/{unit-tests => integration}/Statics/Test_Cache.php (88%) rename tests/phpunit/{unit-tests => integration}/Statics/Test_kses.php (98%) create mode 100644 tests/phpunit/integration/TestCase.php diff --git a/tests/phpunit/Concerns/CleansFilesystem.php b/tests/phpunit/Concerns/CleansFilesystem.php new file mode 100644 index 000000000..d4147338c --- /dev/null +++ b/tests/phpunit/Concerns/CleansFilesystem.php @@ -0,0 +1,73 @@ + Absolute paths queued for cleanup. + */ + private $gfpdf_cleanup_paths = []; + + /** + * Queue a path for removal in tear_down(). Files and directories supported. + * + * @param string $path Absolute filesystem path. + */ + protected function register_path_for_cleanup( $path ) { + $this->gfpdf_cleanup_paths[] = $path; + } + + /** + * Removes every queued path. Call from a subclass tear_down() override + * BEFORE parent::tear_down() — WP teardown can drop temp dirs we created. + */ + protected function clean_registered_paths() { + foreach ( $this->gfpdf_cleanup_paths as $path ) { + $this->remove_path( $path ); + } + + $this->gfpdf_cleanup_paths = []; + } + + /** + * Recursive remove for both files and directories. + * + * @param string $path Absolute filesystem path. + */ + private function remove_path( $path ) { + if ( ! file_exists( $path ) && ! is_link( $path ) ) { + return; + } + + if ( is_file( $path ) || is_link( $path ) ) { + @unlink( $path ); + + return; + } + + $items = scandir( $path ); + if ( false === $items ) { + return; + } + + foreach ( $items as $item ) { + if ( '.' === $item || '..' === $item ) { + continue; + } + $this->remove_path( $path . DIRECTORY_SEPARATOR . $item ); + } + + @rmdir( $path ); + } +} diff --git a/tests/phpunit/Concerns/HasGfpdfFixtures.php b/tests/phpunit/Concerns/HasGfpdfFixtures.php new file mode 100644 index 000000000..404601eb9 --- /dev/null +++ b/tests/phpunit/Concerns/HasGfpdfFixtures.php @@ -0,0 +1,101 @@ +form[ $key ] ) ) { + $available = implode( ', ', array_keys( (array) $GLOBALS['GFPDF_Test']->form ) ); + $this->fail( "Form fixture '$key' is not loaded. Available: $available" ); + } + + return $GLOBALS['GFPDF_Test']->form[ $key ]; + } + + /** + * Returns one of the shared entry fixtures stored under $key. + * + * @param string $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 ) { + if ( ! isset( $GLOBALS['GFPDF_Test']->entries[ $key ][ $index ] ) ) { + $this->fail( "Entry fixture '$key'[$index] is not loaded." ); + } + + return $GLOBALS['GFPDF_Test']->entries[ $key ][ $index ]; + } + + /** + * Returns the Gravity PDF Router (DI container). + * + * @return \GFPDF\Router + */ + protected function gfpdf() { + global $gfpdf; + + return $gfpdf; + } + + /** + * Sentinel check that the shared fixture catalogue is intact. + * + * Called from each base class's set_up(). Catches the failure mode where + * a previous test deleted a shared form/entry from $GLOBALS['GFPDF_Test']. + * Deep mutation of fixture contents is not checked — too expensive at + * 1100+ tests; this is a fast presence check. + */ + protected function assertFixturesIntact() { + $expected_forms = [ + 'all-form-fields', + 'form-settings', + 'gravityform-1', + 'gravityform-2', + 'repeater-empty-form', + 'repeater-consent-form', + 'non-group-products-form', + ]; + + $expected_entries = [ + 'all-form-fields', + 'gravityform-1', + 'repeater-empty-form', + 'repeater-consent-form', + 'non-group-products-form', + ]; + + foreach ( $expected_forms as $key ) { + $this->assertNotEmpty( + $GLOBALS['GFPDF_Test']->form[ $key ] ?? null, + "Shared form fixture '$key' missing — a prior test mutated \$GLOBALS['GFPDF_Test']->form" + ); + } + + foreach ( $expected_entries as $key ) { + $this->assertNotEmpty( + $GLOBALS['GFPDF_Test']->entries[ $key ] ?? null, + "Shared entry fixture '$key' missing — a prior test mutated \$GLOBALS['GFPDF_Test']->entries" + ); + } + } +} diff --git a/tests/phpunit/Concerns/UsesFactory.php b/tests/phpunit/Concerns/UsesFactory.php new file mode 100644 index 000000000..dd1b07d3a --- /dev/null +++ b/tests/phpunit/Concerns/UsesFactory.php @@ -0,0 +1,34 @@ +gfpdf_factory ) { + $this->gfpdf_factory = new GF_UnitTest_Factory(); + } + + return $this->gfpdf_factory; + } +} diff --git a/tests/phpunit/README.md b/tests/phpunit/README.md new file mode 100644 index 000000000..7bef91a29 --- /dev/null +++ b/tests/phpunit/README.md @@ -0,0 +1,131 @@ +# Gravity PDF — PHPUnit Test Suite + +This directory holds the plugin's PHPUnit integration tests, run inside the +`wp-env` Docker container via `yarn test:php` / `yarn test:php:multisite`. + +The suite is mid-refactor; see [`.claude/plans/2026-05-25-phpunit-tests-refactor.md`](../../.claude/plans/2026-05-25-phpunit-tests-refactor.md) +for the phase plan and [`COVERAGE_BASELINE.md`](COVERAGE_BASELINE.md) for the +runtime + coverage baseline every PR is compared against. + +## Layout + +``` +tests/phpunit/ +├── COVERAGE_BASELINE.md +├── README.md ← you are here +├── Concerns/ ← shared traits (NOT discovered by PHPUnit) +│ ├── HasGfpdfFixtures.php +│ ├── CleansFilesystem.php +│ └── UsesFactory.php +├── integration/ ← new location, mirrors src/ 1:1 +│ ├── TestCase.php +│ ├── AjaxTestCase.php +│ └── Statics/ +│ ├── Test_Cache.php +│ └── Test_kses.php +└── unit-tests/ ← legacy location, drained subdir-by-subdir +``` + +`unit-tests/` and `integration/` co-exist during Phases 1–2. Both directories +are listed in `tools/phpunit/config.xml`. Phase 2 ends with `unit-tests/` +empty and the config entry removed. + +## Naming convention + +| Source file | Test file | +| :--- | :--- | +| `src/Statics/Cache.php` | `tests/phpunit/integration/Statics/Test_Cache.php` | +| `src/Model/Model_PDF.php` | `tests/phpunit/integration/Model/Test_Model_PDF.php` | +| `src/Controller/Controller_Settings.php` | `tests/phpunit/integration/Controller/Test_Controller_Settings.php` | + +One `Test_.php` per non-trivial `src/` class, at the matching path. +Class name = `Test_`; method names use `test_` snake_case to keep +`phpunit --filter test_something` searches predictable. + +## Choosing a base class + +| Need | Base class | +| :--- | :--- | +| Standard integration test (DB, hooks, options) | `\GFPDF\Tests\Integration\TestCase` | +| Test that dispatches a `wp_ajax_*` action via `_handleAjax()` | `\GFPDF\Tests\Integration\AjaxTestCase` | + +Both extend WordPress's stock test cases and `use \GFPDF\Tests\Concerns\HasGfpdfFixtures`, +which provides: + +- `$this->form( 'all-form-fields' )` — shared form fixture loaded once per suite. +- `$this->entry( 'all-form-fields', 0 )` — shared entry fixture. +- `$this->gfpdf()` — the `GFPDF\Router` DI container (same as the `$gfpdf` global). +- `$this->assertFixturesIntact()` — automatically called from `set_up()` to catch + cross-test fixture mutation. Override `set_up()` only if you call + `parent::set_up()`. + +## Writing a new test + +```php +form( 'all-form-fields' ); + $entry = $this->entry( 'all-form-fields' ); + + $this->assertSame( + Cache::get_hash( $form, $entry, [] ), + Cache::get_hash( $form, $entry, [] ) + ); + } +} +``` + +Conventions: + +- `declare(strict_types=1);` — required. +- Namespace matches the class under test (`GFPDF\Statics`, `GFPDF\Model`, etc.). +- `@group` annotation — every test class should have at least one (e.g. `controller`, + `model`, `helper`, `ajax`, `slow-pdf-processes`) so contributors can run a slice. + +## Fixture access + +`tools/phpunit/bootstrap.php::create_stubs()` loads seven JSON forms and five +batches of entries into `$GLOBALS['GFPDF_Test']` once per suite. Use the trait +accessors rather than the global directly — same source, but failures point +to the missing key instead of throwing an undefined-index notice. + +**Never call `GFAPI::add_form()` / `GFAPI::add_entry()` in a test body.** +Phase 3 of the refactor enforces this; use the existing factory +(`GF_UnitTest_Factory` at `tools/phpunit/gravityforms-factory.php`) via the +`UsesFactory` trait when you need a per-test form/entry. + +## The "test if non-trivial" rule + +Skip a `Test_*.php` for a class that is: +1. Under 30 lines of code, **and** +2. Has no methods of its own (only inherited), **and** +3. Has no constructor logic. + +The 11 classes under `src/Exceptions/` are covered by a single +`integration/Exceptions/Test_Exception_Hierarchy.php` smoke test, not 11 +individual files. Phase 4 of the refactor lands that test. + +## Running + +```bash +yarn wp-env:integration start # one-time per session +yarn test:php # full suite +yarn test:php -- --filter Test_Cache # single class +yarn test:php -- --group statics # group +yarn test:php:multisite # WP multisite mode +``` + +See top-level `CLAUDE.md` and `COVERAGE_BASELINE.md` for the runtime budget +and the methodology for regenerating baseline timings. diff --git a/tests/phpunit/integration/AjaxTestCase.php b/tests/phpunit/integration/AjaxTestCase.php new file mode 100644 index 000000000..440826460 --- /dev/null +++ b/tests/phpunit/integration/AjaxTestCase.php @@ -0,0 +1,21 @@ +_handleAjax(). + */ +abstract class AjaxTestCase extends WP_Ajax_UnitTestCase { + + use HasGfpdfFixtures; + + public function set_up() { + parent::set_up(); + $this->assertFixturesIntact(); + } +} diff --git a/tests/phpunit/unit-tests/Statics/Test_Cache.php b/tests/phpunit/integration/Statics/Test_Cache.php similarity index 88% rename from tests/phpunit/unit-tests/Statics/Test_Cache.php rename to tests/phpunit/integration/Statics/Test_Cache.php index a1029b360..7b7adafb4 100644 --- a/tests/phpunit/unit-tests/Statics/Test_Cache.php +++ b/tests/phpunit/integration/Statics/Test_Cache.php @@ -4,7 +4,7 @@ namespace GFPDF\Statics; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -15,7 +15,7 @@ /** * @group statics */ -class Test_Cache extends WP_UnitTestCase { +class Test_Cache extends TestCase { public function test_get_hash() { $results = $this->create_form_and_entries(); @@ -41,11 +41,10 @@ public function test_get_hash() { } protected function create_form_and_entries() { - global $gfpdf; - - $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; - $entry = $GLOBALS['GFPDF_Test']->entries['all-form-fields'][0]; + $form = $this->form( 'all-form-fields' ); + $entry = $this->entry( 'all-form-fields' ); + $gfpdf = $this->gfpdf(); $gfpdf->data->form_settings = []; $gfpdf->data->form_settings[ $form['id'] ] = $form['gfpdf_form_settings']; diff --git a/tests/phpunit/unit-tests/Statics/Test_kses.php b/tests/phpunit/integration/Statics/Test_kses.php similarity index 98% rename from tests/phpunit/unit-tests/Statics/Test_kses.php rename to tests/phpunit/integration/Statics/Test_kses.php index 0692ae7ff..08ca596a0 100644 --- a/tests/phpunit/unit-tests/Statics/Test_kses.php +++ b/tests/phpunit/integration/Statics/Test_kses.php @@ -4,8 +4,7 @@ namespace GFPDF\Statics; -use GFPDF\Exceptions\GravityPdfException; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -18,7 +17,7 @@ * * @group statics */ -class Test_Kses extends WP_UnitTestCase { +class Test_Kses extends TestCase { /** * @dataProvider provider_parse_pdf_tags_and_attributes diff --git a/tests/phpunit/integration/TestCase.php b/tests/phpunit/integration/TestCase.php new file mode 100644 index 000000000..8444bc322 --- /dev/null +++ b/tests/phpunit/integration/TestCase.php @@ -0,0 +1,23 @@ +assertFixturesIntact(); + } +} diff --git a/tools/phpunit/bootstrap.php b/tools/phpunit/bootstrap.php index e25436b60..245e45846 100644 --- a/tools/phpunit/bootstrap.php +++ b/tools/phpunit/bootstrap.php @@ -80,10 +80,16 @@ public function __construct() { /* Load Mocks */ $this->mocks(); + + /* Load shared TestCase + Concerns infrastructure (Phase 1 of phpunit refactor) */ + $this->load_test_infrastructure(); } /** - * Load Addon Mocks + * Load Addon Mocks. + * + * Currently loads the Zapier add-on stub so tests that exercise the Zapier + * integration code paths can run without the real add-on installed. * * @since 6.3 */ @@ -91,6 +97,24 @@ public function mocks() { require_once __DIR__ . '/Mocks/zapier-mock.php'; } + /** + * Load the shared TestCase + trait files used by tests under + * tests/phpunit/integration/. Required explicitly because the + * Concerns/ directory is intentionally not part of the PHPUnit + * list. + * + * @since 7.0 + */ + public function load_test_infrastructure() { + $root = $this->plugin_dir . '/tests/phpunit'; + + require_once $root . '/Concerns/HasGfpdfFixtures.php'; + require_once $root . '/Concerns/CleansFilesystem.php'; + require_once $root . '/Concerns/UsesFactory.php'; + require_once $root . '/integration/TestCase.php'; + require_once $root . '/integration/AjaxTestCase.php'; + } + /** * Load Gravity Forms and Gravity PDF * diff --git a/tools/phpunit/config-multisite.xml b/tools/phpunit/config-multisite.xml index c3b5033ad..38e401164 100644 --- a/tools/phpunit/config-multisite.xml +++ b/tools/phpunit/config-multisite.xml @@ -13,6 +13,7 @@ ../../tests/phpunit/unit-tests/ + ../../tests/phpunit/integration/ diff --git a/tools/phpunit/config.xml b/tools/phpunit/config.xml index 39c15105d..87434d449 100644 --- a/tools/phpunit/config.xml +++ b/tools/phpunit/config.xml @@ -9,6 +9,7 @@ ../../tests/phpunit/unit-tests/ + ../../tests/phpunit/integration/ From f9714366f0974b65dae516965abbfd8ec63de4d7 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 14:54:04 +1000 Subject: [PATCH 05/45] chore(phpunit): drop premature AjaxTestCase + unused traits No-slop cleanup of the Phase 1 commit: removed AjaxTestCase, CleansFilesystem, and UsesFactory because nothing in Phase 1 consumes them. Phase 2 introduces AjaxTestCase when it splits test-ajax.php; Phase 3 introduces the other two when it adopts factories and filesystem cleanup. Also trimmed task-reference comments and tightened the trait/TestCase phpdoc. Statics pilot unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/phpunit/Concerns/CleansFilesystem.php | 73 --------------------- tests/phpunit/Concerns/HasGfpdfFixtures.php | 6 +- tests/phpunit/Concerns/UsesFactory.php | 34 ---------- tests/phpunit/README.md | 27 ++++---- tests/phpunit/integration/AjaxTestCase.php | 21 ------ tests/phpunit/integration/TestCase.php | 5 -- tools/phpunit/bootstrap.php | 11 +--- 7 files changed, 17 insertions(+), 160 deletions(-) delete mode 100644 tests/phpunit/Concerns/CleansFilesystem.php delete mode 100644 tests/phpunit/Concerns/UsesFactory.php delete mode 100644 tests/phpunit/integration/AjaxTestCase.php diff --git a/tests/phpunit/Concerns/CleansFilesystem.php b/tests/phpunit/Concerns/CleansFilesystem.php deleted file mode 100644 index d4147338c..000000000 --- a/tests/phpunit/Concerns/CleansFilesystem.php +++ /dev/null @@ -1,73 +0,0 @@ - Absolute paths queued for cleanup. - */ - private $gfpdf_cleanup_paths = []; - - /** - * Queue a path for removal in tear_down(). Files and directories supported. - * - * @param string $path Absolute filesystem path. - */ - protected function register_path_for_cleanup( $path ) { - $this->gfpdf_cleanup_paths[] = $path; - } - - /** - * Removes every queued path. Call from a subclass tear_down() override - * BEFORE parent::tear_down() — WP teardown can drop temp dirs we created. - */ - protected function clean_registered_paths() { - foreach ( $this->gfpdf_cleanup_paths as $path ) { - $this->remove_path( $path ); - } - - $this->gfpdf_cleanup_paths = []; - } - - /** - * Recursive remove for both files and directories. - * - * @param string $path Absolute filesystem path. - */ - private function remove_path( $path ) { - if ( ! file_exists( $path ) && ! is_link( $path ) ) { - return; - } - - if ( is_file( $path ) || is_link( $path ) ) { - @unlink( $path ); - - return; - } - - $items = scandir( $path ); - if ( false === $items ) { - return; - } - - foreach ( $items as $item ) { - if ( '.' === $item || '..' === $item ) { - continue; - } - $this->remove_path( $path . DIRECTORY_SEPARATOR . $item ); - } - - @rmdir( $path ); - } -} diff --git a/tests/phpunit/Concerns/HasGfpdfFixtures.php b/tests/phpunit/Concerns/HasGfpdfFixtures.php index 404601eb9..5f6d7483a 100644 --- a/tests/phpunit/Concerns/HasGfpdfFixtures.php +++ b/tests/phpunit/Concerns/HasGfpdfFixtures.php @@ -8,9 +8,9 @@ * Ergonomic accessors for the shared form/entry fixtures loaded once per * suite by tools/phpunit/bootstrap.php into $GLOBALS['GFPDF_Test']. * - * Both \GFPDF\Tests\Integration\TestCase and AjaxTestCase use this trait — - * PHP has no multiple inheritance, so the accessors must live in a trait - * that each base class can `use`. + * Lives in a trait so a future AJAX base extending WP_Ajax_UnitTestCase can + * share the same accessors without duplicating them (PHP has no multiple + * inheritance). */ trait HasGfpdfFixtures { diff --git a/tests/phpunit/Concerns/UsesFactory.php b/tests/phpunit/Concerns/UsesFactory.php deleted file mode 100644 index dd1b07d3a..000000000 --- a/tests/phpunit/Concerns/UsesFactory.php +++ /dev/null @@ -1,34 +0,0 @@ -gfpdf_factory ) { - $this->gfpdf_factory = new GF_UnitTest_Factory(); - } - - return $this->gfpdf_factory; - } -} diff --git a/tests/phpunit/README.md b/tests/phpunit/README.md index 7bef91a29..ea183db08 100644 --- a/tests/phpunit/README.md +++ b/tests/phpunit/README.md @@ -14,12 +14,9 @@ tests/phpunit/ ├── COVERAGE_BASELINE.md ├── README.md ← you are here ├── Concerns/ ← shared traits (NOT discovered by PHPUnit) -│ ├── HasGfpdfFixtures.php -│ ├── CleansFilesystem.php -│ └── UsesFactory.php +│ └── HasGfpdfFixtures.php ├── integration/ ← new location, mirrors src/ 1:1 │ ├── TestCase.php -│ ├── AjaxTestCase.php │ └── Statics/ │ ├── Test_Cache.php │ └── Test_kses.php @@ -42,15 +39,10 @@ One `Test_.php` per non-trivial `src/` class, at the matching path. Class name = `Test_`; method names use `test_` snake_case to keep `phpunit --filter test_something` searches predictable. -## Choosing a base class +## Base class -| Need | Base class | -| :--- | :--- | -| Standard integration test (DB, hooks, options) | `\GFPDF\Tests\Integration\TestCase` | -| Test that dispatches a `wp_ajax_*` action via `_handleAjax()` | `\GFPDF\Tests\Integration\AjaxTestCase` | - -Both extend WordPress's stock test cases and `use \GFPDF\Tests\Concerns\HasGfpdfFixtures`, -which provides: +Tests extend `\GFPDF\Tests\Integration\TestCase`, which extends `WP_UnitTestCase` +and `use`s `\GFPDF\Tests\Concerns\HasGfpdfFixtures`. The trait provides: - `$this->form( 'all-form-fields' )` — shared form fixture loaded once per suite. - `$this->entry( 'all-form-fields', 0 )` — shared entry fixture. @@ -59,6 +51,9 @@ which provides: cross-test fixture mutation. Override `set_up()` only if you call `parent::set_up()`. +A second base for `WP_Ajax_UnitTestCase`-derived tests will be added when Phase 2 +splits `test-ajax.php`. + ## Writing a new test ```php @@ -101,10 +96,10 @@ batches of entries into `$GLOBALS['GFPDF_Test']` once per suite. Use the trait accessors rather than the global directly — same source, but failures point to the missing key instead of throwing an undefined-index notice. -**Never call `GFAPI::add_form()` / `GFAPI::add_entry()` in a test body.** -Phase 3 of the refactor enforces this; use the existing factory -(`GF_UnitTest_Factory` at `tools/phpunit/gravityforms-factory.php`) via the -`UsesFactory` trait when you need a per-test form/entry. +**Avoid calling `GFAPI::add_form()` / `GFAPI::add_entry()` from a test body** — +prefer the existing `GF_UnitTest_Factory` (`tools/phpunit/gravityforms-factory.php`) +when you need a per-test form/entry. Phase 3 of the refactor will enforce this +across the suite and ship a trait-based accessor. ## The "test if non-trivial" rule diff --git a/tests/phpunit/integration/AjaxTestCase.php b/tests/phpunit/integration/AjaxTestCase.php deleted file mode 100644 index 440826460..000000000 --- a/tests/phpunit/integration/AjaxTestCase.php +++ /dev/null @@ -1,21 +0,0 @@ -_handleAjax(). - */ -abstract class AjaxTestCase extends WP_Ajax_UnitTestCase { - - use HasGfpdfFixtures; - - public function set_up() { - parent::set_up(); - $this->assertFixturesIntact(); - } -} diff --git a/tests/phpunit/integration/TestCase.php b/tests/phpunit/integration/TestCase.php index 8444bc322..1b328505d 100644 --- a/tests/phpunit/integration/TestCase.php +++ b/tests/phpunit/integration/TestCase.php @@ -7,11 +7,6 @@ use GFPDF\Tests\Concerns\HasGfpdfFixtures; use WP_UnitTestCase; -/** - * Default base for non-AJAX integration tests. - * - * Use AjaxTestCase instead when the test exercises a wp_ajax_* action. - */ abstract class TestCase extends WP_UnitTestCase { use HasGfpdfFixtures; diff --git a/tools/phpunit/bootstrap.php b/tools/phpunit/bootstrap.php index 245e45846..97cb55196 100644 --- a/tools/phpunit/bootstrap.php +++ b/tools/phpunit/bootstrap.php @@ -81,7 +81,6 @@ public function __construct() { /* Load Mocks */ $this->mocks(); - /* Load shared TestCase + Concerns infrastructure (Phase 1 of phpunit refactor) */ $this->load_test_infrastructure(); } @@ -98,10 +97,9 @@ public function mocks() { } /** - * Load the shared TestCase + trait files used by tests under - * tests/phpunit/integration/. Required explicitly because the - * Concerns/ directory is intentionally not part of the PHPUnit - * list. + * Required explicitly because tests/phpunit/Concerns/ is intentionally + * not part of the PHPUnit list (traits are not + * tests). * * @since 7.0 */ @@ -109,10 +107,7 @@ public function load_test_infrastructure() { $root = $this->plugin_dir . '/tests/phpunit'; require_once $root . '/Concerns/HasGfpdfFixtures.php'; - require_once $root . '/Concerns/CleansFilesystem.php'; - require_once $root . '/Concerns/UsesFactory.php'; require_once $root . '/integration/TestCase.php'; - require_once $root . '/integration/AjaxTestCase.php'; } /** From 22e946b20cdec8236c5adf8164fb6deb10cab78a Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 15:15:20 +1000 Subject: [PATCH 06/45] chore(phpunit): drain unit-tests/ into integration/ mirroring src/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the PHPUnit refactor: bulk structural normalization. All 73 legacy tests under tests/phpunit/unit-tests/ now live under tests/phpunit/integration/ mirroring src/ paths. 41 existing PascalCase tests moved + switched base class to GFPDF\Tests\Integration\TestCase. 30 root-level kebab-case test-*.php files renamed to PascalCase, moved to their closest src/ subdir, and re-namespaced from GFPDF\Tests to match. Four Rest/ + Controller/ files with mis-set namespaces also corrected. Restored AjaxTestCase now that test-ajax.php's split gives it real callers. Two kitchen-sink files split per the plan: - test-ajax.php (9 methods, 1 class) → 4 *_Ajax.php under integration/Model/ grouped by target Model class (Form_Settings, Templates, Settings, Custom_Fonts). Form_Settings_Ajax keeps the per-test add_form (will move to factory in Phase 3); the other three splits no longer import a form at all. - test-helper-misc.php (19 methods, 735 lines) → 4 files under integration/Helper/ grouped by concern: Colors, Pages, Forms, Config. Both phpunit configs drop the now-empty unit-tests/ entry. Test count unchanged at 1119 (8 skipped integration, 1 skipped multisite). Runtime: 27.6s integration / 27.2s multisite — both -28% vs Phase 0 baseline (38.5s / 38.6s). Speedup comes from the AJAX split: 9 tests that were the slowest cluster (26.7s of 38.5s total) now run in 0.1s once test-ajax.php's per-test set_up overhead is split across four smaller classes and three of them drop the form-import entirely. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/phpunit/integration/AjaxTestCase.php | 18 + .../Test_Controller_Custom_Fonts.php | 4 +- .../Test_Controller_Export_Entries.php | 4 +- .../Controller/Test_Controller_Pdf_Queue.php | 6 +- .../Controller/Test_Controller_Settings.php | 4 +- .../Test_Controller_System_Report.php | 4 +- .../Test_Controller_Upgrade_Routines.php | 4 +- .../Controller/Test_Controller_Webhooks.php | 4 +- .../Controller/Test_Controller_Zapier.php | 4 +- .../Helper/Fields/Test_Field_Consent.php | 4 +- .../Helper/Fields/Test_Field_Form.php | 4 +- .../Helper/Fields/Test_Field_Image_Choice.php | 4 +- .../Helper/Fields/Test_Field_List.php | 4 +- .../Helper/Fields/Test_Field_Markup.php} | 7 +- .../Helper/Fields/Test_Field_Multi_Choice.php | 4 +- .../Helper/Fields/Test_Field_Option.php | 4 +- .../Helper/Fields/Test_Field_Poll.php | 4 +- .../Fields/Test_Field_Post_Category.php | 4 +- .../Fields/Test_Field_Post_Custom_Field.php | 4 +- .../Helper/Fields/Test_Field_Post_Image.php | 4 +- .../Helper/Fields/Test_Field_Product.php | 4 +- .../Helper/Fields/Test_Field_Products.php | 4 +- .../Helper/Fields/Test_Field_Radio.php | 4 +- .../Helper/Fields/Test_Field_Repeater.php | 4 +- .../Helper/Fields/Test_Field_Section.php | 4 +- .../Helper/Fields/Test_Field_Select.php | 4 +- .../Helper/Fields/Test_Field_Signature.php | 4 +- .../Helper/Fields/Test_Field_Survey.php | 4 +- .../Helper/Fields/Test_Field_Textarea.php | 4 +- .../Helper/Fonts/Test_FlushCache.php | 4 +- .../Licensing/Test_EDD_SL_Plugin_Updater.php | 4 +- .../Helper/Log/Test_Redact_Processor.php | 0 .../Helper/Mpdf/Test_Cache.php | 4 +- .../Helper/Mpdf/Test_Request.php | 4 +- .../Helper/Test_Addon.php} | 7 +- .../Helper/Test_Field_Container.php} | 7 +- .../Helper/Test_Form_Data.php} | 7 +- .../Helper/Test_Gravity_Forms.php} | 7 +- .../Helper/Test_Helper_Data.php} | 7 +- .../Test_Helper_Field_Container_Gf25.php | 4 +- .../Helper/Test_Helper_Misc_Colors.php | 85 ++ .../Helper/Test_Helper_Misc_Config.php | 104 +++ .../Helper/Test_Helper_Misc_Forms.php | 249 ++++++ .../Helper/Test_Helper_Misc_Pages.php | 121 +++ .../Helper/Test_Helper_Mpdf.php} | 7 +- .../Helper/Test_Helper_Templates.php} | 7 +- .../Helper/Test_Interfaces.php} | 7 +- .../Helper/Test_Logger.php} | 7 +- .../Helper/Test_MVC_Abstracts.php} | 7 +- .../Helper/Test_Notices.php} | 7 +- .../Helper/Test_Options_API.php} | 7 +- .../Helper/Test_QueryPath.php} | 7 +- .../Helper/Test_Settings.php} | 7 +- .../Helper/Test_Singleton.php} | 7 +- .../Helper/Test_Url_Signer.php} | 7 +- .../Model/Test_Actions.php} | 7 +- .../Model/Test_Form_Settings.php} | 7 +- .../Model/Test_Installer.php} | 7 +- .../Model/Test_Model_Custom_Fonts.php | 4 +- .../Model/Test_Model_Custom_Fonts_Ajax.php | 55 ++ .../Model/Test_Model_Form_Settings_Ajax.php | 207 +++++ .../Model/Test_Model_Mergetags.php | 4 +- .../Model/Test_Model_Pdf.php | 4 +- .../Model/Test_Model_Pdf_Meta_Box.php | 4 +- .../Model/Test_Model_Settings.php | 4 +- .../Model/Test_Model_Settings_Ajax.php | 33 + .../Model/Test_Model_System_Report.php | 4 +- .../Model/Test_Model_Templates_Ajax.php | 91 +++ .../Model/Test_PDF.php} | 7 +- .../Model/Test_Shortcodes.php} | 7 +- .../Model/Test_Slow_PDF_Processes.php} | 7 +- .../Model/Test_Templates.php} | 7 +- .../Model/Test_Uninstaller.php} | 7 +- .../Rest/Test_Rest.php | 6 +- .../Rest/Test_Rest_Form_Settings.php | 2 +- .../Rest/Test_Rest_Pdf_Preview.php | 2 +- .../test-api.php => integration/Test_Api.php} | 4 +- .../Test_Autoloader.php} | 4 +- .../Test_Bootstrap.php} | 4 +- .../Test_Deprecated.php} | 4 +- .../Test_Pre_Checks.php} | 4 +- .../View/Test_View_System_Report.php | 4 +- tests/phpunit/unit-tests/test-ajax.php | 552 ------------- tests/phpunit/unit-tests/test-helper-misc.php | 735 ------------------ tools/phpunit/bootstrap.php | 1 + tools/phpunit/config-multisite.xml | 1 - tools/phpunit/config.xml | 1 - 87 files changed, 1133 insertions(+), 1483 deletions(-) create mode 100644 tests/phpunit/integration/AjaxTestCase.php rename tests/phpunit/{unit-tests => integration}/Controller/Test_Controller_Custom_Fonts.php (99%) rename tests/phpunit/{unit-tests => integration}/Controller/Test_Controller_Export_Entries.php (95%) rename tests/phpunit/{unit-tests => integration}/Controller/Test_Controller_Pdf_Queue.php (98%) rename tests/phpunit/{unit-tests => integration}/Controller/Test_Controller_Settings.php (95%) rename tests/phpunit/{unit-tests => integration}/Controller/Test_Controller_System_Report.php (97%) rename tests/phpunit/{unit-tests => integration}/Controller/Test_Controller_Upgrade_Routines.php (91%) rename tests/phpunit/{unit-tests => integration}/Controller/Test_Controller_Webhooks.php (94%) rename tests/phpunit/{unit-tests => integration}/Controller/Test_Controller_Zapier.php (97%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Consent.php (96%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Form.php (95%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Image_Choice.php (99%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_List.php (95%) rename tests/phpunit/{unit-tests/test-field-markup.php => integration/Helper/Fields/Test_Field_Markup.php} (96%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Multi_Choice.php (98%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Option.php (94%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Poll.php (93%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Post_Category.php (93%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Post_Custom_Field.php (93%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Post_Image.php (97%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Product.php (96%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Products.php (97%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Radio.php (95%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Repeater.php (97%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Section.php (94%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Select.php (97%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Signature.php (97%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Survey.php (93%) rename tests/phpunit/{unit-tests => integration}/Helper/Fields/Test_Field_Textarea.php (93%) rename tests/phpunit/{unit-tests => integration}/Helper/Fonts/Test_FlushCache.php (88%) rename tests/phpunit/{unit-tests => integration}/Helper/Licensing/Test_EDD_SL_Plugin_Updater.php (99%) rename tests/phpunit/{unit-tests => integration}/Helper/Log/Test_Redact_Processor.php (100%) rename tests/phpunit/{unit-tests => integration}/Helper/Mpdf/Test_Cache.php (94%) rename tests/phpunit/{unit-tests => integration}/Helper/Mpdf/Test_Request.php (96%) rename tests/phpunit/{unit-tests/test-addon.php => integration/Helper/Test_Addon.php} (99%) rename tests/phpunit/{unit-tests/test-field-container.php => integration/Helper/Test_Field_Container.php} (98%) rename tests/phpunit/{unit-tests/test-form-data.php => integration/Helper/Test_Form_Data.php} (99%) rename tests/phpunit/{unit-tests/test-gravity-forms.php => integration/Helper/Test_Gravity_Forms.php} (99%) rename tests/phpunit/{unit-tests/test-helper-data.php => integration/Helper/Test_Helper_Data.php} (97%) rename tests/phpunit/{unit-tests => integration}/Helper/Test_Helper_Field_Container_Gf25.php (97%) create mode 100644 tests/phpunit/integration/Helper/Test_Helper_Misc_Colors.php create mode 100644 tests/phpunit/integration/Helper/Test_Helper_Misc_Config.php create mode 100644 tests/phpunit/integration/Helper/Test_Helper_Misc_Forms.php create mode 100644 tests/phpunit/integration/Helper/Test_Helper_Misc_Pages.php rename tests/phpunit/{unit-tests/test-helper-mpdf.php => integration/Helper/Test_Helper_Mpdf.php} (89%) rename tests/phpunit/{unit-tests/test-helper-templates.php => integration/Helper/Test_Helper_Templates.php} (99%) rename tests/phpunit/{unit-tests/test-interfaces.php => integration/Helper/Test_Interfaces.php} (89%) rename tests/phpunit/{unit-tests/test-logger.php => integration/Helper/Test_Logger.php} (95%) rename tests/phpunit/{unit-tests/test-mvc-abstracts.php => integration/Helper/Test_MVC_Abstracts.php} (96%) rename tests/phpunit/{unit-tests/test-notices.php => integration/Helper/Test_Notices.php} (97%) rename tests/phpunit/{unit-tests/test-options-api.php => integration/Helper/Test_Options_API.php} (99%) rename tests/phpunit/{unit-tests/test-query-path.php => integration/Helper/Test_QueryPath.php} (91%) rename tests/phpunit/{unit-tests/test-settings.php => integration/Helper/Test_Settings.php} (99%) rename tests/phpunit/{unit-tests/test-singleton.php => integration/Helper/Test_Singleton.php} (97%) rename tests/phpunit/{unit-tests/test-url-signer.php => integration/Helper/Test_Url_Signer.php} (98%) rename tests/phpunit/{unit-tests/test-actions.php => integration/Model/Test_Actions.php} (98%) rename tests/phpunit/{unit-tests/test-form-settings.php => integration/Model/Test_Form_Settings.php} (99%) rename tests/phpunit/{unit-tests/test-installer.php => integration/Model/Test_Installer.php} (98%) rename tests/phpunit/{unit-tests => integration}/Model/Test_Model_Custom_Fonts.php (98%) create mode 100644 tests/phpunit/integration/Model/Test_Model_Custom_Fonts_Ajax.php create mode 100644 tests/phpunit/integration/Model/Test_Model_Form_Settings_Ajax.php rename tests/phpunit/{unit-tests => integration}/Model/Test_Model_Mergetags.php (99%) rename tests/phpunit/{unit-tests => integration}/Model/Test_Model_Pdf.php (98%) rename tests/phpunit/{unit-tests => integration}/Model/Test_Model_Pdf_Meta_Box.php (97%) rename tests/phpunit/{unit-tests => integration}/Model/Test_Model_Settings.php (98%) create mode 100644 tests/phpunit/integration/Model/Test_Model_Settings_Ajax.php rename tests/phpunit/{unit-tests => integration}/Model/Test_Model_System_Report.php (96%) create mode 100644 tests/phpunit/integration/Model/Test_Model_Templates_Ajax.php rename tests/phpunit/{unit-tests/test-pdf.php => integration/Model/Test_PDF.php} (99%) rename tests/phpunit/{unit-tests/test-shortcodes.php => integration/Model/Test_Shortcodes.php} (99%) rename tests/phpunit/{unit-tests/test-slow-pdf-processes.php => integration/Model/Test_Slow_PDF_Processes.php} (99%) rename tests/phpunit/{unit-tests/test-templates.php => integration/Model/Test_Templates.php} (98%) rename tests/phpunit/{unit-tests/test-uninstaller.php => integration/Model/Test_Uninstaller.php} (97%) rename tests/phpunit/{unit-tests => integration}/Rest/Test_Rest.php (94%) rename tests/phpunit/{unit-tests => integration}/Rest/Test_Rest_Form_Settings.php (99%) rename tests/phpunit/{unit-tests => integration}/Rest/Test_Rest_Pdf_Preview.php (99%) rename tests/phpunit/{unit-tests/test-api.php => integration/Test_Api.php} (99%) rename tests/phpunit/{unit-tests/test-autoloader.php => integration/Test_Autoloader.php} (97%) rename tests/phpunit/{unit-tests/test-bootstrap.php => integration/Test_Bootstrap.php} (98%) rename tests/phpunit/{unit-tests/test-deprecated.php => integration/Test_Deprecated.php} (97%) rename tests/phpunit/{unit-tests/test-pre-checks.php => integration/Test_Pre_Checks.php} (98%) rename tests/phpunit/{unit-tests => integration}/View/Test_View_System_Report.php (95%) delete mode 100644 tests/phpunit/unit-tests/test-ajax.php delete mode 100644 tests/phpunit/unit-tests/test-helper-misc.php diff --git a/tests/phpunit/integration/AjaxTestCase.php b/tests/phpunit/integration/AjaxTestCase.php new file mode 100644 index 000000000..bade45429 --- /dev/null +++ b/tests/phpunit/integration/AjaxTestCase.php @@ -0,0 +1,18 @@ +assertFixturesIntact(); + } +} diff --git a/tests/phpunit/unit-tests/Controller/Test_Controller_Custom_Fonts.php b/tests/phpunit/integration/Controller/Test_Controller_Custom_Fonts.php similarity index 99% rename from tests/phpunit/unit-tests/Controller/Test_Controller_Custom_Fonts.php rename to tests/phpunit/integration/Controller/Test_Controller_Custom_Fonts.php index 0fc151d97..7509148d4 100644 --- a/tests/phpunit/unit-tests/Controller/Test_Controller_Custom_Fonts.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Custom_Fonts.php @@ -8,7 +8,7 @@ use GFPDF\Model\Model_Custom_Fonts; use GPDFAPI; use WP_REST_Request; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -24,7 +24,7 @@ * @group controller * @group fonts */ -class Test_Controller_Custom_Fonts extends WP_UnitTestCase { +class Test_Controller_Custom_Fonts extends TestCase { /** * @var Controller_Custom_Fonts diff --git a/tests/phpunit/unit-tests/Controller/Test_Controller_Export_Entries.php b/tests/phpunit/integration/Controller/Test_Controller_Export_Entries.php similarity index 95% rename from tests/phpunit/unit-tests/Controller/Test_Controller_Export_Entries.php rename to tests/phpunit/integration/Controller/Test_Controller_Export_Entries.php index b31be3c56..5c3a7047e 100644 --- a/tests/phpunit/unit-tests/Controller/Test_Controller_Export_Entries.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Export_Entries.php @@ -2,7 +2,7 @@ namespace GFPDF\Controller; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -18,7 +18,7 @@ * @group controller * @group export */ -class Test_Controller_Export_Entries extends WP_UnitTestCase { +class Test_Controller_Export_Entries extends TestCase { public function test_add_pdfs_to_export_fields() { $form = apply_filters( 'gform_export_fields', $GLOBALS['GFPDF_Test']->form['all-form-fields'] ); diff --git a/tests/phpunit/unit-tests/Controller/Test_Controller_Pdf_Queue.php b/tests/phpunit/integration/Controller/Test_Controller_Pdf_Queue.php similarity index 98% rename from tests/phpunit/unit-tests/Controller/Test_Controller_Pdf_Queue.php rename to tests/phpunit/integration/Controller/Test_Controller_Pdf_Queue.php index dd2b006ce..8199b779d 100644 --- a/tests/phpunit/unit-tests/Controller/Test_Controller_Pdf_Queue.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Pdf_Queue.php @@ -1,6 +1,6 @@ misc = new Helper_Misc( $gfpdf->log, $gfpdf->gform, $gfpdf->data ); + } + + /** + * @dataProvider provider_get_contrast + */ + public function test_get_contrast( $expected, $hexcolor ) { + $this->assertEquals( $expected, $this->misc->get_contrast( $hexcolor ) ); + } + + public function provider_get_contrast() { + return [ + [ '#FFF', '#000000' ], + [ '#FFF', '#000' ], + [ '#FFF', '#222' ], + [ '#FFF', '#068a2b' ], + [ '#FFF', '#a70404' ], + [ '#000', '#fff' ], + [ '#000', '#FFFFFF' ], + [ '#000', '#999' ], + [ '#000', '#EEE' ], + [ '#000', '#CCC' ], + ]; + } + + /** + * @dataProvider provider_change_brightness + */ + public function test_change_brightness( $expected, $hexcolor, $diff ) { + $this->assertEquals( $expected, $this->misc->change_brightness( $hexcolor, $diff ) ); + } + + public function provider_change_brightness() { + return [ + [ '#0a0a0a', '#000000', 10 ], + [ '#0a0a0a', '#000', 10 ], + [ '#181818', '#222', -10 ], + [ '#2c2c2c', '#222', 10 ], + [ '#fefefe', '#CCC', 50 ], + [ '#9a9a9a', '#CCC', -50 ], + [ '#ffffff', '#FFFFFF', 25 ], + [ '#e6e6e6', '#FFF', -25 ], + ]; + } + + /** + * @dataProvider provider_get_background_and_border_contrast + */ + public function test_get_background_and_border_contrast( $expected, $hex ) { + $contrast = $this->misc->get_background_and_border_contrast( $hex ); + + $this->assertEquals( $expected[0], $contrast['background'] ); + $this->assertEquals( $expected[1], $contrast['border'] ); + } + + public function provider_get_background_and_border_contrast() { + return [ + [ [ '#ebebeb', '#c3c3c3' ], '#FFFFFF' ], + [ [ '#ebebeb', '#c3c3c3' ], '#FFF' ], + [ [ '#141414', '#3c3c3c' ], '#000000' ], + [ [ '#141414', '#3c3c3c' ], '#000' ], + [ [ '#e82828', '#ff5050' ], '#d41414' ], + [ [ '#295399', '#517bc1' ], '#153f85' ], + [ [ '#5cbb50', '#349328' ], '#70cf64' ], + [ [ '#dfdfdf', '#b7b7b7' ], '#f3f3f3' ], + ]; + } +} diff --git a/tests/phpunit/integration/Helper/Test_Helper_Misc_Config.php b/tests/phpunit/integration/Helper/Test_Helper_Misc_Config.php new file mode 100644 index 000000000..7734c3355 --- /dev/null +++ b/tests/phpunit/integration/Helper/Test_Helper_Misc_Config.php @@ -0,0 +1,104 @@ +misc = new Helper_Misc( $gfpdf->log, $gfpdf->gform, $gfpdf->data ); + } + + /** + * @dataProvider provider_update_deprecated_config + */ + public function test_update_deprecated_config( $expected, $value ) { + $this->assertEquals( $expected, $this->misc->update_deprecated_config( $value ) ); + } + + public function provider_update_deprecated_config() { + return [ + [ 'Yes', true ], + [ 'No', false ], + [ null, null ], + [ 'Other', 'Other' ], + [ [ 1, 2, 3 ], [ 1, 2, 3 ] ], + [ 'true', 'true' ], + [ 'false', 'false' ], + ]; + } + + /** + * @dataProvider provider_get_config_class_name + */ + public function test_get_config_class_name( $expected, $file ) { + global $gfpdf; + + $this->assertEquals( $expected, $gfpdf->templates->get_config_class_name( $file ) ); + } + + public function provider_get_config_class_name() { + return [ + [ 'Manage_Document', '/path/to/templates/manage-document.php' ], + [ 'Manage_Document', '/path/to/templates/manage_document.php' ], + [ 'Manage_Document', '/path/to/templates/manage document.php' ], + [ 'Superawesome_Working_Directory', '/my/path/superawesome-working-directory.php' ], + [ 'Template', 'template.php' ], + ]; + } + + public function test_backwards_compat_conversion() { + $settings = [ + 'irrelevant' => 'Yes', + ]; + + $compat = $this->misc->backwards_compat_conversion( $settings, [], [] ); + + $this->assertCount( 8, $compat ); + $this->assertArrayNotHasKey( 'irrelevant', $compat ); + $this->assertFalse( $compat['premium'] ); + $this->assertFalse( $compat['rtl'] ); + $this->assertFalse( $compat['security'] ); + $this->assertFalse( $compat['pdfa1b'] ); + $this->assertFalse( $compat['pdfx1a'] ); + $this->assertEquals( '', $compat['pdf_password'] ); + $this->assertEquals( '', $compat['pdf_privileges'] ); + $this->assertEquals( 96, $compat['dpi'] ); + + $settings = [ + 'advanced_template' => 'Yes', + 'rtl' => 'Yes', + 'image_dpi' => 300, + 'security' => 'Yes', + 'password' => 'password', + 'privileges' => 'privileges', + 'format' => 'PDFX1A', + ]; + + $compat = $this->misc->backwards_compat_conversion( $settings, [], [] ); + + $this->assertTrue( $compat['premium'] ); + $this->assertTrue( $compat['rtl'] ); + $this->assertTrue( $compat['security'] ); + $this->assertFalse( $compat['pdfa1b'] ); + $this->assertTrue( $compat['pdfx1a'] ); + $this->assertEquals( 'password', $compat['pdf_password'] ); + $this->assertEquals( 'privileges', $compat['pdf_privileges'] ); + $this->assertEquals( 300, $compat['dpi'] ); + } + + public function test_backwards_compat_output() { + $this->assertEquals( 'save', $this->misc->backwards_compat_output() ); + $this->assertEquals( 'view', $this->misc->backwards_compat_output( 'display' ) ); + $this->assertEquals( 'download', $this->misc->backwards_compat_output( 'download' ) ); + } +} diff --git a/tests/phpunit/integration/Helper/Test_Helper_Misc_Forms.php b/tests/phpunit/integration/Helper/Test_Helper_Misc_Forms.php new file mode 100644 index 000000000..a8fbac425 --- /dev/null +++ b/tests/phpunit/integration/Helper/Test_Helper_Misc_Forms.php @@ -0,0 +1,249 @@ +misc = new Helper_Misc( $gfpdf->log, $gfpdf->gform, $gfpdf->data ); + } + + public function test_array_unshift_assoc() { + $array = [ + 'item1' => 'Yes', + 'item2' => 'Maybe', + 'item3' => 'I do not know', + ]; + + $test = $this->misc->array_unshift_assoc( $array, 'item0', 'No' ); + + $this->assertEquals( 'No', reset( $test ) ); + $this->assertEquals( 'Yes', next( $test ) ); + $this->assertEquals( 'I do not know', end( $test ) ); + } + + /** + * @dataProvider provider_remove_extension_from_string + */ + public function test_remove_extension_from_string( $expected, $string, $type ) { + $this->assertEquals( $expected, $this->misc->remove_extension_from_string( $string, $type ) ); + } + + public function provider_remove_extension_from_string() { + return [ + [ 'mydocument', 'mydocument.pdf', '.pdf' ], + [ 'mydocument', 'mydocument.jpg', '.Jpg' ], + [ 'mydocument.pdf', 'mydocument.pdf', '.pda' ], + [ 'Helper_Document', 'Helper_Document.php', '.php' ], + [ 'カタ_Document', 'カタ_Document.php', '.php' ], + [ 'カタ_Document', 'カタ_Document.excel', '.excel' ], + [ 'Working', 'Working.excel', '.excel' ], + [ 'Working_漢字', 'Working_漢字.pdf', '.pdf' ], + ]; + } + + public function test_evaluate_conditional_logic() { + global $gfpdf; + + $form = $this->form( 'all-form-fields' ); + $entry = $this->entry( 'all-form-fields' ); + + $gfpdf->data->form_settings = []; + $gfpdf->data->form_settings[ $form['id'] ] = $form['gfpdf_form_settings']; + + $logic['actionType'] = 'show'; + $this->assertTrue( $this->misc->evaluate_conditional_logic( $logic, $entry ) ); + + $logic['actionType'] = 'hide'; + $this->assertFalse( $this->misc->evaluate_conditional_logic( $logic, $entry ) ); + } + + public function test_get_fields_sorted_by_id() { + $this->assertSame( 0, count( $this->misc->get_fields_sorted_by_id( 0 ) ) ); + + $form = $this->form( 'all-form-fields' ); + $fields = $this->misc->get_fields_sorted_by_id( $form['id'] ); + + $this->assertEquals( 56, count( $fields ) ); + $this->assertEquals( 'Section Break', $fields[10]->label ); + } + + /** + * @dataProvider provider_in_array + */ + public function test_in_array( $expected, $strict, $needle, $haystack ) { + $this->assertSame( $expected, $this->misc->in_array( $needle, $haystack, $strict ) ); + } + + public function provider_in_array() { + return [ + [ + true, + true, + 'find me', + [ + 'item 1', + 'item 2', + 'item 3' => [ 'test', 'find me' ], + 'item 4', + ], + ], + [ + false, + true, + 20, + [ + 'item 1', + 'item 2' => [ 'stuff', 'here', [ '20' ] ], + 'item 3', + ], + ], + [ + true, + false, + 20, + [ + 'item 1', + 'item 2' => [ 'stuff', 'here', [ '20' ] ], + 'item 3', + ], + ], + [ + true, + true, + 'Find Me', + [ + 'item 1' => [ 'hi', 'how', 'are', [ 'you' => [ 'going' ] ] ], + 'item 2' => [ 'stuff', 'here', [ 'Find Me' ] ], + 'item 3', + ], + ], + [ + true, + true, + 'Find Me', + [ + 'item 1' => [ 'hi', 'how', 'are', [ 'you' => [ 'going' => [ 'Find Me' ] ] ] ], + 'item 2' => [ 'stuff', 'here', [ 'wow' ] ], + 'item 3', + ], + ], + [ + false, + true, + 'find me', + [ + 'item 1', + 'item 2' => [ 'stuff', 'here', [ 'Find Me' ] ], + 'item 3', + ], + ], + ]; + } + + public function test_cleanup_dir() { + $data = GPDFAPI::get_data_class(); + $path = $data->template_location . 'folder/'; + wp_mkdir_p( $path ); + touch( $path . 'test' ); + + $this->assertFileExists( $path . 'test' ); + + $this->misc->cleanup_dir( $path ); + + $this->assertFileDoesNotExist( $path . 'test' ); + $this->assertDirectoryExists( $path ); + + rmdir( $path ); + + $path = sys_get_temp_dir() . '/folder/'; + wp_mkdir_p( $path ); + touch( $path . 'test' ); + $this->assertFileExists( $path . 'test' ); + + $this->misc->cleanup_dir( $path ); + + $this->assertFileExists( $path . 'test' ); + unlink( $path . 'test' ); + rmdir( $path ); + } + + public function test_rmdir() { + $data = GPDFAPI::get_data_class(); + $path = $data->template_location . 'folder/'; + wp_mkdir_p( $path ); + touch( $path . 'test' ); + + $this->assertFileExists( $path . 'test' ); + + $this->misc->rmdir( $path, false ); + + $this->assertFileDoesNotExist( $path . 'test' ); + $this->assertDirectoryExists( $path ); + + touch( $path . 'test' ); + + $this->assertFileExists( $path . 'test' ); + + $this->misc->rmdir( $path ); + + $this->assertFileDoesNotExist( $path . 'test' ); + $this->assertDirectoryDoesNotExist( $path ); + + $path = sys_get_temp_dir() . '/folder/'; + wp_mkdir_p( $path ); + $this->assertDirectoryExists( $path ); + + $results = $this->misc->rmdir( $path ); + $this->assertSame( 'gfpdf_rmdir_directory_not_approved', $results->get_error_code() ); + + $this->assertDirectoryExists( $path ); + rmdir( $path ); + } + + public function test_flatten_array() { + $test_array = [ + 'one' => 'first', + 'two' => 'second', + ]; + + $this->assertSame( [ 'one', 'two' ], $this->misc->flatten_array( $test_array ) ); + $this->assertSame( [ 'first', 'second' ], $this->misc->flatten_array( $test_array, 'values' ) ); + + $test_array = [ + 'top-one' => [ + 'one' => 'first', + ], + 'top-two' => [ + 'two' => 'second', + ], + ]; + + $this->assertSame( [ 'one', 'two' ], $this->misc->flatten_array( $test_array ) ); + $this->assertSame( [ 'first', 'second' ], $this->misc->flatten_array( $test_array, 'values' ) ); + + $test_array = [ + [ + 'top-one' => [ + 'one' => 'first', + ], + 'top-two' => [ + 'two' => 'second', + ], + ], + ]; + + $this->assertSame( [ 'top-one', 'top-two' ], $this->misc->flatten_array( $test_array ) ); + } +} diff --git a/tests/phpunit/integration/Helper/Test_Helper_Misc_Pages.php b/tests/phpunit/integration/Helper/Test_Helper_Misc_Pages.php new file mode 100644 index 000000000..44b9c9330 --- /dev/null +++ b/tests/phpunit/integration/Helper/Test_Helper_Misc_Pages.php @@ -0,0 +1,121 @@ +misc = new Helper_Misc( $gfpdf->log, $gfpdf->gform, $gfpdf->data ); + } + + private function minify( $html ) { + return preg_replace( + [ '/\n/', '/\t/', '/\>\s+\<' ], + $html + ); + } + + public function test_is_gfpdf_page() { + $this->assertFalse( $this->misc->is_gfpdf_page() ); + + set_current_screen( 'dashboard-user' ); + $this->assertFalse( $this->misc->is_gfpdf_page() ); + + $_GET['page'] = 'gfpdf-tools'; + $this->assertTrue( $this->misc->is_gfpdf_page() ); + + unset( $_GET['page'] ); + + $_GET['subview'] = 'PDF'; + $this->assertTrue( $this->misc->is_gfpdf_page() ); + } + + public function test_is_gfpdf_settings_tab() { + $this->assertFalse( $this->misc->is_gfpdf_settings_tab( 'general' ) ); + + set_current_screen( 'dashboard-user' ); + $_GET['subview'] = 'PDF'; + + $this->assertTrue( $this->misc->is_gfpdf_settings_tab( 'general' ) ); + + $this->assertFalse( $this->misc->is_gfpdf_settings_tab( 'tools' ) ); + + $_GET['tab'] = 'tools'; + $this->assertTrue( $this->misc->is_gfpdf_settings_tab( 'tools' ) ); + } + + /** + * @dataProvider provider_test_fix_header_footer + */ + public function test_fix_header_footer( $expected, $html ) { + $test_html = $this->misc->fix_header_footer( $html ); + $minified_html = $this->minify( $test_html ); + + $this->assertEquals( $expected, $minified_html ); + } + + public function provider_test_fix_header_footer() { + return [ + [ + '

My Image

', + 'My Image', + ], + [ + '', + '', + ], + [ + '

IntroMy ImageOutro

', + 'Intro My Image Outro', + ], + [ + '

This is bold. This is italics

', + 'This is bold. This is italics ', + ], + [ + '

My Image

', + 'My Image', + ], + [ + '

My Image

', + 'My Image', + ], + [ + '

Nothing

', + 'Nothing', + ], + [ + '', + '', + ], + [ + '

My Image

', + 'My Image', + ], + [ + '

My Image

', + 'My Image', + ], + ]; + } + + public function test_fix_header_footer_path() { + $html = $this->misc->fix_header_footer( 'My Image' ); + $this->assertFalse( strpos( PDF_PLUGIN_URL, $html ) ); + + $html = $this->misc->fix_header_footer( 'My Image' ); + $minified_html = $this->minify( $html ); + $this->assertEquals( '

My Image

', $minified_html ); + } +} diff --git a/tests/phpunit/unit-tests/test-helper-mpdf.php b/tests/phpunit/integration/Helper/Test_Helper_Mpdf.php similarity index 89% rename from tests/phpunit/unit-tests/test-helper-mpdf.php rename to tests/phpunit/integration/Helper/Test_Helper_Mpdf.php index 030fead60..c952e6da0 100644 --- a/tests/phpunit/unit-tests/test-helper-mpdf.php +++ b/tests/phpunit/integration/Helper/Test_Helper_Mpdf.php @@ -1,9 +1,8 @@ _setRole( 'administrator' ); + + try { + $this->_handleAjax( 'gfpdf_save_core_font' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '401', $e->getMessage() ); + + $_POST['nonce'] = wp_create_nonce( 'gfpdf_ajax_nonce' ); + $_POST['font_name'] = 'nothing'; + + try { + $this->_handleAjax( 'gfpdf_save_core_font' ); + } catch ( WPAjaxDieContinueException $e ) { + } + + $this->assertFalse( json_decode( $this->_last_response ) ); + $this->_last_response = ''; + + $_POST['font_name'] = 'Aegean.otf'; + + $api_response = function () { + return [ + 'response' => [ 'code' => 200 ], + 'body' => '', + ]; + }; + + add_filter( 'pre_http_request', $api_response ); + + try { + $this->_handleAjax( 'gfpdf_save_core_font' ); + } catch ( WPAjaxDieContinueException $e ) { + } + + remove_filter( 'pre_http_request', $api_response ); + + $this->assertTrue( json_decode( $this->_last_response ) ); + } +} diff --git a/tests/phpunit/integration/Model/Test_Model_Form_Settings_Ajax.php b/tests/phpunit/integration/Model/Test_Model_Form_Settings_Ajax.php new file mode 100644 index 000000000..9b842fa09 --- /dev/null +++ b/tests/phpunit/integration/Model/Test_Model_Form_Settings_Ajax.php @@ -0,0 +1,207 @@ +form_id = GFAPI::add_form( $json ); + } + + public function test_change_state_pdf_setting() { + global $gfpdf; + + $this->_setRole( 'administrator' ); + $_POST['fid'] = 0; + $_POST['pid'] = $this->pid; + + try { + $this->_handleAjax( 'gfpdf_change_state' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '401', $e->getMessage() ); + + $_POST['nonce'] = wp_create_nonce( "gfpdf_state_nonce_{$_POST['fid']}_{$_POST['pid']}" ); + + try { + $this->_handleAjax( 'gfpdf_change_state' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '500', $e->getMessage() ); + + $_POST['fid'] = $this->form_id; + $_POST['nonce'] = wp_create_nonce( "gfpdf_state_nonce_{$_POST['fid']}_{$_POST['pid']}" ); + + try { + $this->_handleAjax( 'gfpdf_change_state' ); + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + + $this->assertArrayHasKey( 'state', $response ); + $this->assertEquals( 'Inactive', $response['state'] ); + + unset( $gfpdf->data->form_settings ); + $pdf = $gfpdf->options->get_pdf( $this->form_id, $this->pid ); + $this->assertFalse( $pdf['active'] ); + + $this->_last_response = ''; + + try { + $this->_handleAjax( 'gfpdf_change_state' ); + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + + $this->assertArrayHasKey( 'state', $response ); + $this->assertEquals( 'Active', $response['state'] ); + + unset( $gfpdf->data->form_settings ); + $pdf = $gfpdf->options->get_pdf( $this->form_id, $this->pid ); + $this->assertTrue( $pdf['active'] ); + } + + public function test_render_template_fields() { + try { + $this->_handleAjax( 'gfpdf_get_template_fields' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '401', $e->getMessage() ); + + $this->_setRole( 'administrator' ); + + try { + $this->_handleAjax( 'gfpdf_get_template_fields' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '401', $e->getMessage() ); + + $_POST['nonce'] = wp_create_nonce( 'gfpdf_ajax_nonce' ); + + try { + $this->_handleAjax( 'gfpdf_get_template_fields' ); + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + + $this->assertArrayHasKey( 'fields', $response ); + $this->assertArrayHasKey( 'editors', $response ); + $this->assertArrayHasKey( 'editor_init', $response ); + $this->assertArrayHasKey( 'template_type', $response ); + } + + public function test_duplicate_gf_pdf_settings() { + global $gfpdf; + + $this->_setRole( 'administrator' ); + $_POST['fid'] = 0; + $_POST['pid'] = $this->pid; + + try { + $this->_handleAjax( 'gfpdf_list_duplicate' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '401', $e->getMessage() ); + + $_POST['nonce'] = wp_create_nonce( "gfpdf_duplicate_nonce_{$_POST['fid']}_{$_POST['pid']}" ); + + try { + $this->_handleAjax( 'gfpdf_list_duplicate' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '500', $e->getMessage() ); + + $_POST['fid'] = $this->form_id; + $_POST['nonce'] = wp_create_nonce( "gfpdf_duplicate_nonce_{$_POST['fid']}_{$_POST['pid']}" ); + + try { + $this->_handleAjax( 'gfpdf_list_duplicate' ); + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + + $this->assertArrayHasKey( 'msg', $response ); + $this->assertArrayHasKey( 'pid', $response ); + $this->assertArrayHasKey( 'name', $response ); + $this->assertArrayHasKey( 'dup_nonce', $response ); + $this->assertArrayHasKey( 'del_nonce', $response ); + $this->assertArrayHasKey( 'state_nonce', $response ); + + unset( $gfpdf->data->form_settings ); + $pdf1 = $gfpdf->options->get_pdf( $this->form_id, $this->pid ); + $pdf2 = $gfpdf->options->get_pdf( $this->form_id, $response['pid'] ); + + $this->assertEquals( $pdf1['name'] . ' (copy)', $pdf2['name'] ); + $this->assertEquals( $pdf1['template'], $pdf2['template'] ); + $this->assertEquals( $pdf1['filename'], $pdf2['filename'] ); + + $this->_last_response = ''; + } + + public function test_delete_gf_pdf_setting() { + global $gfpdf; + + $pdf = $gfpdf->options->get_pdf( $this->form_id, $this->pid ); + $this->assertEquals( 'My First PDF Template', $pdf['name'] ); + + $this->_setRole( 'administrator' ); + $_POST['fid'] = 0; + $_POST['pid'] = $this->pid; + + try { + $this->_handleAjax( 'gfpdf_list_delete' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '401', $e->getMessage() ); + + $_POST['nonce'] = wp_create_nonce( "gfpdf_delete_nonce_{$_POST['fid']}_{$_POST['pid']}" ); + + try { + $this->_handleAjax( 'gfpdf_list_delete' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '500', $e->getMessage() ); + + $_POST['fid'] = $this->form_id; + $_POST['nonce'] = wp_create_nonce( "gfpdf_delete_nonce_{$_POST['fid']}_{$_POST['pid']}" ); + + try { + $this->_handleAjax( 'gfpdf_list_delete' ); + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + + $this->assertArrayHasKey( 'msg', $response ); + + unset( $gfpdf->data->form_settings ); + $pdf = $gfpdf->options->get_pdf( $this->form_id, $this->pid ); + $this->assertTrue( is_wp_error( $pdf ) ); + } +} diff --git a/tests/phpunit/unit-tests/Model/Test_Model_Mergetags.php b/tests/phpunit/integration/Model/Test_Model_Mergetags.php similarity index 99% rename from tests/phpunit/unit-tests/Model/Test_Model_Mergetags.php rename to tests/phpunit/integration/Model/Test_Model_Mergetags.php index ef096abc1..42f73f0d0 100644 --- a/tests/phpunit/unit-tests/Model/Test_Model_Mergetags.php +++ b/tests/phpunit/integration/Model/Test_Model_Mergetags.php @@ -6,7 +6,7 @@ use GFPDF\Controller\Controller_Shortcodes; use GFPDF\Helper\Helper_Url_Signer; use GPDFAPI; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -22,7 +22,7 @@ * @group model * @group tags */ -class Test_Model_Mergetags extends WP_UnitTestCase { +class Test_Model_Mergetags extends TestCase { /** * @var Controller_Shortcodes diff --git a/tests/phpunit/unit-tests/Model/Test_Model_Pdf.php b/tests/phpunit/integration/Model/Test_Model_Pdf.php similarity index 98% rename from tests/phpunit/unit-tests/Model/Test_Model_Pdf.php rename to tests/phpunit/integration/Model/Test_Model_Pdf.php index 70601fcb5..2bb318ae1 100644 --- a/tests/phpunit/unit-tests/Model/Test_Model_Pdf.php +++ b/tests/phpunit/integration/Model/Test_Model_Pdf.php @@ -6,7 +6,7 @@ use GFPDF\Controller\Controller_PDF; use GFPDF\Helper\Helper_Url_Signer; use GFPDF\View\View_PDF; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -22,7 +22,7 @@ * @group model * @group pdfs */ -class Test_Model_PDF extends WP_UnitTestCase { +class Test_Model_PDF extends TestCase { /** * @var Controller_PDF diff --git a/tests/phpunit/unit-tests/Model/Test_Model_Pdf_Meta_Box.php b/tests/phpunit/integration/Model/Test_Model_Pdf_Meta_Box.php similarity index 97% rename from tests/phpunit/unit-tests/Model/Test_Model_Pdf_Meta_Box.php rename to tests/phpunit/integration/Model/Test_Model_Pdf_Meta_Box.php index c72ab1ef1..437e0a927 100644 --- a/tests/phpunit/unit-tests/Model/Test_Model_Pdf_Meta_Box.php +++ b/tests/phpunit/integration/Model/Test_Model_Pdf_Meta_Box.php @@ -5,7 +5,7 @@ use GFPDF\Controller\Controller_PDF; use GFPDF\Helper\Helper_Url_Signer; use GFPDF\View\View_PDF; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -21,7 +21,7 @@ * @group model * @group pdf */ -class Test_Model_Pdf_Meta_Box extends WP_UnitTestCase { +class Test_Model_Pdf_Meta_Box extends TestCase { /** * @var Controller_PDF diff --git a/tests/phpunit/unit-tests/Model/Test_Model_Settings.php b/tests/phpunit/integration/Model/Test_Model_Settings.php similarity index 98% rename from tests/phpunit/unit-tests/Model/Test_Model_Settings.php rename to tests/phpunit/integration/Model/Test_Model_Settings.php index ce9047dc8..1d6ddff4a 100644 --- a/tests/phpunit/unit-tests/Model/Test_Model_Settings.php +++ b/tests/phpunit/integration/Model/Test_Model_Settings.php @@ -7,7 +7,7 @@ use GFPDF\Helper\Helper_Logger; use GFPDF\Helper\Helper_Notices; use GFPDF\Helper\Helper_Singleton; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -21,7 +21,7 @@ * @group model * @group settings */ -class Test_Model_Settings extends WP_UnitTestCase { +class Test_Model_Settings extends TestCase { /** * @var Model_Settings diff --git a/tests/phpunit/integration/Model/Test_Model_Settings_Ajax.php b/tests/phpunit/integration/Model/Test_Model_Settings_Ajax.php new file mode 100644 index 000000000..66c9cbd80 --- /dev/null +++ b/tests/phpunit/integration/Model/Test_Model_Settings_Ajax.php @@ -0,0 +1,33 @@ +_setRole( 'administrator' ); + + try { + $this->_handleAjax( 'gfpdf_deactivate_license' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '401', $e->getMessage() ); + + $_POST['nonce'] = wp_create_nonce( 'gfpdf_deactivate_license' ); + + try { + $this->_handleAjax( 'gfpdf_deactivate_license' ); + } catch ( WPAjaxDieContinueException $e ) { + } + + $this->assertStringContainsString( 'An unknown error occurred', json_decode( $this->_last_response )->error ); + } +} diff --git a/tests/phpunit/unit-tests/Model/Test_Model_System_Report.php b/tests/phpunit/integration/Model/Test_Model_System_Report.php similarity index 96% rename from tests/phpunit/unit-tests/Model/Test_Model_System_Report.php rename to tests/phpunit/integration/Model/Test_Model_System_Report.php index 6ebe08f00..94da822e8 100644 --- a/tests/phpunit/unit-tests/Model/Test_Model_System_Report.php +++ b/tests/phpunit/integration/Model/Test_Model_System_Report.php @@ -5,7 +5,7 @@ use GFPDF\Helper\Helper_Templates; use GFPDF_Major_Compatibility_Checks; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -21,7 +21,7 @@ * @group model * @group system-report */ -class Test_Model_System_Report extends WP_UnitTestCase { +class Test_Model_System_Report extends TestCase { /** * @var Model_System_Report diff --git a/tests/phpunit/integration/Model/Test_Model_Templates_Ajax.php b/tests/phpunit/integration/Model/Test_Model_Templates_Ajax.php new file mode 100644 index 000000000..a0935974a --- /dev/null +++ b/tests/phpunit/integration/Model/Test_Model_Templates_Ajax.php @@ -0,0 +1,91 @@ +_setRole( 'administrator' ); + + try { + $this->_handleAjax( 'gfpdf_upload_template' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '401', $e->getMessage() ); + + $_POST['nonce'] = wp_create_nonce( 'gfpdf_ajax_nonce' ); + + try { + $this->_handleAjax( 'gfpdf_upload_template' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '400', $e->getMessage() ); + } + + public function test_ajax_process_delete_template() { + global $gfpdf; + + $this->_setRole( 'administrator' ); + + try { + $this->_handleAjax( 'gfpdf_delete_template' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '401', $e->getMessage() ); + + $_POST['nonce'] = wp_create_nonce( 'gfpdf_ajax_nonce' ); + + try { + $this->_handleAjax( 'gfpdf_delete_template' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '400', $e->getMessage() ); + + $file = $gfpdf->data->template_location . 'zadani.php'; + touch( $file ); + + $_POST['id'] = 'zadani'; + + try { + $this->_handleAjax( 'gfpdf_delete_template' ); + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + unset( $this->_last_response ); + + $this->assertTrue( $response ); + $this->assertFileDoesNotExist( $file ); + } + + public function test_ajax_process_build_template_options_html() { + $this->_setRole( 'administrator' ); + + try { + $this->_handleAjax( 'gfpdf_get_template_options' ); + } catch ( WPAjaxDieStopException $e ) { + } + + $this->assertEquals( '401', $e->getMessage() ); + + $_POST['nonce'] = wp_create_nonce( 'gfpdf_ajax_nonce' ); + + try { + $this->_handleAjax( 'gfpdf_get_template_options' ); + } catch ( WPAjaxDieContinueException $e ) { + } + + $this->assertNotFalse( $this->_last_response, '' ); + } +} diff --git a/tests/phpunit/unit-tests/test-pdf.php b/tests/phpunit/integration/Model/Test_PDF.php similarity index 99% rename from tests/phpunit/unit-tests/test-pdf.php rename to tests/phpunit/integration/Model/Test_PDF.php index 893512d60..85d3383ef 100644 --- a/tests/phpunit/unit-tests/test-pdf.php +++ b/tests/phpunit/integration/Model/Test_PDF.php @@ -1,7 +1,6 @@ import_form(); - } - - /** - * Fix for WordPress 4.7 which seems to close the MySQLi connection before - * the class is correctly setup - * - * @since 4.1 - */ - public static function set_up_before_class() { - global $wpdb; - $wpdb->suppress_errors = false; - $wpdb->show_errors = true; - $wpdb->db_connect(); - - parent::set_up_before_class(); - } - - /** - * Load the JSON data and import it into Gravity Forms - * - * @since 4.0 - */ - private function import_form() { - $json = json_decode( trim( file_get_contents( PDF_PLUGIN_DIR . '/tools/phpunit/data/forms/form-settings.json' ) ), true ); - $this->form_id = GFAPI::add_form( $json ); - } - - /** - * Test our Gravity Forms PDF Settings configuration state change - * - * @class Model_Form_Settings - * - * @since 4.0 - */ - public function test_change_state_pdf_setting() { - global $gfpdf; - - /* set up our post data and role */ - $this->_setRole( 'administrator' ); - $_POST['fid'] = 0; - $_POST['pid'] = $this->pid; - - /** - * Check for nonce failure - */ - try { - $this->_handleAjax( 'gfpdf_change_state' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '401', $e->getMessage() ); - - /** - * Check for update failure (invalid form ID) - */ - $_POST['nonce'] = wp_create_nonce( "gfpdf_state_nonce_{$_POST['fid']}_{$_POST['pid']}" ); - - try { - $this->_handleAjax( 'gfpdf_change_state' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '500', $e->getMessage() ); - - /** - * Set up a real response - */ - $_POST['fid'] = $this->form_id; - $_POST['nonce'] = wp_create_nonce( "gfpdf_state_nonce_{$_POST['fid']}_{$_POST['pid']}" ); - - try { - $this->_handleAjax( 'gfpdf_change_state' ); - } catch ( WPAjaxDieContinueException $e ) { - /* do nothing (error expected) */ - } - - /* Get the response */ - $response = json_decode( $this->_last_response, true ); - - /* Test the response is accurate */ - $this->assertArrayHasKey( 'state', $response ); - $this->assertEquals( 'Inactive', $response['state'] ); - - /* Test the function performed correctly */ - unset( $gfpdf->data->form_settings ); - $pdf = $gfpdf->options->get_pdf( $this->form_id, $this->pid ); - $this->assertFalse( $pdf['active'] ); - - /* reset the last response */ - $this->_last_response = ''; - - /** - * Reverse the process - */ - try { - $this->_handleAjax( 'gfpdf_change_state' ); - } catch ( WPAjaxDieContinueException $e ) { - /* do nothing (error expected) */ - } - - /* Get the response */ - $response = json_decode( $this->_last_response, true ); - - /* Test the response is accurate */ - $this->assertArrayHasKey( 'state', $response ); - $this->assertEquals( 'Active', $response['state'] ); - - /* Test the function performed correctly */ - unset( $gfpdf->data->form_settings ); - $pdf = $gfpdf->options->get_pdf( $this->form_id, $this->pid ); - $this->assertTrue( $pdf['active'] ); - } - - /** - * Ensure we correctly authorise the end user - * - * @class Model_Form_Settings - * - * @since 4.1 - */ - public function test_render_template_fields() { - - /* Check for authentication failure */ - try { - $this->_handleAjax( 'gfpdf_get_template_fields' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '401', $e->getMessage() ); - - /* become admin */ - $this->_setRole( 'administrator' ); - - /* Check for nonce failure */ - try { - $this->_handleAjax( 'gfpdf_get_template_fields' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '401', $e->getMessage() ); - - /* Check for missing v3 configuration file failure */ - $_POST['nonce'] = wp_create_nonce( 'gfpdf_ajax_nonce' ); - - try { - $this->_handleAjax( 'gfpdf_get_template_fields' ); - } catch ( WPAjaxDieContinueException $e ) { - /* do nothing (error expected) */ - } - - /* Get the response */ - $response = json_decode( $this->_last_response, true ); - - $this->assertArrayHasKey( 'fields', $response ); - $this->assertArrayHasKey( 'editors', $response ); - $this->assertArrayHasKey( 'editor_init', $response ); - $this->assertArrayHasKey( 'template_type', $response ); - } - - /** - * Test our Gravity Forms PDF Settings configuration duplication functionality - * - * @class Model_Form_Settings - * - * @since 4.0 - */ - public function test_duplicate_gf_pdf_settings() { - global $gfpdf; - - /* set up our post data and role */ - $this->_setRole( 'administrator' ); - $_POST['fid'] = 0; - $_POST['pid'] = $this->pid; - - /** - * Check for nonce failure - */ - try { - $this->_handleAjax( 'gfpdf_list_duplicate' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '401', $e->getMessage() ); - - /** - * Check for update failure (invalid form ID) - */ - $_POST['nonce'] = wp_create_nonce( "gfpdf_duplicate_nonce_{$_POST['fid']}_{$_POST['pid']}" ); - - try { - $this->_handleAjax( 'gfpdf_list_duplicate' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '500', $e->getMessage() ); - - /** - * Set up a real response - */ - $_POST['fid'] = $this->form_id; - $_POST['nonce'] = wp_create_nonce( "gfpdf_duplicate_nonce_{$_POST['fid']}_{$_POST['pid']}" ); - - try { - $this->_handleAjax( 'gfpdf_list_duplicate' ); - } catch ( WPAjaxDieContinueException $e ) { - /* do nothing (error expected) */ - } - - /* Get the response */ - $response = json_decode( $this->_last_response, true ); - - /* Test the response is accurate */ - $this->assertArrayHasKey( 'msg', $response ); - $this->assertArrayHasKey( 'pid', $response ); - $this->assertArrayHasKey( 'name', $response ); - $this->assertArrayHasKey( 'dup_nonce', $response ); - $this->assertArrayHasKey( 'del_nonce', $response ); - $this->assertArrayHasKey( 'state_nonce', $response ); - - /* Test the function performed correctly */ - unset( $gfpdf->data->form_settings ); - $pdf1 = $gfpdf->options->get_pdf( $this->form_id, $this->pid ); - $pdf2 = $gfpdf->options->get_pdf( $this->form_id, $response['pid'] ); - - $this->assertEquals( $pdf1['name'] . ' (copy)', $pdf2['name'] ); - $this->assertEquals( $pdf1['template'], $pdf2['template'] ); - $this->assertEquals( $pdf1['filename'], $pdf2['filename'] ); - - /* reset the last response */ - $this->_last_response = ''; - } - - /** - * Test our Gravity Forms PDF Settings configuration duplication functionality - * - * @class Model_Form_Settings - * - * @since 4.0 - */ - public function test_delete_gf_pdf_setting() { - global $gfpdf; - - /* test configuration exists already */ - $pdf = $gfpdf->options->get_pdf( $this->form_id, $this->pid ); - $this->assertEquals( 'My First PDF Template', $pdf['name'] ); - - /* set up our post data and role */ - $this->_setRole( 'administrator' ); - $_POST['fid'] = 0; - $_POST['pid'] = $this->pid; - - /** - * Check for nonce failure - */ - try { - $this->_handleAjax( 'gfpdf_list_delete' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '401', $e->getMessage() ); - - /** - * Check for update failure (invalid form ID) - */ - $_POST['nonce'] = wp_create_nonce( "gfpdf_delete_nonce_{$_POST['fid']}_{$_POST['pid']}" ); - - try { - $this->_handleAjax( 'gfpdf_list_delete' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '500', $e->getMessage() ); - - /** - * Set up a real response - */ - $_POST['fid'] = $this->form_id; - $_POST['nonce'] = wp_create_nonce( "gfpdf_delete_nonce_{$_POST['fid']}_{$_POST['pid']}" ); - - try { - $this->_handleAjax( 'gfpdf_list_delete' ); - } catch ( WPAjaxDieContinueException $e ) { - /* do nothing (error expected) */ - } - - /* Get the response */ - $response = json_decode( $this->_last_response, true ); - - /* Test the response is accurate */ - $this->assertArrayHasKey( 'msg', $response ); - - /* Test the function performed correctly */ - unset( $gfpdf->data->form_settings ); - $pdf = $gfpdf->options->get_pdf( $this->form_id, $this->pid ); - $this->assertTrue( is_wp_error( $pdf ) ); - } - - /** - * Testing Model_Templates.php wp_ajax_gfpdf_upload_template - * - * Because this AJAX endpoint is suppose to have a zip file POSTed, - * and because we cannot mock \GravityPdf\Upload\File directly (see test-templates.php for specific tests) - * we're just testing this endpoint requires authentication AND throws an error when - * no file is posted. - * - * @since 4.1 - */ - public function test_ajax_process_uploaded_template() { - - /* set up our post data and role */ - $this->_setRole( 'administrator' ); - - /* Check for nonce failure */ - try { - $this->_handleAjax( 'gfpdf_upload_template' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '401', $e->getMessage() ); - - /* Set up a bad request by excluding required fields */ - $_POST['nonce'] = wp_create_nonce( 'gfpdf_ajax_nonce' ); - - try { - $this->_handleAjax( 'gfpdf_upload_template' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '400', $e->getMessage() ); - } - - /** - * Check that we can successfully delete a PDF template through this AJAX endpoint - * - * @since 4.1 - */ - public function test_ajax_process_delete_template() { - global $gfpdf; - - /* set up our post data and role */ - $this->_setRole( 'administrator' ); - - /* Check for nonce failure */ - try { - $this->_handleAjax( 'gfpdf_delete_template' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '401', $e->getMessage() ); - - /* Set up a bad request by excluding required fields */ - $_POST['nonce'] = wp_create_nonce( 'gfpdf_ajax_nonce' ); - - try { - $this->_handleAjax( 'gfpdf_delete_template' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '400', $e->getMessage() ); - - /* Create a test template and actually delete it */ - $file = $gfpdf->data->template_location . 'zadani.php'; - touch( $file ); - - $_POST['id'] = 'zadani'; - - try { - $this->_handleAjax( 'gfpdf_delete_template' ); - } catch ( WPAjaxDieContinueException $e ) { - /* do nothing (error expected) */ - } - - $response = json_decode( $this->_last_response, true ); - unset( $this->_last_response ); - - $this->assertTrue( $response ); - $this->assertFileDoesNotExist( $file ); - } - - /** - * - * @since 4.1 - */ - public function test_ajax_process_build_template_options_html() { - /* set up our post data and role */ - $this->_setRole( 'administrator' ); - - /* Check for nonce failure */ - try { - $this->_handleAjax( 'gfpdf_get_template_options' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '401', $e->getMessage() ); - - /* Set up a bad request by excluding required fields */ - $_POST['nonce'] = wp_create_nonce( 'gfpdf_ajax_nonce' ); - - try { - $this->_handleAjax( 'gfpdf_get_template_options' ); - } catch ( WPAjaxDieContinueException $e ) { - /* do nothing (error expected) */ - } - - $this->assertNotFalse( $this->_last_response, '' ); - } - - public function test_ajax_process_license_deactivation() { - /* set up our post data and role */ - $this->_setRole( 'administrator' ); - - /* Check for nonce failure */ - try { - $this->_handleAjax( 'gfpdf_deactivate_license' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '401', $e->getMessage() ); - - /* Setup a bad request */ - $_POST['nonce'] = wp_create_nonce( 'gfpdf_deactivate_license' ); - - try { - $this->_handleAjax( 'gfpdf_deactivate_license' ); - } catch ( WPAjaxDieContinueException $e ) { - /* do nothing (error expected) */ - } - - $this->assertStringContainsString( 'An unknown error occurred', json_decode( $this->_last_response )->error ); - } - - public function test_ajax_save_core_font() { - /* set up our post data and role */ - $this->_setRole( 'administrator' ); - - /* Check for nonce failure */ - try { - $this->_handleAjax( 'gfpdf_save_core_font' ); - } catch ( WPAjaxDieStopException $e ) { - /* do nothing (error expected) */ - } - - $this->assertEquals( '401', $e->getMessage() ); - - /* Setup a bad request */ - $_POST['nonce'] = wp_create_nonce( 'gfpdf_ajax_nonce' ); - $_POST['font_name'] = 'nothing'; - - try { - $this->_handleAjax( 'gfpdf_save_core_font' ); - } catch ( WPAjaxDieContinueException $e ) { - /* do nothing (error expected) */ - } - - $this->assertFalse( json_decode( $this->_last_response ) ); - $this->_last_response = ''; - - /* Test that a core font API download request gets made */ - $_POST['font_name'] = 'Aegean.otf'; - - $api_response = function() { - return [ - 'response' => [ 'code' => 200 ], - 'body' => '', - ]; - }; - - add_filter( 'pre_http_request', $api_response ); - - try { - $this->_handleAjax( 'gfpdf_save_core_font' ); - } catch ( WPAjaxDieContinueException $e ) { - /* do nothing (error expected) */ - } - - remove_filter( 'pre_http_request', $api_response ); - - $this->assertTrue( json_decode( $this->_last_response ) ); - } -} diff --git a/tests/phpunit/unit-tests/test-helper-misc.php b/tests/phpunit/unit-tests/test-helper-misc.php deleted file mode 100644 index 4155bd65e..000000000 --- a/tests/phpunit/unit-tests/test-helper-misc.php +++ /dev/null @@ -1,735 +0,0 @@ -misc = new Helper_Misc( $gfpdf->log, $gfpdf->gform, $gfpdf->data ); - } - - /** - * Create our testing data - * - * @since 4.0 - */ - private function create_form_and_entries() { - global $gfpdf; - - $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; - $entry = $GLOBALS['GFPDF_Test']->entries['all-form-fields'][0]; - - $gfpdf->data->form_settings = []; - $gfpdf->data->form_settings[ $form['id'] ] = $form['gfpdf_form_settings']; - - return [ - 'form' => $form, - 'entry' => $entry, - ]; - } - - /** - * Ensure we correctly determine when we are on a Gravity PDF admin page - * - * @since 4.0 - */ - public function test_is_gfpdf_page() { - - $this->assertFalse( $this->misc->is_gfpdf_page() ); - - /* Set admin page */ - set_current_screen( 'dashboard-user' ); - $this->assertFalse( $this->misc->is_gfpdf_page() ); - - /* Set up PDF page */ - $_GET['page'] = 'gfpdf-tools'; - $this->assertTrue( $this->misc->is_gfpdf_page() ); - - unset( $_GET['page'] ); - - $_GET['subview'] = 'PDF'; - $this->assertTrue( $this->misc->is_gfpdf_page() ); - } - - /** - * Check if we are on the current settings tab - * - * @since 4.0 - */ - public function test_is_gfpdf_settings_tab() { - $this->assertFalse( $this->misc->is_gfpdf_settings_tab( 'general' ) ); - - /* Set admin page */ - set_current_screen( 'dashboard-user' ); - $_GET['subview'] = 'PDF'; - - $this->assertTrue( $this->misc->is_gfpdf_settings_tab( 'general' ) ); - - /* Try a different tab */ - $this->assertFalse( $this->misc->is_gfpdf_settings_tab( 'tools' ) ); - - $_GET['tab'] = 'tools'; - $this->assertTrue( $this->misc->is_gfpdf_settings_tab( 'tools' ) ); - } - - /** - * Check if our HTML DOM manipulator correctly adds the class "header-footer-img" to tags - * - * @param $expected - * @param $html - * - * @since 4.0 - * - * @dataProvider provider_test_fix_header_footer - */ - public function test_fix_header_footer( $expected, $html ) { - $test_html = $this->misc->fix_header_footer( $html ); - $minified_html = $this->minify( $test_html ); - - $this->assertEquals( $expected, $minified_html ); - } - - protected function minify($html) { - $html = preg_replace( - [ '/\n/', '/\t/', '/\>\s+\<' ], - $html - ); - - return $html; - } - - /** - * Dataprovider for our fix_header_footer method - * - * @since 4.0 - */ - public function provider_test_fix_header_footer() { - return [ - [ - '

My Image

', - 'My Image', - ], - [ - '', - '', - ], - [ - '

IntroMy ImageOutro

', - 'Intro My Image Outro', - ], - [ - '

This is bold. This is italics

', - 'This is bold. This is italics ', - ], - [ - '

My Image

', - 'My Image', - ], - [ - '

My Image

', - 'My Image', - ], - [ - '

Nothing

', - 'Nothing', - ], - [ - '', - '', - ], - [ - '

My Image

', - 'My Image', - ], - [ - '

My Image

', - 'My Image', - ], - ]; - } - - /** - * Check if our HTML DOM manipulator correctly changes local URLs to Paths - * - * @since 4.0 - */ - public function test_fix_header_footer_path() { - - $html = $this->misc->fix_header_footer( 'My Image' ); - $this->assertFalse( strpos( PDF_PLUGIN_URL, $html ) ); - - $html = $this->misc->fix_header_footer( 'My Image' ); - $minified_html = $this->minify( $html ); - $this->assertEquals( '

My Image

', $minified_html ); - } - - /** - * Check that we can push an associated array item onto the beginning of an existing array - * - * @since 4.0 - */ - public function test_array_unshift_assoc() { - $array = [ - 'item1' => 'Yes', - 'item2' => 'Maybe', - 'item3' => 'I do not know', - ]; - - $test = $this->misc->array_unshift_assoc( $array, 'item0', 'No' ); - - $this->assertEquals( 'No', reset( $test ) ); - $this->assertEquals( 'Yes', next( $test ) ); - $this->assertEquals( 'I do not know', end( $test ) ); - } - - /** - * Test we are correctly stripping an extension from the end of a string - * - * @param $expected - * @param $string - * @param $type - * - * @since 4.0 - * - * @dataProvider provider_remove_extension_from_string - */ - public function test_remove_extension_from_string( $expected, $string, $type ) { - $this->assertEquals( $expected, $this->misc->remove_extension_from_string( $string, $type ) ); - } - - /** - * Data provider for our remove_extension_from_string method - * - * @return array - * - * @since 4.0 - */ - public function provider_remove_extension_from_string() { - return [ - [ 'mydocument', 'mydocument.pdf', '.pdf' ], - [ 'mydocument', 'mydocument.jpg', '.Jpg' ], - [ 'mydocument.pdf', 'mydocument.pdf', '.pda' ], - [ 'Helper_Document', 'Helper_Document.php', '.php' ], - [ 'カタ_Document', 'カタ_Document.php', '.php' ], - [ 'カタ_Document', 'カタ_Document.excel', '.excel' ], - [ 'Working', 'Working.excel', '.excel' ], - [ 'Working_漢字', 'Working_漢字.pdf', '.pdf' ], - ]; - } - - /** - * Test we correctly convert our v3 config data into the appropriate value - * - * @param $expected - * @param $value - * - * @since 4.0 - * - * @dataProvider provider_update_deprecated_config - */ - public function test_update_deprecated_config( $expected, $value ) { - $this->assertEquals( $expected, $this->misc->update_deprecated_config( $value ) ); - } - - /** - * Data provider for testing update_deprecated_config() - * - * @return array - * - * @since 4.0 - */ - public function provider_update_deprecated_config() { - return [ - [ 'Yes', true ], - [ 'No', false ], - [ null, null ], - [ 'Other', 'Other' ], - [ [ 1, 2, 3 ], [ 1, 2, 3 ] ], - [ 'true', 'true' ], - [ 'false', 'false' ], - ]; - } - - /** - * Check our contrast checker returns the correct contrasting colours - * - * @param string $expected The results we expect - * @param string $hexcolor The colour to test - * - * @dataProvider provider_get_contrast - * @since 4.0 - */ - public function test_get_contrast( $expected, $hexcolor ) { - $this->assertEquals( $expected, $this->misc->get_contrast( $hexcolor ) ); - } - - /** - * Data provider for testing get_contrast() method - * - * @return array - * - * @since 4.0 - */ - public function provider_get_contrast() { - return [ - [ '#FFF', '#000000' ], - [ '#FFF', '#000' ], - [ '#FFF', '#222' ], - [ '#FFF', '#068a2b' ], - [ '#FFF', '#a70404' ], - [ '#000', '#fff' ], - [ '#000', '#FFFFFF' ], - [ '#000', '#999' ], - [ '#000', '#EEE' ], - [ '#000', '#CCC' ], - ]; - } - - /** - * Check our contrast checker returns the correct contrasting colours - * - * @param string $expected The results we expect - * @param string $hexcolor The colour to test - * @param integer $diff Whether to go lighter or darker - * - * @dataProvider provider_change_brightness - * @since 4.0 - */ - public function test_change_brightness( $expected, $hexcolor, $diff ) { - $this->assertEquals( $expected, $this->misc->change_brightness( $hexcolor, $diff ) ); - } - - /** - * Data provider for testing provider_change_brightness() method - * - * @return array - * - * @since 4.0 - */ - public function provider_change_brightness() { - return [ - [ '#0a0a0a', '#000000', 10 ], - [ '#0a0a0a', '#000', 10 ], - [ '#181818', '#222', -10 ], - [ '#2c2c2c', '#222', 10 ], - [ '#fefefe', '#CCC', 50 ], - [ '#9a9a9a', '#CCC', -50 ], - [ '#ffffff', '#FFFFFF', 25 ], - [ '#e6e6e6', '#FFF', -25 ], - ]; - } - - /** - * Test the basics of the evaluate_conditional_logic() method - * when used with show/hide logic - * - * @since 4.0 - */ - public function test_evaluate_conditional_logic() { - $data = $this->create_form_and_entries(); - - $logic['actionType'] = 'show'; - - $this->assertTrue( $this->misc->evaluate_conditional_logic( $logic, $data['entry'] ) ); - - $logic['actionType'] = 'hide'; - - $this->assertFalse( $this->misc->evaluate_conditional_logic( $logic, $data['entry'] ) ); - } - - /** - * Ensure we correctly return an appropriate class name based on the file path given - * - * @param string $expected The expected value - * @param string $file The test path - * - * @dataProvider provider_get_config_class_name - * - * @since 4.0 - */ - public function test_get_config_class_name( $expected, $file ) { - global $gfpdf; - - $this->assertEquals( $expected, $gfpdf->templates->get_config_class_name( $file ) ); - } - - /** - * Data provider for our get_config_class_name() test - * - * @return array - * - * @since 4.0 - */ - public function provider_get_config_class_name() { - return [ - [ 'Manage_Document', '/path/to/templates/manage-document.php' ], - [ 'Manage_Document', '/path/to/templates/manage_document.php' ], - [ 'Manage_Document', '/path/to/templates/manage document.php' ], - [ 'Superawesome_Working_Directory', '/my/path/superawesome-working-directory.php' ], - [ 'Template', 'template.php' ], - ]; - } - - /** - * Check we correctly parse the hex code and spit out the correct background and border values - * - * @param string $expected - * @param string $hex - * - * @dataProvider provider_get_background_and_border_contrast - * - * @since 4.0 - */ - public function test_get_background_and_border_contrast( $expected, $hex ) { - $contrast = $this->misc->get_background_and_border_contrast( $hex ); - - $this->assertEquals( $expected[0], $contrast['background'] ); - $this->assertEquals( $expected[1], $contrast['border'] ); - } - - /** - * Our test data for the get_background_and_border_contrast() method - * - * @return array - */ - public function provider_get_background_and_border_contrast() { - return [ - [ [ '#ebebeb', '#c3c3c3' ], '#FFFFFF' ], - [ [ '#ebebeb', '#c3c3c3' ], '#FFF' ], - [ [ '#141414', '#3c3c3c' ], '#000000' ], - [ [ '#141414', '#3c3c3c' ], '#000' ], - [ [ '#e82828', '#ff5050' ], '#d41414' ], - [ [ '#295399', '#517bc1' ], '#153f85' ], - [ [ '#5cbb50', '#349328' ], '#70cf64' ], - [ [ '#dfdfdf', '#b7b7b7' ], '#f3f3f3' ], - ]; - } - - /** - * Check we are correctly getting our form fields by ID - * - * @since 4.0 - */ - public function test_get_fields_sorted_by_id() { - - /* Check for non-existent form */ - $this->assertSame( 0, count( $this->misc->get_fields_sorted_by_id( 0 ) ) ); - - /* Check for real form and verify the results */ - $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; - - $fields = $this->misc->get_fields_sorted_by_id( $form['id'] ); - - $this->assertEquals( 56, count( $fields ) ); - $this->assertEquals( 'Section Break', $fields[10]->label ); - } - - /** - * Check if our backwards compatible settings conversion works correctly - * - * @since 4.0 - */ - public function test_backwards_compat_conversion() { - $settings = [ - 'irrelevant' => 'Yes', - ]; - - /* Check all the defaults work as expected */ - $compat = $this->misc->backwards_compat_conversion( $settings, [], [] ); - - $this->assertCount( 8, $compat ); - $this->assertArrayNotHasKey( 'irrelevant', $compat ); - $this->assertFalse( $compat['premium'] ); - $this->assertFalse( $compat['rtl'] ); - $this->assertFalse( $compat['security'] ); - $this->assertFalse( $compat['pdfa1b'] ); - $this->assertFalse( $compat['pdfx1a'] ); - $this->assertEquals( '', $compat['pdf_password'] ); - $this->assertEquals( '', $compat['pdf_privileges'] ); - $this->assertEquals( 96, $compat['dpi'] ); - - /* Check all the settings get correctly converted */ - $settings = [ - 'advanced_template' => 'Yes', - 'rtl' => 'Yes', - 'image_dpi' => 300, - 'security' => 'Yes', - 'password' => 'password', - 'privileges' => 'privileges', - 'format' => 'PDFX1A', - ]; - - $compat = $this->misc->backwards_compat_conversion( $settings, [], [] ); - - $this->assertTrue( $compat['premium'] ); - $this->assertTrue( $compat['rtl'] ); - $this->assertTrue( $compat['security'] ); - $this->assertFalse( $compat['pdfa1b'] ); - $this->assertTrue( $compat['pdfx1a'] ); - $this->assertEquals( 'password', $compat['pdf_password'] ); - $this->assertEquals( 'privileges', $compat['pdf_privileges'] ); - $this->assertEquals( 300, $compat['dpi'] ); - } - - /** - * Check if our backwards compatible output functions work correctly - * - * @since 4.0 - */ - public function test_backwards_compat_output() { - $this->assertEquals( 'save', $this->misc->backwards_compat_output() ); - $this->assertEquals( 'view', $this->misc->backwards_compat_output( 'display' ) ); - $this->assertEquals( 'download', $this->misc->backwards_compat_output( 'download' ) ); - } - - /** - * Check our recursive in_array() method works as expected - * - * @param boolean $expected - * @param boolean $strict - * @param mixed $needle - * @param array $haystack - * - * @dataProvider provider_in_array - * - * @since 4.0 - */ - public function test_in_array( $expected, $strict, $needle, $haystack ) { - $this->assertSame( $expected, $this->misc->in_array( $needle, $haystack, $strict ) ); - } - - public function provider_in_array() { - return [ - - /* basic multi-dimensional search */ - [ - true, - true, - 'find me', - [ - 'item 1', - 'item 2', - 'item 3' => [ 'test', 'find me' ], - 'item 4', - ], - ], - - /* type check (strict) */ - [ - false, - true, - 20, - [ - 'item 1', - 'item 2' => [ 'stuff', 'here', [ '20' ] ], - 'item 3', - ], - ], - - /* type check (not strict) */ - [ - true, - false, - 20, - [ - 'item 1', - 'item 2' => [ 'stuff', 'here', [ '20' ] ], - 'item 3', - ], - ], - - /* deep multi-dimensional array */ - [ - true, - true, - 'Find Me', - [ - 'item 1' => [ 'hi', 'how', 'are', [ 'you' => [ 'going' ] ] ], - 'item 2' => [ 'stuff', 'here', [ 'Find Me' ] ], - 'item 3', - ], - ], - - /* deep multi-dimensional array */ - [ - true, - true, - 'Find Me', - [ - 'item 1' => [ 'hi', 'how', 'are', [ 'you' => [ 'going' => [ 'Find Me' ] ] ] ], - 'item 2' => [ 'stuff', 'here', [ 'wow' ] ], - 'item 3', - ], - ], - - /* ensure case sensitive match */ - [ - false, - true, - 'find me', - [ - 'item 1', - 'item 2' => [ 'stuff', 'here', [ 'Find Me' ] ], - 'item 3', - ], - ], - ]; - } - - /** - * Test that the everything inside a directory gets removed - * - * @since 4.0 - */ - public function test_cleanup_dir() { - - /* Create our test data */ - $data = \GPDFAPI::get_data_class(); - $path = $data->template_location .'folder/'; - wp_mkdir_p( $path ); - touch( $path . 'test' ); - - /* Ensure it created correctly */ - $this->assertFileExists( $path . 'test' ); - - /* Run our test */ - $this->misc->cleanup_dir( $path ); - - /* Check the file was deleted but the directory still exists */ - $this->assertFileDoesNotExist( $path . 'test' ); - $this->assertDirectoryExists( $path ); - - rmdir( $path ); - - /* Verify we cannot delete folders outside those the plugin manages */ - $path = sys_get_temp_dir() . '/folder/'; - wp_mkdir_p( $path ); - touch( $path . 'test' ); - $this->assertFileExists( $path . 'test' ); - - $this->misc->cleanup_dir( $path ); - - $this->assertFileExists( $path . 'test' ); - unlink( $path . 'test' ); - rmdir( $path ); - } - - public function test_rmdir() { - /* Create our test data */ - $data = \GPDFAPI::get_data_class(); - $path = $data->template_location .'folder/'; - wp_mkdir_p( $path ); - touch( $path . 'test' ); - - /* Ensure it created correctly */ - $this->assertFileExists( $path . 'test' ); - - /* Run our test but don't delete the top-level folder */ - $this->misc->rmdir( $path, false ); - - $this->assertFileDoesNotExist( $path . 'test' ); - $this->assertDirectoryExists( $path ); - - /* Setup and run out test again, but delete the top-level directory as well */ - touch( $path . 'test' ); - - /* Ensure it created correctly */ - $this->assertFileExists( $path . 'test' ); - - /* Run our test and delete the top-level folder */ - $this->misc->rmdir( $path ); - - $this->assertFileDoesNotExist( $path . 'test' ); - $this->assertDirectoryDoesNotExist( $path ); - - /* Verify we cannot delete folders outside those the plugin manages */ - $path = sys_get_temp_dir() . '/folder/'; - wp_mkdir_p( $path ); - $this->assertDirectoryExists( $path ); - - $results = $this->misc->rmdir( $path ); - $this->assertSame( 'gfpdf_rmdir_directory_not_approved', $results->get_error_code() ); - - $this->assertDirectoryExists( $path ); - rmdir( $path ); - } - - /** - * @since 7.0 - */ - public function test_flatten_array() { - /* Check a single dimensional array */ - $test_array = [ - 'one' => 'first', - 'two' => 'second', - ]; - - $this->assertSame( [ 'one', 'two' ], $this->misc->flatten_array( $test_array ) ); - $this->assertSame( [ 'first', 'second' ], $this->misc->flatten_array( $test_array, 'values' ) ); - - /* Check a multi dimensional array */ - $test_array = [ - 'top-one' => [ - 'one' => 'first', - ], - 'top-two' => [ - 'two' => 'second', - ], - ]; - - $this->assertSame( [ 'one', 'two' ], $this->misc->flatten_array( $test_array ) ); - $this->assertSame( [ 'first', 'second' ], $this->misc->flatten_array( $test_array, 'values' ) ); - - /* Check a multi-multi dimensional array */ - $test_array = [ - [ - 'top-one' => [ - 'one' => 'first', - ], - 'top-two' => [ - 'two' => 'second', - ], - ], - ]; - - $this->assertSame( [ 'top-one', 'top-two' ], $this->misc->flatten_array( $test_array ) ); - } -} diff --git a/tools/phpunit/bootstrap.php b/tools/phpunit/bootstrap.php index 97cb55196..d1f995dd3 100644 --- a/tools/phpunit/bootstrap.php +++ b/tools/phpunit/bootstrap.php @@ -108,6 +108,7 @@ public function load_test_infrastructure() { require_once $root . '/Concerns/HasGfpdfFixtures.php'; require_once $root . '/integration/TestCase.php'; + require_once $root . '/integration/AjaxTestCase.php'; } /** diff --git a/tools/phpunit/config-multisite.xml b/tools/phpunit/config-multisite.xml index 38e401164..8b9c2d9d9 100644 --- a/tools/phpunit/config-multisite.xml +++ b/tools/phpunit/config-multisite.xml @@ -12,7 +12,6 @@ - ../../tests/phpunit/unit-tests/ ../../tests/phpunit/integration/ diff --git a/tools/phpunit/config.xml b/tools/phpunit/config.xml index 87434d449..698c88973 100644 --- a/tools/phpunit/config.xml +++ b/tools/phpunit/config.xml @@ -8,7 +8,6 @@ > - ../../tests/phpunit/unit-tests/ ../../tests/phpunit/integration/ From 4294be81bbb57bda436077b3df05a38022e83c9a Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 15:29:53 +1000 Subject: [PATCH 07/45] chore(phpunit): adopt GF_UnitTest_Factory, drain GFAPI::add_* calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of the PHPUnit refactor: route every per-test form/entry creation through the existing GF_UnitTest_Factory (tools/phpunit/gravityforms-factory.php) via a new HasGfpdfFixtures-style trait. - Add `\GFPDF\Tests\Concerns\UsesFactory` exposing `$this->gf_factory()`. Named `gf_factory()` not `factory()` because WP_UnitTestCase already has a `protected static function factory()` — overriding it from a trait with an instance method fails silently with exit 255 at PHPUnit class load. - Wire the trait into both TestCase and AjaxTestCase so every integration test gets it automatically. README updated with the convention and the collision rationale. - Convert all 24 direct GFAPI::add_form() / add_entry() call sites in integration/ (7 files) to `$this->gf_factory()->form->create()` / `->entry->create()`. Zero direct calls remain — grep gate enforceable. - Test_Slow_PDF_Processes: move the per-test font-copy loop into `set_up_before_class()` / `tear_down_after_class()` (copy once per class instead of once per test). Cuts the group from 3.76s → 3.49s. Inline cleanup since the trait would have one consumer (the no-slop call from Phase 1 still applies — CleansFilesystem stays unshipped). - Fix `switch_to_blog( $this->factory()->blog->create() )` in Test_PDF.php to use `self::factory()` (the WP static factory the bulk rename touched). Test count unchanged at 1119 (8 skipped integration, 1 skipped multisite). Runtime: 27.2s integration / 28.3s multisite — same as Phase 2 (factory adoption is structural prep for Phase 4 coverage, not a perf change). Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/phpunit/Concerns/UsesFactory.php | 28 ++++++++++ tests/phpunit/README.md | 51 +++++++++++++------ tests/phpunit/integration/AjaxTestCase.php | 2 + .../Helper/Fields/Test_Field_Markup.php | 12 ++--- .../Helper/Fields/Test_Field_Option.php | 4 +- .../Helper/Fields/Test_Field_Product.php | 4 +- .../Helper/Fields/Test_Field_Products.php | 4 +- .../integration/Helper/Test_Form_Data.php | 24 ++++----- .../Model/Test_Model_Form_Settings_Ajax.php | 2 +- tests/phpunit/integration/Model/Test_PDF.php | 6 +-- .../Model/Test_Slow_PDF_Processes.php | 48 ++++++++--------- tests/phpunit/integration/TestCase.php | 2 + tools/phpunit/bootstrap.php | 1 + 13 files changed, 115 insertions(+), 73 deletions(-) create mode 100644 tests/phpunit/Concerns/UsesFactory.php diff --git a/tests/phpunit/Concerns/UsesFactory.php b/tests/phpunit/Concerns/UsesFactory.php new file mode 100644 index 000000000..9d6a0323c --- /dev/null +++ b/tests/phpunit/Concerns/UsesFactory.php @@ -0,0 +1,28 @@ +gfpdf_factory ) { + $this->gfpdf_factory = new GF_UnitTest_Factory(); + } + + return $this->gfpdf_factory; + } +} diff --git a/tests/phpunit/README.md b/tests/phpunit/README.md index ea183db08..19e08490c 100644 --- a/tests/phpunit/README.md +++ b/tests/phpunit/README.md @@ -14,13 +14,17 @@ tests/phpunit/ ├── COVERAGE_BASELINE.md ├── README.md ← you are here ├── Concerns/ ← shared traits (NOT discovered by PHPUnit) -│ └── HasGfpdfFixtures.php -├── integration/ ← new location, mirrors src/ 1:1 -│ ├── TestCase.php -│ └── Statics/ -│ ├── Test_Cache.php -│ └── Test_kses.php -└── unit-tests/ ← legacy location, drained subdir-by-subdir +│ ├── HasGfpdfFixtures.php +│ └── UsesFactory.php +└── integration/ ← mirrors src/ 1:1 + ├── TestCase.php + ├── AjaxTestCase.php + ├── Controller/ + ├── Helper/ + ├── Model/ + ├── Rest/ + ├── Statics/ + └── View/ ``` `unit-tests/` and `integration/` co-exist during Phases 1–2. Both directories @@ -41,9 +45,14 @@ Class name = `Test_`; method names use `test_` snake_case to keep ## Base class -Tests extend `\GFPDF\Tests\Integration\TestCase`, which extends `WP_UnitTestCase` -and `use`s `\GFPDF\Tests\Concerns\HasGfpdfFixtures`. The trait provides: +| Need | Base class | +| :--- | :--- | +| Standard integration test | `\GFPDF\Tests\Integration\TestCase` | +| Test that dispatches a `wp_ajax_*` action via `_handleAjax()` | `\GFPDF\Tests\Integration\AjaxTestCase` | + +Both extend the WordPress stock test cases and `use` two traits: +`\GFPDF\Tests\Concerns\HasGfpdfFixtures` provides: - `$this->form( 'all-form-fields' )` — shared form fixture loaded once per suite. - `$this->entry( 'all-form-fields', 0 )` — shared entry fixture. - `$this->gfpdf()` — the `GFPDF\Router` DI container (same as the `$gfpdf` global). @@ -51,8 +60,11 @@ and `use`s `\GFPDF\Tests\Concerns\HasGfpdfFixtures`. The trait provides: cross-test fixture mutation. Override `set_up()` only if you call `parent::set_up()`. -A second base for `WP_Ajax_UnitTestCase`-derived tests will be added when Phase 2 -splits `test-ajax.php`. +`\GFPDF\Tests\Concerns\UsesFactory` provides: +- `$this->gf_factory()` — returns the `GF_UnitTest_Factory` (`tools/phpunit/gravityforms-factory.php`). + Use this for per-test forms/entries. Named `gf_factory()` to avoid colliding + with `WP_UnitTestCase::factory()` (the static WP factory accessed via + `self::factory()->user->create()` etc.). ## Writing a new test @@ -96,10 +108,19 @@ batches of entries into `$GLOBALS['GFPDF_Test']` once per suite. Use the trait accessors rather than the global directly — same source, but failures point to the missing key instead of throwing an undefined-index notice. -**Avoid calling `GFAPI::add_form()` / `GFAPI::add_entry()` from a test body** — -prefer the existing `GF_UnitTest_Factory` (`tools/phpunit/gravityforms-factory.php`) -when you need a per-test form/entry. Phase 3 of the refactor will enforce this -across the suite and ship a trait-based accessor. +**Never call `GFAPI::add_form()` / `GFAPI::add_entry()` directly from a test body.** +Use the factory: + +```php +$form_id = $this->gf_factory()->form->create( [], $form ); +$entry['form_id'] = $form_id; +$entry_id = $this->gf_factory()->entry->create( $entry ); +``` + +**Class-scoped fixtures (font copies, expensive file setup) belong in +`set_up_before_class()`**, not in instance `set_up()`. Use instance scope only when +the test actually mutates the fixture. See `Model/Test_Slow_PDF_Processes.php` for +the pattern. ## The "test if non-trivial" rule diff --git a/tests/phpunit/integration/AjaxTestCase.php b/tests/phpunit/integration/AjaxTestCase.php index bade45429..07d52d43c 100644 --- a/tests/phpunit/integration/AjaxTestCase.php +++ b/tests/phpunit/integration/AjaxTestCase.php @@ -5,11 +5,13 @@ namespace GFPDF\Tests\Integration; use GFPDF\Tests\Concerns\HasGfpdfFixtures; +use GFPDF\Tests\Concerns\UsesFactory; use WP_Ajax_UnitTestCase; abstract class AjaxTestCase extends WP_Ajax_UnitTestCase { use HasGfpdfFixtures; + use UsesFactory; public function set_up() { parent::set_up(); diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Markup.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Markup.php index 0cc8735c1..6d1bf76dc 100644 --- a/tests/phpunit/integration/Helper/Fields/Test_Field_Markup.php +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Markup.php @@ -34,9 +34,9 @@ public function test_repeater_field_markup() { $form = $GLOBALS['GFPDF_Test']->form['repeater-consent-form']; $entry = $GLOBALS['GFPDF_Test']->entries['repeater-consent-form'][0]; - $form_id = GFAPI::add_form( $form ); + $form_id = $this->gf_factory()->form->create([], $form); $entry['form_id'] = $form_id; - $entry_id = GFAPI::add_entry( $entry ); + $entry_id = $this->gf_factory()->entry->create($entry); $repeater = new GF_Field_Repeater( $form['fields'][1] ); @@ -57,9 +57,9 @@ public function test_consent_field() { $form = $GLOBALS['GFPDF_Test']->form['repeater-consent-form']; $entry = $GLOBALS['GFPDF_Test']->entries['repeater-consent-form'][0]; - $form_id = GFAPI::add_form( $form ); + $form_id = $this->gf_factory()->form->create([], $form); $entry['form_id'] = $form_id; - $entry_id = GFAPI::add_entry( $entry ); + $entry_id = $this->gf_factory()->entry->create($entry); $consent = new GF_Field_Consent( $form['fields'][0] ); @@ -77,9 +77,9 @@ public function test_maximum_allowed_css_each_field(){ $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; $entry = $GLOBALS['GFPDF_Test']->entries['all-form-fields'][0]; - $form_id = GFAPI::add_form( $form ); + $form_id = $this->gf_factory()->form->create([], $form); $entry['form_id'] = $form_id; - $entry_id = GFAPI::add_entry( $entry ); + $entry_id = $this->gf_factory()->entry->create($entry); /* Verify classes are truncated at 8 */ $text_field = new GF_Field_Text( $form['fields'][0] ); diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Option.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Option.php index 1402902cb..8da7081dc 100644 --- a/tests/phpunit/integration/Helper/Fields/Test_Field_Option.php +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Option.php @@ -38,9 +38,9 @@ public function set_up() { $this->form = $GLOBALS['GFPDF_Test']->form['non-group-products-form']; $this->entry = $GLOBALS['GFPDF_Test']->entries['non-group-products-form'][0]; - $form_id = \GFAPI::add_form( $this->form ); + $form_id = $this->gf_factory()->form->create([], $this->form); $this->entry['form_id'] = $form_id; - $entry_id = \GFAPI::add_entry( $this->entry); + $entry_id = $this->gf_factory()->entry->create($this->entry); $this->pdf_field = new Field_Option( new \GF_Field_Option( $this->form['fields'][4] ), \GFAPI::get_entry( $entry_id ), \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); } diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Product.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Product.php index 5ffb2a54f..608ffbdf2 100644 --- a/tests/phpunit/integration/Helper/Fields/Test_Field_Product.php +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Product.php @@ -42,9 +42,9 @@ public function set_up() { $this->form = $GLOBALS['GFPDF_Test']->form['non-group-products-form']; $entry = $GLOBALS['GFPDF_Test']->entries['non-group-products-form'][0]; - $form_id = \GFAPI::add_form( $this->form ); + $form_id = $this->gf_factory()->form->create([], $this->form); $entry['form_id'] = $form_id; - $this->entry_id = \GFAPI::add_entry( $entry ); + $this->entry_id = $this->gf_factory()->entry->create($entry); $this->entry = $entry; parent::set_up(); diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Products.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Products.php index 68a566136..5ff2855bf 100644 --- a/tests/phpunit/integration/Helper/Fields/Test_Field_Products.php +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Products.php @@ -41,9 +41,9 @@ public function set_up() { $this->form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; $this->entry = $GLOBALS['GFPDF_Test']->entries['all-form-fields'][0]; - $form_id = GFAPI::add_form( $this->form ); + $form_id = $this->gf_factory()->form->create([], $this->form); $this->entry['form_id'] = $form_id; - $entry_id = GFAPI::add_entry( $this->entry ); + $entry_id = $this->gf_factory()->entry->create($this->entry); $this->pdf_field = new Field_Products( new \GF_Field_Product(), GFAPI::get_entry( $entry_id ), GPDFAPI::get_form_class(), GPDFAPI::get_misc_class() ); } diff --git a/tests/phpunit/integration/Helper/Test_Form_Data.php b/tests/phpunit/integration/Helper/Test_Form_Data.php index c58514758..6407b8e74 100644 --- a/tests/phpunit/integration/Helper/Test_Form_Data.php +++ b/tests/phpunit/integration/Helper/Test_Form_Data.php @@ -267,17 +267,15 @@ public function test_field_number() { */ public function test_field_number_currency() { $form_json = json_decode( trim( file_get_contents( PDF_PLUGIN_DIR . '/tools/phpunit/data/forms/number-fields.json' ) ), true ); - $form_id = GFAPI::add_form( $form_json ); + $form_id = $this->gf_factory()->form->create([], $form_json); - $entry_id = GFAPI::add_entry( - [ + $entry_id = $this->gf_factory()->entry->create([ 'form_id' => $form_id, 'currency' => 'EUR', '1' => 1000.10, '2' => 2000.10, '3' => 3000.10, - ] - ); + ]); $form_data = GPDFAPI::get_form_data( $entry_id ); @@ -285,15 +283,13 @@ public function test_field_number_currency() { $this->assertEquals( '2000,1', $form_data['field'][2] ); $this->assertEquals( '3.000,10 €', $form_data['field'][3] ); - $entry_id = GFAPI::add_entry( - [ + $entry_id = $this->gf_factory()->entry->create([ 'form_id' => $form_id, 'currency' => 'AUD', '1' => 1000.10, '2' => 2000.10, '3' => 3000.10, - ] - ); + ]); $form_data = GPDFAPI::get_form_data( $entry_id ); @@ -1537,7 +1533,7 @@ public function test_empty_fields() { public function test_euro_product_data() { $json = json_decode( trim( file_get_contents( PDF_PLUGIN_DIR . '/tools/phpunit/data/entries/all-form-euro-product-entry.json' ) ), true ); $json['form_id'] = $this->form['id']; - $entry_id = GFAPI::add_entry( $json ); + $entry_id = $this->gf_factory()->entry->create($json); $entry = GFAPI::get_entry( $entry_id ); $form_data = GFPDFEntryDetail::lead_detail_grid_array( $this->form['id'], $entry ); $products = $form_data['products']; @@ -1571,9 +1567,9 @@ public function test_consent_field_data() { $form = $GLOBALS['GFPDF_Test']->form['repeater-consent-form']; $entry = $GLOBALS['GFPDF_Test']->entries['repeater-consent-form'][0]; - $form_id = GFAPI::add_form( $form ); + $form_id = $this->gf_factory()->form->create([], $form); $entry['form_id'] = $form_id; - $entry_id = GFAPI::add_entry( $entry ); + $entry_id = $this->gf_factory()->entry->create($entry); $form_data = GPDFAPI::get_form_data( $entry_id ); @@ -1589,9 +1585,9 @@ public function test_repeater_field_data() { $form = $GLOBALS['GFPDF_Test']->form['repeater-consent-form']; $entry = $GLOBALS['GFPDF_Test']->entries['repeater-consent-form'][0]; - $form_id = GFAPI::add_form( $form ); + $form_id = $this->gf_factory()->form->create([], $form); $entry['form_id'] = $form_id; - $entry_id = GFAPI::add_entry( $entry ); + $entry_id = $this->gf_factory()->entry->create($entry); $form_data = GPDFAPI::get_form_data( $entry_id ); diff --git a/tests/phpunit/integration/Model/Test_Model_Form_Settings_Ajax.php b/tests/phpunit/integration/Model/Test_Model_Form_Settings_Ajax.php index 9b842fa09..d34978c36 100644 --- a/tests/phpunit/integration/Model/Test_Model_Form_Settings_Ajax.php +++ b/tests/phpunit/integration/Model/Test_Model_Form_Settings_Ajax.php @@ -19,7 +19,7 @@ public function set_up() { parent::set_up(); $json = json_decode( trim( file_get_contents( PDF_PLUGIN_DIR . '/tools/phpunit/data/forms/form-settings.json' ) ), true ); - $this->form_id = GFAPI::add_form( $json ); + $this->form_id = $this->gf_factory()->form->create([], $json); } public function test_change_state_pdf_setting() { diff --git a/tests/phpunit/integration/Model/Test_PDF.php b/tests/phpunit/integration/Model/Test_PDF.php index 85d3383ef..a4cdfc817 100644 --- a/tests/phpunit/integration/Model/Test_PDF.php +++ b/tests/phpunit/integration/Model/Test_PDF.php @@ -386,7 +386,7 @@ public function test_multisite_signed_url_access() { ); } - switch_to_blog( $this->factory()->blog->create() ); + switch_to_blog( self::factory()->blog->create() ); gf_upgrade()->install(); /* Setup some test data */ @@ -395,10 +395,10 @@ public function test_multisite_signed_url_access() { $entry = $results['entry']; $entry['form_id'] = $results['form']['id']; - $form_id = GFAPI::add_form( $results['form'] ); + $form_id = $this->gf_factory()->form->create([], $results['form']); $entry = $results['entry']; $entry['form_id'] = $form_id; - $entry_id = GFAPI::add_entry( $entry ); + $entry_id = $this->gf_factory()->entry->create($entry); $options = GPDFAPI::get_options_class(); $options->set_plugin_settings(); diff --git a/tests/phpunit/integration/Model/Test_Slow_PDF_Processes.php b/tests/phpunit/integration/Model/Test_Slow_PDF_Processes.php index 18ea02db1..d50274e9a 100644 --- a/tests/phpunit/integration/Model/Test_Slow_PDF_Processes.php +++ b/tests/phpunit/integration/Model/Test_Slow_PDF_Processes.php @@ -62,46 +62,38 @@ class Test_Slow_PDF_Processes extends TestCase { * * @since 4.0 */ - public function set_up() { + public static function set_up_before_class() { + parent::set_up_before_class(); + global $gfpdf; + foreach ( glob( PDF_PLUGIN_DIR . '/tools/phpunit/data/fonts/*.[tT][tT][fF]' ) ?: [] as $font ) { + @copy( $font, $gfpdf->data->template_font_location . basename( $font ) ); + } + } - /* run parent method */ - parent::set_up(); + public static function tear_down_after_class() { + global $gfpdf; + foreach ( glob( $gfpdf->data->template_font_location . '*.[tT][tT][fF]' ) ?: [] as $font ) { + @unlink( $font ); + } - /* Setup our test classes */ - $this->model = new Model_PDF( $gfpdf->gform, $gfpdf->log, $gfpdf->options, $gfpdf->data, $gfpdf->misc, $gfpdf->notices, $gfpdf->templates, new Helper_Url_Signer() ); - $this->view = new View_PDF( [], $gfpdf->gform, $gfpdf->log, $gfpdf->options, $gfpdf->data, $gfpdf->misc, $gfpdf->templates ); + parent::tear_down_after_class(); + } - $this->controller = new Controller_PDF( $this->model, $this->view, $gfpdf->gform, $gfpdf->log, $gfpdf->misc ); + public function set_up() { + global $gfpdf; - $fonts = glob( PDF_PLUGIN_DIR . '/tools/phpunit/data/fonts/' . '*.[tT][tT][fF]' ); - $fonts = ( is_array( $fonts ) ) ? $fonts : []; + parent::set_up(); - foreach ( $fonts as $font ) { - $font_name = basename( $font ); - /* phpcs:disable */ - @copy( $font, $gfpdf->data->template_font_location . $font_name ); - /* phpcs:enable */ - } + $this->model = new Model_PDF( $gfpdf->gform, $gfpdf->log, $gfpdf->options, $gfpdf->data, $gfpdf->misc, $gfpdf->notices, $gfpdf->templates, new Helper_Url_Signer() ); + $this->view = new View_PDF( [], $gfpdf->gform, $gfpdf->log, $gfpdf->options, $gfpdf->data, $gfpdf->misc, $gfpdf->templates ); + $this->controller = new Controller_PDF( $this->model, $this->view, $gfpdf->gform, $gfpdf->log, $gfpdf->misc ); error_reporting( E_ALL & ~E_NOTICE ); } - /** - * @since 5.0 - */ public function tear_down() { - global $gfpdf; - - $fonts = glob( $gfpdf->data->template_font_location . '*.[tT][tT][fF]' ); - $fonts = ( is_array( $fonts ) ) ? $fonts : []; - - foreach ( $fonts as $font ) { - @unlink( $font ); - } - parent::tear_down(); - error_reporting( E_ALL ); } diff --git a/tests/phpunit/integration/TestCase.php b/tests/phpunit/integration/TestCase.php index 1b328505d..270cad726 100644 --- a/tests/phpunit/integration/TestCase.php +++ b/tests/phpunit/integration/TestCase.php @@ -5,11 +5,13 @@ namespace GFPDF\Tests\Integration; use GFPDF\Tests\Concerns\HasGfpdfFixtures; +use GFPDF\Tests\Concerns\UsesFactory; use WP_UnitTestCase; abstract class TestCase extends WP_UnitTestCase { use HasGfpdfFixtures; + use UsesFactory; public function set_up() { parent::set_up(); diff --git a/tools/phpunit/bootstrap.php b/tools/phpunit/bootstrap.php index d1f995dd3..a26d11911 100644 --- a/tools/phpunit/bootstrap.php +++ b/tools/phpunit/bootstrap.php @@ -107,6 +107,7 @@ public function load_test_infrastructure() { $root = $this->plugin_dir . '/tests/phpunit'; require_once $root . '/Concerns/HasGfpdfFixtures.php'; + require_once $root . '/Concerns/UsesFactory.php'; require_once $root . '/integration/TestCase.php'; require_once $root . '/integration/AjaxTestCase.php'; } From accbe3cddeedcdfc1daad4a013507a53eef87500 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 15:31:07 +1000 Subject: [PATCH 08/45] chore(phpunit): drop redundant @return in UsesFactory Replace the @return docblock on gf_factory() with a native PHP 7.3 return type declaration. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/phpunit/Concerns/UsesFactory.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/phpunit/Concerns/UsesFactory.php b/tests/phpunit/Concerns/UsesFactory.php index 9d6a0323c..40f73b45f 100644 --- a/tests/phpunit/Concerns/UsesFactory.php +++ b/tests/phpunit/Concerns/UsesFactory.php @@ -15,10 +15,7 @@ trait UsesFactory { /** @var GF_UnitTest_Factory|null */ private $gfpdf_factory; - /** - * @return GF_UnitTest_Factory - */ - protected function gf_factory() { + protected function gf_factory(): GF_UnitTest_Factory { if ( null === $this->gfpdf_factory ) { $this->gfpdf_factory = new GF_UnitTest_Factory(); } From 340235d4bd3f480b1dc14ad9a88db90fe692c711 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 15:44:55 +1000 Subject: [PATCH 09/45] ci(phpunit): enforce Phase 0 coverage floor on every PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 CI gate (scope-limited per user): introduce a script that fails the build if overall line coverage drops below the Phase 0 baseline of 76.33%, and fix the two pre-existing workflow bugs the baseline doc flagged. - New `tools/phpunit/coverage-gate.php`: parses the Clover XML's totals, compares against MIN_COVERAGE_PERCENT (76.33%), exits non-zero on regression. Smoke-tested both pass and fail paths against the locally captured tmp/coverage/report-xml/baseline.xml. - `.github/workflows/phpunit.tests.yml` coverage cell: - `--xdebug=debug` → `--xdebug=coverage` (debug mode never generated coverage data — pre-existing bug). - Replace the yarn test:php wrapper with a direct vendor/bin/phpunit invocation inside the container to work around PHPUnit 9.6 + Xdebug 3's RecursiveDirectoryIterator failure on src/templates when run through yarn (documented in COVERAGE_BASELINE.md). - New "Enforce coverage floor" step running the gate after the clover XML is generated. - COVERAGE_BASELINE.md: mark the workflow-fix follow-up as resolved and point the ratchet-up instruction at the new script's constant. Net new test coverage (characterization tests for Model_PDF / Helper_Abstract_Options / etc) is deferred — the gate locks in the floor so subsequent work can only push it up. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/phpunit.tests.yml | 19 ++++++++++-- tests/phpunit/COVERAGE_BASELINE.md | 4 +-- tools/phpunit/coverage-gate.php | 46 +++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 tools/phpunit/coverage-gate.php diff --git a/.github/workflows/phpunit.tests.yml b/.github/workflows/phpunit.tests.yml index 3b7f3e045..db9f62e08 100644 --- a/.github/workflows/phpunit.tests.yml +++ b/.github/workflows/phpunit.tests.yml @@ -110,7 +110,7 @@ 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 }} @@ -124,10 +124,25 @@ jobs: run: | yarn test:php:multisite --verbose --log-junit=/var/www/html/wp-content/plugins/gravity-pdf/tmp/junit/phpunit-multisite-php${{ matrix.php }}.xml + # 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 if: ${{ matrix.report }} run: | - yarn test:php --do-not-cache-result --verbose --log-junit=/var/www/html/wp-content/plugins/gravity-pdf/tmp/junit/phpunit-coverage-php${{ matrix.php }}.xml --coverage-clover=/var/www/html/wp-content/plugins/gravity-pdf/tmp/coverage/report-xml/php-coverage1.xml + 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: Enforce coverage floor + if: ${{ matrix.report }} + run: php tools/phpunit/coverage-gate.php tmp/coverage/report-xml/php-coverage1.xml - name: Upload PHPUnit JUnit timings uses: actions/upload-artifact@v6 diff --git a/tests/phpunit/COVERAGE_BASELINE.md b/tests/phpunit/COVERAGE_BASELINE.md index 7bf655c72..73f6916d4 100644 --- a/tests/phpunit/COVERAGE_BASELINE.md +++ b/tests/phpunit/COVERAGE_BASELINE.md @@ -108,11 +108,11 @@ Source: `tmp/coverage/report-xml/baseline.xml` (Clover format, 845 KB, 208 files | Plugin root (`pdf.php`, `api.php`, `gravity-pdf-updater.php`) | 3 | 209 / 289 | **72.32%** | Mixed | | **OVERALL** | **208** | **9928 / 13007** | **76.33%** | — | -The **76.33%** overall is the CI gate that Phase 4 introduces: `coverage ≥ 76.33%` per PR, ratcheted upward quarterly. +The **76.33%** overall is the CI gate enforced by `tools/phpunit/coverage-gate.php`: `coverage ≥ 76.33%` per PR, ratcheted upward quarterly by editing the `MIN_COVERAGE_PERCENT` constant in that file. Coverage runtime overhead is modest in xdebug `coverage` mode: 47s (vs 38s without) — only ~24% slower. -> **Important methodology note** — `yarn test:php --coverage-clover=...` consistently fails on this codebase with `RecursiveDirectoryIterator::__construct(.../src/templates): Failed to open directory` when invoked through the yarn wrapper, even when `src/templates/` exists and is readable. **Invoking `vendor/bin/phpunit` directly inside the container works.** Suspected cause is a working-directory resolution quirk in PHPUnit 9.6 + Xdebug 3 coverage when the config path is absolute. This affects the CI workflow's coverage cell too (`.github/workflows/phpunit.tests.yml`); the Phase 4 CI coverage gate will need to switch the coverage step to the direct-phpunit form documented below. Filed as a follow-up for Phase 4. +> **Important methodology note** — `yarn test:php --coverage-clover=...` consistently fails on this codebase with `RecursiveDirectoryIterator::__construct(.../src/templates): Failed to open directory` when invoked through the yarn wrapper, even when `src/templates/` exists and is readable. **Invoking `vendor/bin/phpunit` directly inside the container works.** Suspected cause is a working-directory resolution quirk in PHPUnit 9.6 + Xdebug 3 coverage when the config path is absolute. Phase 4 switched `.github/workflows/phpunit.tests.yml` to the direct-phpunit form and changed the coverage cell's wp-env startup from `--xdebug=debug` (no-op for coverage) to `--xdebug=coverage`. ## Playwright (e2e) baseline diff --git a/tools/phpunit/coverage-gate.php b/tools/phpunit/coverage-gate.php new file mode 100644 index 000000000..07f26f653 --- /dev/null +++ b/tools/phpunit/coverage-gate.php @@ -0,0 +1,46 @@ +project->metrics ) ) { + fwrite( STDERR, "coverage-gate: missing in $xml_path\n" ); + exit( 1 ); +} + +$totals = $xml->project->metrics; +$statements = (int) $totals['statements']; +$covered = (int) $totals['coveredstatements']; +$percent = $statements > 0 ? ( $covered / $statements * 100 ) : 0.0; + +printf( + "coverage: %d/%d statements covered (%.2f%%); floor: %.2f%%\n", + $covered, + $statements, + $percent, + $min_percent +); + +if ( round( $percent, 2 ) < $min_percent ) { + fwrite( STDERR, sprintf( "FAIL: coverage %.2f%% below floor %.2f%%\n", $percent, $min_percent ) ); + exit( 1 ); +} + +echo "OK\n"; From dd73a5934a561ec80f4d34cc6b1fac13752b3675 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 15:46:02 +1000 Subject: [PATCH 10/45] chore(phpunit): drop unused min-percent override in coverage-gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The $argv[2] threshold override had no caller — the workflow passes only the XML path, and the documented ratchet-up path is editing the constant. Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/phpunit/coverage-gate.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tools/phpunit/coverage-gate.php b/tools/phpunit/coverage-gate.php index 07f26f653..dc42154fe 100644 --- a/tools/phpunit/coverage-gate.php +++ b/tools/phpunit/coverage-gate.php @@ -2,17 +2,16 @@ /** * Fail the build if overall line coverage falls below the Phase 0 baseline. * - * Usage: php tools/phpunit/coverage-gate.php [path/to/clover.xml] [min-percent] + * Usage: php tools/phpunit/coverage-gate.php [path/to/clover.xml] * - * Defaults: tmp/coverage/report-xml/baseline.xml at 76.33%. - * Baseline is documented in tests/phpunit/COVERAGE_BASELINE.md and is meant to - * ratchet upward quarterly — update the constant below when raising it. + * Defaults to tmp/coverage/report-xml/baseline.xml. Ratchet the floor upward + * by editing MIN_COVERAGE_PERCENT below; the value is also documented in + * tests/phpunit/COVERAGE_BASELINE.md. */ const MIN_COVERAGE_PERCENT = 76.33; -$xml_path = $argv[1] ?? 'tmp/coverage/report-xml/baseline.xml'; -$min_percent = isset( $argv[2] ) ? (float) $argv[2] : MIN_COVERAGE_PERCENT; +$xml_path = $argv[1] ?? 'tmp/coverage/report-xml/baseline.xml'; $xml = @simplexml_load_file( $xml_path ); if ( false === $xml ) { @@ -35,11 +34,11 @@ $covered, $statements, $percent, - $min_percent + MIN_COVERAGE_PERCENT ); -if ( round( $percent, 2 ) < $min_percent ) { - fwrite( STDERR, sprintf( "FAIL: coverage %.2f%% below floor %.2f%%\n", $percent, $min_percent ) ); +if ( round( $percent, 2 ) < MIN_COVERAGE_PERCENT ) { + fwrite( STDERR, sprintf( "FAIL: coverage %.2f%% below floor %.2f%%\n", $percent, MIN_COVERAGE_PERCENT ) ); exit( 1 ); } From 8578e180f3d9c847044dc90b279e2197aabfd88e Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 16:09:42 +1000 Subject: [PATCH 11/45] test(phpunit): fill Exceptions hierarchy + Statics coverage gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 first chunk (Exceptions + Statics per the refactor plan): - tests/phpunit/integration/Exceptions/Test_Exception_Hierarchy.php Single smoke test (22 cases via dataProvider) asserting every src/Exceptions/*.php extends the documented parent and instantiates as a Throwable with message + code wired through. Per plan: one file for the 11 trivial exception classes, not 11 files. - tests/phpunit/integration/Statics/Test_Debug.php (3 tests) Covers Debug::is_enabled / can_view / is_enabled_and_can_view. The is_enabled() env branch is forced true in this suite because wp-tests-config.php defines WP_ENVIRONMENT_TYPE='local' — both option states therefore land on the same result, asserted explicitly. - tests/phpunit/integration/Statics/Test_Queue_Callbacks.php (4 tests) Covers the failure branches of create_pdf (wp_error → throws), send_notification (missing form, invalid entry → throws) plus the current-user restore side-effect that runs even when create_pdf fails. Coverage delta (clover, 1148 / 17678): 76.33% → 76.83% (+0.50pp). Statics: 82.71% → 94.24%. Exceptions: 22.73% → 50.00% (ceiling — the ABSPATH-exit branches are unreachable from tests). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Exceptions/Test_Exception_Hierarchy.php | 65 +++++++++++++++++++ .../integration/Statics/Test_Debug.php | 49 ++++++++++++++ .../Statics/Test_Queue_Callbacks.php | 47 ++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 tests/phpunit/integration/Exceptions/Test_Exception_Hierarchy.php create mode 100644 tests/phpunit/integration/Statics/Test_Debug.php create mode 100644 tests/phpunit/integration/Statics/Test_Queue_Callbacks.php diff --git a/tests/phpunit/integration/Exceptions/Test_Exception_Hierarchy.php b/tests/phpunit/integration/Exceptions/Test_Exception_Hierarchy.php new file mode 100644 index 000000000..f0fd874a6 --- /dev/null +++ b/tests/phpunit/integration/Exceptions/Test_Exception_Hierarchy.php @@ -0,0 +1,65 @@ +assertTrue( + is_subclass_of( $class, $parent ), + "$class must extend $parent" + ); + } + + /** + * @dataProvider provider_hierarchy + */ + public function test_constructor_passes_message_and_code( string $class ) { + $instance = new $class( 'msg', 42 ); + + $this->assertSame( 'msg', $instance->getMessage() ); + $this->assertSame( 42, $instance->getCode() ); + } + + public function provider_hierarchy(): array { + return [ + 'GravityPdfException → Exception' => [ GravityPdfException::class, Exception::class ], + 'GravityPdfRuntimeException → RuntimeException' => [ GravityPdfRuntimeException::class, RuntimeException::class ], + 'GravityPdfDomainException → DomainException' => [ GravityPdfDomainException::class, DomainException::class ], + 'GravityPdfDatabaseUpdateException → GravityPdfRuntimeException' => [ GravityPdfDatabaseUpdateException::class, GravityPdfRuntimeException::class ], + 'GravityPdfFontNotFoundException → GravityPdfDomainException' => [ GravityPdfFontNotFoundException::class, GravityPdfDomainException::class ], + 'GravityPdfIdException → GravityPdfException' => [ GravityPdfIdException::class, GravityPdfException::class ], + 'GravityPdfModelNotUpdatedException → GravityPdfException' => [ GravityPdfModelNotUpdatedException::class, GravityPdfException::class ], + 'GravityPdfShortcodeEntryIdException → GravityPdfException' => [ GravityPdfShortcodeEntryIdException::class, GravityPdfException::class ], + 'GravityPdfShortcodePdfConditionalLogicFailedException → GravityPdfException' => [ GravityPdfShortcodePdfConditionalLogicFailedException::class, GravityPdfException::class ], + 'GravityPdfShortcodePdfConfigNotFoundException → GravityPdfException' => [ GravityPdfShortcodePdfConfigNotFoundException::class, GravityPdfException::class ], + 'GravityPdfShortcodePdfInactiveException → GravityPdfException' => [ GravityPdfShortcodePdfInactiveException::class, GravityPdfException::class ], + ]; + } +} diff --git a/tests/phpunit/integration/Statics/Test_Debug.php b/tests/phpunit/integration/Statics/Test_Debug.php new file mode 100644 index 000000000..b31a3c9b2 --- /dev/null +++ b/tests/phpunit/integration/Statics/Test_Debug.php @@ -0,0 +1,49 @@ +options->update_option( 'debug_mode', 'No' ); + $this->assertTrue( Debug::is_enabled(), 'non-production env should enable debug regardless of option' ); + + $gfpdf->options->update_option( 'debug_mode', 'Yes' ); + $this->assertTrue( Debug::is_enabled(), 'explicit Yes should also enable debug' ); + } + + public function test_can_view_requires_logging_capability() { + $admin = self::factory()->user->create( [ 'role' => 'administrator' ] ); + wp_set_current_user( $admin ); + $this->assertTrue( Debug::can_view() ); + + $subscriber = self::factory()->user->create( [ 'role' => 'subscriber' ] ); + wp_set_current_user( $subscriber ); + $this->assertFalse( Debug::can_view() ); + } + + public function test_is_enabled_and_can_view_requires_both() { + $admin = self::factory()->user->create( [ 'role' => 'administrator' ] ); + wp_set_current_user( $admin ); + $this->assertTrue( Debug::is_enabled_and_can_view() ); + + $subscriber = self::factory()->user->create( [ 'role' => 'subscriber' ] ); + wp_set_current_user( $subscriber ); + $this->assertFalse( Debug::is_enabled_and_can_view() ); + } +} diff --git a/tests/phpunit/integration/Statics/Test_Queue_Callbacks.php b/tests/phpunit/integration/Statics/Test_Queue_Callbacks.php new file mode 100644 index 000000000..6fdac4cae --- /dev/null +++ b/tests/phpunit/integration/Statics/Test_Queue_Callbacks.php @@ -0,0 +1,47 @@ +expectException( Exception::class ); + Queue_Callbacks::create_pdf( 0, '' ); + } + + public function test_create_pdf_restores_previous_user_after_run() { + $original = self::factory()->user->create( [ 'role' => 'administrator' ] ); + $masquerade = self::factory()->user->create( [ 'role' => 'administrator' ] ); + wp_set_current_user( $original ); + + try { + Queue_Callbacks::create_pdf( 0, '', $masquerade ); + } catch ( Exception $e ) { + // Expected — invalid IDs throw, but the user-restore must still happen. + } + + $this->assertSame( $original, get_current_user_id(), 'previous user must be restored even on failure' ); + } + + public function test_send_notification_throws_when_form_missing() { + $this->expectException( Exception::class ); + Queue_Callbacks::send_notification( 99999, 0, [] ); + } + + public function test_send_notification_throws_when_entry_invalid() { + $form_id = $this->form( 'all-form-fields' )['id']; + + $this->expectException( Exception::class ); + Queue_Callbacks::send_notification( $form_id, 0, [] ); + } +} From ca38d9ead1bbd69c5a906d687d3f58af958474ea Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 16:29:46 +1000 Subject: [PATCH 12/45] test(phpunit): characterize 11 previously untested controllers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 Controllers chunk of the PHPUnit refactor: add integration tests for all 11 controllers that had no test file, pinning observable behaviour as a safety net before the Phase 6 Model_PDF refactor. Coverage delta: 76.83% → 77.05% overall; Controllers 76.78% → 85.18%. Suite: 1148 → 1209 tests (+61), 33.0s runtime (well under 38.5s baseline). Controllers covered: - Actions, Activation, Debug, Form_Settings, Install, Mergetags, PDF, Save_Core_Fonts, Shortcodes, Templates, Uninstaller Controller_PDF gets a characterization-test layer (hook registration, cron scheduling, KSES filter add/remove, prevent_index, guard branches in process_pdf_endpoint and process_legacy_pdf_endpoint, html=1 debug helper, nested-forms cache-hash guards). End-to-end PDF generation flows remain covered by Test_PDF / Test_Slow_PDF_Processes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Controller/Test_Controller_Actions.php | 125 ++++++++++++ .../Controller/Test_Controller_Activation.php | 67 +++++++ .../Controller/Test_Controller_Debug.php | 93 +++++++++ .../Test_Controller_Form_Settings.php | 165 ++++++++++++++++ .../Controller/Test_Controller_Install.php | 97 ++++++++++ .../Controller/Test_Controller_Mergetags.php | 52 +++++ .../Controller/Test_Controller_PDF.php | 180 ++++++++++++++++++ .../Test_Controller_Save_Core_Fonts.php | 60 ++++++ .../Controller/Test_Controller_Shortcodes.php | 56 ++++++ .../Controller/Test_Controller_Templates.php | 40 ++++ .../Test_Controller_Uninstaller.php | 87 +++++++++ 11 files changed, 1022 insertions(+) create mode 100644 tests/phpunit/integration/Controller/Test_Controller_Actions.php create mode 100644 tests/phpunit/integration/Controller/Test_Controller_Activation.php create mode 100644 tests/phpunit/integration/Controller/Test_Controller_Debug.php create mode 100644 tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php create mode 100644 tests/phpunit/integration/Controller/Test_Controller_Install.php create mode 100644 tests/phpunit/integration/Controller/Test_Controller_Mergetags.php create mode 100644 tests/phpunit/integration/Controller/Test_Controller_PDF.php create mode 100644 tests/phpunit/integration/Controller/Test_Controller_Save_Core_Fonts.php create mode 100644 tests/phpunit/integration/Controller/Test_Controller_Shortcodes.php create mode 100644 tests/phpunit/integration/Controller/Test_Controller_Templates.php create mode 100644 tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php diff --git a/tests/phpunit/integration/Controller/Test_Controller_Actions.php b/tests/phpunit/integration/Controller/Test_Controller_Actions.php new file mode 100644 index 000000000..61d0f1916 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Actions.php @@ -0,0 +1,125 @@ +controller = $gfpdf->singleton->get_class( 'Controller_Actions' ); + } + + public function tear_down() { + unset( $_POST['gfpdf_action'], $_POST['gfpdf-dismiss-notice'], $_GET['page'] ); + + parent::tear_down(); + } + + public function test_init_registers_admin_init_hooks() { + remove_all_actions( 'admin_init' ); + + $this->controller->init(); + + $this->assertNotFalse( has_action( 'admin_init', [ $this->controller, 'route' ] ) ); + $this->assertNotFalse( has_action( 'admin_init', [ $this->controller, 'route_notices' ] ) ); + } + + public function test_get_routes_includes_default_core_fonts_route() { + $routes = $this->controller->get_routes(); + + $this->assertCount( 1, $routes ); + $this->assertSame( 'install_core_fonts', $routes[0]['action'] ); + $this->assertSame( 'gravityforms_edit_settings', $routes[0]['capability'] ); + $this->assertIsCallable( $routes[0]['condition'] ); + $this->assertIsCallable( $routes[0]['process'] ); + $this->assertIsCallable( $routes[0]['view'] ); + } + + public function test_get_routes_is_filterable() { + add_filter( + 'gfpdf_one_time_action_routes', + static function ( $routes ) { + $routes[] = [ 'action' => 'custom' ]; + + return $routes; + } + ); + + $routes = $this->controller->get_routes(); + remove_all_filters( 'gfpdf_one_time_action_routes' ); + + $this->assertCount( 2, $routes ); + $this->assertSame( 'custom', $routes[1]['action'] ); + } + + public function test_route_notices_short_circuits_on_getting_started_page() { + global $gfpdf; + + $gfpdf->notices->clear(); + $_GET['page'] = 'gfpdf-getting-started'; + set_current_screen( 'gf_settings' ); + + $this->controller->route_notices(); + + $this->assertFalse( $gfpdf->notices->has_notice() ); + } + + public function test_route_does_nothing_when_post_action_absent() { + $this->controller->route(); + + $this->assertTrue( true, 'route() returns silently when gfpdf_action POST is unset' ); + } + + public function test_route_dismisses_notice_when_dismiss_flag_set() { + global $gfpdf; + + $admin = self::factory()->user->create( [ 'role' => 'administrator' ] ); + wp_set_current_user( $admin ); + set_current_screen( 'edit.php' ); + + add_filter( + 'gfpdf_one_time_action_routes', + static function () { + return [ + [ + 'action' => 'always_true', + 'action_text' => 'Always', + 'condition' => '__return_true', + 'process' => static function () {}, + 'view' => static function () { return ''; }, + 'capability' => 'gravityforms_edit_settings', + ], + ]; + } + ); + + $_POST['gfpdf_action'] = 'gfpdf_always_true'; + $_POST['gfpdf_action_always_true'] = wp_create_nonce( 'gfpdf_action_always_true' ); + $_POST['gfpdf-dismiss-notice'] = '1'; + + $model = $gfpdf->singleton->get_class( 'Model_Actions' ); + $this->controller->route(); + + remove_all_filters( 'gfpdf_one_time_action_routes' ); + + $this->assertTrue( $model->is_notice_already_dismissed( 'always_true' ) ); + } +} diff --git a/tests/phpunit/integration/Controller/Test_Controller_Activation.php b/tests/phpunit/integration/Controller/Test_Controller_Activation.php new file mode 100644 index 000000000..8fea067e8 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Activation.php @@ -0,0 +1,67 @@ +assertFalse( wp_next_scheduled( 'gfpdf_cleanup_tmp_dir' ) ); + $this->assertFalse( wp_next_scheduled( 'gfpdf_network_update_check' ) ); + $this->assertFalse( wp_next_scheduled( 'gfpdf_bulk_license_check' ) ); + } + + public function test_deactivation_removes_plugin_rewrite_rules() { + global $gfpdf; + + $rules = [ + '^' . $gfpdf->data->permalink => 'index.php?gpdf=1', + '^some/other/rule' => 'index.php?other=1', + ]; + update_option( 'rewrite_rules', $rules ); + + Controller_Activation::deactivation(); + + $updated = get_option( 'rewrite_rules' ); + $this->assertArrayNotHasKey( '^' . $gfpdf->data->permalink, $updated ); + $this->assertArrayHasKey( '^some/other/rule', $updated ); + } + + public function test_deactivation_leaves_rewrite_rules_when_no_plugin_rules_present() { + $rules = [ '^some/other/rule' => 'index.php?other=1' ]; + update_option( 'rewrite_rules', $rules ); + + Controller_Activation::deactivation(); + + $this->assertSame( $rules, get_option( 'rewrite_rules' ) ); + } + + public function test_deactivation_flushes_template_transient_cache() { + global $gfpdf; + + set_transient( $gfpdf->data->template_transient_cache, 'cached', HOUR_IN_SECONDS ); + + Controller_Activation::deactivation(); + + $this->assertFalse( get_transient( $gfpdf->data->template_transient_cache ) ); + } +} diff --git a/tests/phpunit/integration/Controller/Test_Controller_Debug.php b/tests/phpunit/integration/Controller/Test_Controller_Debug.php new file mode 100644 index 000000000..784861b8e --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Debug.php @@ -0,0 +1,93 @@ +controller = new Controller_Debug( $gfpdf->data, $gfpdf->options, $gfpdf->templates ); + } + + public function test_init_registers_hooks() { + remove_all_actions( 'update_option_gfpdf_settings' ); + remove_all_filters( 'gfpdf_mpdf_class' ); + + $this->controller->init(); + + $this->assertNotFalse( has_action( 'update_option_gfpdf_settings', [ $this->controller, 'maybe_flush_transient_cache' ] ) ); + $this->assertNotFalse( has_filter( 'gfpdf_mpdf_class', [ $this->controller, 'maybe_add_pdf_stats' ] ) ); + } + + public function test_maybe_flush_transient_cache_flushes_when_debug_mode_toggled_on() { + global $gfpdf; + + set_transient( $gfpdf->data->template_transient_cache, 'cached', HOUR_IN_SECONDS ); + + $this->controller->maybe_flush_transient_cache( [ 'debug_mode' => 'No' ], [ 'debug_mode' => 'Yes' ] ); + + $this->assertFalse( get_transient( $gfpdf->data->template_transient_cache ) ); + } + + public function test_maybe_flush_transient_cache_skips_when_already_enabled() { + global $gfpdf; + + set_transient( $gfpdf->data->template_transient_cache, 'cached', HOUR_IN_SECONDS ); + + $this->controller->maybe_flush_transient_cache( [ 'debug_mode' => 'Yes' ], [ 'debug_mode' => 'Yes' ] ); + + $this->assertSame( 'cached', get_transient( $gfpdf->data->template_transient_cache ) ); + } + + public function test_maybe_flush_transient_cache_skips_when_debug_mode_absent() { + global $gfpdf; + + set_transient( $gfpdf->data->template_transient_cache, 'cached', HOUR_IN_SECONDS ); + + $this->controller->maybe_flush_transient_cache( [], [] ); + + $this->assertSame( 'cached', get_transient( $gfpdf->data->template_transient_cache ) ); + } + + public function test_maybe_add_pdf_stats_appends_stats_when_debug_mode_on() { + global $gfpdf; + + $gfpdf->options->update_option( 'debug_mode', 'Yes' ); + + $mpdf = new Helper_Mpdf( [ 'mode' => 'c', 'tempDir' => sys_get_temp_dir() ] ); + $this->controller->maybe_add_pdf_stats( $mpdf ); + + $output = $mpdf->Output( '', 'S' ); + $this->assertNotEmpty( $output ); + } + + public function test_maybe_add_pdf_stats_returns_mpdf_unchanged_when_debug_mode_off() { + global $gfpdf; + + $gfpdf->options->update_option( 'debug_mode', 'No' ); + + $mpdf = new Helper_Mpdf( [ 'mode' => 'c', 'tempDir' => sys_get_temp_dir() ] ); + $result = $this->controller->maybe_add_pdf_stats( $mpdf ); + + $this->assertSame( $mpdf, $result ); + } +} diff --git a/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php b/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php new file mode 100644 index 000000000..f89748d38 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php @@ -0,0 +1,165 @@ +controller = $gfpdf->singleton->get_class( 'Controller_Form_Settings' ); + } + + public function tear_down() { + unset( $_GET['id'], $_GET['pid'], $_POST['action'], $_POST['gfpdf_save_pdf'], $_POST['gforms_update_form'] ); + + parent::tear_down(); + } + + public function test_init_registers_action_and_filter_hooks() { + global $gfpdf; + + foreach ( + [ + 'admin_init', + 'gform_form_settings_menu', + 'gform_form_settings_page_' . $gfpdf->data->slug, + 'wp_ajax_gfpdf_list_delete', + 'wp_ajax_gfpdf_list_duplicate', + 'wp_ajax_gfpdf_change_state', + 'wp_ajax_gfpdf_get_template_fields', + ] as $hook + ) { + remove_all_actions( $hook ); + } + + foreach ( + [ + 'gfpdf_form_settings_custom_appearance', + 'gfpdf_form_settings', + 'gfpdf_form_settings_appearance', + 'gfpdf_form_settings_sanitize', + 'tiny_mce_before_init', + 'gform_form_update_meta', + 'gform_rule_source_value', + 'gform_is_value_match', + ] as $hook + ) { + remove_all_filters( $hook ); + } + + $this->controller->init(); + + $this->assertNotFalse( has_action( 'admin_init', [ $this->controller, 'maybe_save_pdf_settings' ] ) ); + $this->assertNotFalse( has_action( 'wp_ajax_gfpdf_list_delete' ) ); + $this->assertNotFalse( has_filter( 'gfpdf_form_settings' ) ); + $this->assertNotFalse( has_filter( 'gform_form_update_meta', [ $this->controller, 'clear_cached_pdf_settings' ] ) ); + $this->assertNotFalse( has_filter( 'tiny_mce_before_init', [ $this->controller, 'store_tinymce_settings' ] ) ); + } + + public function test_store_tinymce_settings_caches_first_call_only() { + global $gfpdf; + + $gfpdf->data->tiny_mce_editor_settings = []; + + $result = $this->controller->store_tinymce_settings( [ 'foo' => 'bar' ] ); + $this->assertSame( [ 'foo' => 'bar' ], $result ); + $this->assertSame( [ 'foo' => 'bar' ], $gfpdf->data->tiny_mce_editor_settings ); + + $second = $this->controller->store_tinymce_settings( [ 'baz' => 'qux' ] ); + $this->assertSame( [ 'baz' => 'qux' ], $second, 'Returns whatever is passed in' ); + $this->assertSame( [ 'foo' => 'bar' ], $gfpdf->data->tiny_mce_editor_settings, 'Cache stays sticky once populated' ); + } + + public function test_clear_cached_pdf_settings_ignores_unrelated_meta() { + $form = [ 'gfpdf_form_settings' => [ 'unchanged' => true ] ]; + + $result = $this->controller->clear_cached_pdf_settings( $form, 1, 'something_else' ); + + $this->assertSame( $form, $result ); + } + + public function test_clear_cached_pdf_settings_ignores_when_no_save_action_posted() { + set_current_screen( 'edit.php' ); + $form = [ 'gfpdf_form_settings' => [ 'unchanged' => true ] ]; + + $result = $this->controller->clear_cached_pdf_settings( $form, 1, 'display_meta' ); + + $this->assertSame( $form, $result ); + } + + public function test_maybe_save_pdf_settings_no_op_when_pid_missing() { + // No $_GET['pid'] set, so process_submission should NOT run; calling shouldn't error. + $this->controller->maybe_save_pdf_settings(); + + $this->assertTrue( true, 'maybe_save_pdf_settings returns silently with no pid' ); + } + + public function test_conditional_logic_is_value_match_returns_original_for_unrelated_fields() { + $result = $this->controller->conditional_logic_is_value_match( + false, + '2026-01-01', + '2025-01-01', + '>', + null, + [ 'fieldId' => 'unrelated' ] + ); + + $this->assertFalse( $result, 'unrelated field passes through original $is_match' ); + } + + public function test_conditional_logic_is_value_match_compares_date_created_with_greater_than() { + $result = $this->controller->conditional_logic_is_value_match( + false, + '2026-06-01', + '2026-01-01', + '>', + null, + [ 'fieldId' => 'date_created' ] + ); + + $this->assertTrue( $result ); + } + + public function test_conditional_logic_is_value_match_compares_payment_date_with_less_than() { + $result = $this->controller->conditional_logic_is_value_match( + true, + '2026-01-01', + '2026-06-01', + '<', + null, + [ 'fieldId' => 'payment_date' ] + ); + + $this->assertTrue( $result ); + } + + public function test_conditional_logic_set_rule_source_value_passes_through_when_no_entry() { + $result = $this->controller->conditional_logic_set_rule_source_value( + 'original', + [ 'fieldId' => 'date_created' ], + [ 'id' => 1 ], + [], + null + ); + + $this->assertSame( 'original', $result ); + } +} diff --git a/tests/phpunit/integration/Controller/Test_Controller_Install.php b/tests/phpunit/integration/Controller/Test_Controller_Install.php new file mode 100644 index 000000000..dc316c251 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Install.php @@ -0,0 +1,97 @@ +controller = $gfpdf->singleton->get_class( 'Controller_Install' ); + } + + public function test_init_registers_action_and_filter_hooks() { + remove_all_actions( 'wp_loaded' ); + remove_all_actions( 'init' ); + remove_all_filters( 'query_vars' ); + + $this->controller->init(); + + $this->assertNotFalse( has_action( 'wp_loaded', [ $this->controller, 'check_install_status' ] ) ); + $this->assertNotFalse( has_action( 'init' ) ); + $this->assertNotFalse( has_filter( 'query_vars' ) ); + } + + public function test_setup_defaults_populates_data_object() { + global $gfpdf; + + $gfpdf->data->is_installed = null; + $gfpdf->data->permalink = null; + $gfpdf->data->working_folder = null; + $gfpdf->data->upload_dir = null; + + $this->controller->setup_defaults(); + + $this->assertIsBool( $gfpdf->data->is_installed ); + $this->assertSame( 'pdf/([A-Za-z0-9]+)/([0-9]+)/?(download)?/?', $gfpdf->data->permalink ); + $this->assertNotEmpty( $gfpdf->data->working_folder ); + $this->assertNotEmpty( $gfpdf->data->upload_dir ); + $this->assertSame( 'gfpdf_template_info', $gfpdf->data->template_transient_cache ); + } + + public function test_check_install_status_short_circuits_for_unauthenticated_request() { + wp_set_current_user( 0 ); + set_current_screen( 'edit.php' ); + + // No capability, so the method exits early without touching options. + $before = get_option( 'gfpdf_current_version' ); + $this->controller->check_install_status(); + $this->assertSame( $before, get_option( 'gfpdf_current_version' ) ); + } + + public function test_check_install_status_syncs_version_for_admin_when_version_mismatched() { + set_current_screen( 'edit.php' ); + $admin = self::factory()->user->create( [ 'role' => 'administrator' ] ); + wp_set_current_user( $admin ); + + update_option( 'gfpdf_current_version', '0.0.1' ); + + $captured = []; + add_action( + 'gfpdf_version_changed', + static function ( $old, $new ) use ( &$captured ) { + $captured = [ $old, $new ]; + }, + 10, + 2 + ); + + $this->controller->check_install_status(); + + $this->assertSame( PDF_EXTENDED_VERSION, get_option( 'gfpdf_current_version' ) ); + $this->assertSame( [ '0.0.1', PDF_EXTENDED_VERSION ], $captured ); + } + + public function test_maybe_uninstall_emits_doing_it_wrong_notice() { + $this->setExpectedIncorrectUsage( 'GFPDF\Controller\Controller_Install::maybe_uninstall' ); + + $this->controller->maybe_uninstall(); + } +} diff --git a/tests/phpunit/integration/Controller/Test_Controller_Mergetags.php b/tests/phpunit/integration/Controller/Test_Controller_Mergetags.php new file mode 100644 index 000000000..e545b6513 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Mergetags.php @@ -0,0 +1,52 @@ +singleton->get_class( 'Model_Mergetags' ) ); + $controller->init(); + + $this->assertNotFalse( has_filter( 'gform_replace_merge_tags' ) ); + $this->assertNotFalse( has_filter( 'gform_custom_merge_tags' ) ); + $this->assertNotFalse( has_filter( 'gform_field_map_choices' ) ); + $this->assertNotFalse( has_filter( 'gform_addon_field_value' ) ); + $this->assertNotFalse( has_filter( 'gform_mailchimp_field_value' ) ); + $this->assertNotFalse( has_filter( 'gpgs_row_value' ) ); + } + + public function test_constructor_wires_model_back_to_controller() { + global $gfpdf; + + $model = $gfpdf->singleton->get_class( 'Model_Mergetags' ); + $controller = new Controller_Mergetags( $model ); + + $this->assertSame( $controller, $model->getController() ); + } +} diff --git a/tests/phpunit/integration/Controller/Test_Controller_PDF.php b/tests/phpunit/integration/Controller/Test_Controller_PDF.php new file mode 100644 index 000000000..01dce0114 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_PDF.php @@ -0,0 +1,180 @@ +gform, $gfpdf->log, $gfpdf->options, $gfpdf->data, $gfpdf->misc, $gfpdf->notices, $gfpdf->templates, new Helper_Url_Signer() ); + $view = new View_PDF( [], $gfpdf->gform, $gfpdf->log, $gfpdf->options, $gfpdf->data, $gfpdf->misc, $gfpdf->templates ); + + $this->controller = new Controller_PDF( $model, $view, $gfpdf->gform, $gfpdf->log, $gfpdf->misc ); + } + + public function tear_down() { + unset( + $GLOBALS['wp']->query_vars['gpdf'], + $GLOBALS['wp']->query_vars['pid'], + $GLOBALS['wp']->query_vars['lid'], + $_GET['gf_pdf'], + $_GET['fid'], + $_GET['lid'], + $_GET['template'], + $_GET['html'], + $_GET['raw'] + ); + + parent::tear_down(); + } + + public function test_init_schedules_cleanup_cron_when_missing() { + wp_clear_scheduled_hook( 'gfpdf_cleanup_tmp_dir' ); + + $this->controller->init(); + + $this->assertNotFalse( wp_next_scheduled( 'gfpdf_cleanup_tmp_dir' ) ); + + wp_clear_scheduled_hook( 'gfpdf_cleanup_tmp_dir' ); + } + + public function test_init_does_not_double_schedule_when_already_present() { + wp_clear_scheduled_hook( 'gfpdf_cleanup_tmp_dir' ); + wp_schedule_event( 1000, 'hourly', 'gfpdf_cleanup_tmp_dir' ); + $existing = wp_next_scheduled( 'gfpdf_cleanup_tmp_dir' ); + + $this->controller->init(); + + $this->assertSame( $existing, wp_next_scheduled( 'gfpdf_cleanup_tmp_dir' ) ); + + wp_clear_scheduled_hook( 'gfpdf_cleanup_tmp_dir' ); + } + + public function test_init_registers_pdf_endpoint_and_middleware_hooks() { + remove_all_actions( 'parse_request' ); + remove_all_filters( 'gfpdf_pdf_middleware' ); + remove_all_filters( 'gfpdf_pdf_html_output' ); + + $this->controller->init(); + + $this->assertNotFalse( has_action( 'parse_request', [ $this->controller, 'process_pdf_endpoint' ] ) ); + $this->assertNotFalse( has_action( 'parse_request', [ $this->controller, 'process_legacy_pdf_endpoint' ] ) ); + $this->assertNotFalse( has_filter( 'gfpdf_pdf_middleware' ) ); + $this->assertNotFalse( has_filter( 'gfpdf_pdf_html_output' ) ); + } + + public function test_add_pre_pdf_hooks_registers_kses_filters() { + remove_all_filters( 'wp_kses_allowed_html' ); + remove_all_filters( 'safe_style_css' ); + + $this->controller->add_pre_pdf_hooks(); + + $this->assertNotFalse( has_filter( 'wp_kses_allowed_html' ) ); + $this->assertNotFalse( has_filter( 'safe_style_css' ) ); + } + + public function test_remove_pre_pdf_hooks_unregisters_kses_filters() { + $this->controller->add_pre_pdf_hooks(); + $this->controller->remove_pre_pdf_hooks(); + + $this->assertFalse( has_filter( 'wp_kses_allowed_html', [ $this->controller->view, 'allow_pdf_html' ] ) ); + $this->assertFalse( has_filter( 'safe_style_css', [ $this->controller->view, 'allow_pdf_css' ] ) ); + } + + public function test_prevent_index_defines_donotcachepage_constant() { + // Cannot un-define constants; this asserts only that the call doesn't throw + // and that the constant is defined after invocation (idempotent). + $this->controller->prevent_index(); + + $this->assertTrue( defined( 'DONOTCACHEPAGE' ) ); + $this->assertTrue( DONOTCACHEPAGE ); + } + + public function test_process_pdf_endpoint_returns_silently_without_query_vars() { + $GLOBALS['wp']->query_vars = []; + + $this->controller->process_pdf_endpoint(); + + $this->assertTrue( true, 'returns silently when gpdf/pid/lid query vars are absent' ); + } + + public function test_process_legacy_pdf_endpoint_returns_silently_without_legacy_params() { + $_GET = []; + + $this->controller->process_legacy_pdf_endpoint(); + + $this->assertTrue( true, 'returns silently when gf_pdf/fid/lid/template GET params are absent' ); + } + + public function test_sgoptimizer_html_minification_fix_emits_doing_it_wrong() { + $this->setExpectedIncorrectUsage( 'GFPDF\Controller\Controller_PDF::sgoptimizer_html_minification_fix' ); + + $this->controller->sgoptimizer_html_minification_fix(); + } + + public function test_add_view_html_debugger_passes_through_non_string_input() { + $result = $this->invoke_protected( 'add_view_html_debugger', [ null, [], [], [], null ] ); + + $this->assertNull( $result ); + } + + public function test_add_view_html_debugger_passes_through_when_html_param_absent() { + unset( $_GET['html'] ); + + $result = $this->invoke_protected( 'add_view_html_debugger', [ '

original

', [], [], [], null ] ); + + $this->assertSame( '

original

', $result ); + } + + public function test_included_nested_forms_in_cache_hash_returns_data_when_entry_id_missing() { + $result = $this->invoke_protected( 'included_nested_forms_in_cache_hash', [ [ 'foo' => 'bar' ], [], [], [] ] ); + + $this->assertSame( [ 'foo' => 'bar' ], $result ); + } + + public function test_included_nested_forms_in_cache_hash_returns_data_when_gpnf_entry_class_missing() { + if ( class_exists( '\GPNF_Entry' ) ) { + $this->markTestSkipped( 'GPNF_Entry available, cannot exercise the missing-class branch.' ); + } + + $result = $this->invoke_protected( 'included_nested_forms_in_cache_hash', [ [ 'foo' => 'bar' ], [], [ 'id' => 1 ], [] ] ); + + $this->assertSame( [ 'foo' => 'bar' ], $result ); + } + + public function test_add_current_form_object_hooks_returns_form_unchanged_when_id_missing() { + $result = $this->invoke_protected( 'add_current_form_object_hooks', [ [ 'fields' => [] ], [], 'source' ] ); + + $this->assertSame( [ 'fields' => [] ], $result ); + } + + private function invoke_protected( string $method, array $args ) { + return ( new ReflectionMethod( $this->controller, $method ) )->invokeArgs( $this->controller, $args ); + } +} diff --git a/tests/phpunit/integration/Controller/Test_Controller_Save_Core_Fonts.php b/tests/phpunit/integration/Controller/Test_Controller_Save_Core_Fonts.php new file mode 100644 index 000000000..0c9d69956 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Save_Core_Fonts.php @@ -0,0 +1,60 @@ +controller = new Controller_Save_Core_Fonts( $gfpdf->log, $gfpdf->data, $gfpdf->misc ); + } + + public function tear_down() { + unset( $_POST['font_name'] ); + + parent::tear_down(); + } + + public function test_init_registers_ajax_endpoint() { + remove_all_actions( 'wp_ajax_gfpdf_save_core_font' ); + + $this->controller->init(); + + $this->assertNotFalse( has_action( 'wp_ajax_gfpdf_save_core_font', [ $this->controller, 'save_core_font' ] ) ); + } + + public function test_download_returns_false_when_font_name_missing() { + unset( $_POST['font_name'] ); + + $this->assertFalse( $this->invoke_download() ); + } + + public function test_download_returns_false_when_font_name_not_on_approved_list() { + $_POST['font_name'] = 'NotARealFont.ttf'; + + $this->assertFalse( $this->invoke_download() ); + } + + private function invoke_download() { + return ( new ReflectionMethod( $this->controller, 'download_and_save_font' ) )->invoke( $this->controller ); + } +} diff --git a/tests/phpunit/integration/Controller/Test_Controller_Shortcodes.php b/tests/phpunit/integration/Controller/Test_Controller_Shortcodes.php new file mode 100644 index 000000000..d207b4ede --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Shortcodes.php @@ -0,0 +1,56 @@ +singleton->get_class( 'Model_Shortcodes' ), + $gfpdf->singleton->get_class( 'View_Shortcodes' ), + $gfpdf->log + ); + $controller->init(); + + $this->assertNotFalse( has_filter( 'gform_admin_pre_render' ) ); + $this->assertNotFalse( has_filter( 'gform_confirmation' ) ); + $this->assertNotFalse( has_filter( 'gform_pre_replace_merge_tags' ) ); + $this->assertNotFalse( has_filter( 'gravityview/fields/custom/content_before' ) ); + $this->assertTrue( shortcode_exists( 'gravitypdf' ) ); + } + + public function test_constructor_wires_model_and_view_back_to_controller() { + global $gfpdf; + + $model = $gfpdf->singleton->get_class( 'Model_Shortcodes' ); + $view = $gfpdf->singleton->get_class( 'View_Shortcodes' ); + $controller = new Controller_Shortcodes( $model, $view, $gfpdf->log ); + + $this->assertSame( $controller, $model->getController() ); + $this->assertSame( $controller, $view->getController() ); + } +} diff --git a/tests/phpunit/integration/Controller/Test_Controller_Templates.php b/tests/phpunit/integration/Controller/Test_Controller_Templates.php new file mode 100644 index 000000000..db8e28a4e --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Templates.php @@ -0,0 +1,40 @@ +singleton->get_class( 'Model_Templates' ) ); + $controller->init(); + + $this->assertNotFalse( has_action( 'wp_ajax_gfpdf_upload_template' ) ); + $this->assertNotFalse( has_action( 'wp_ajax_gfpdf_delete_template' ) ); + $this->assertNotFalse( has_action( 'wp_ajax_gfpdf_get_template_options' ) ); + } + + public function test_constructor_wires_model_back_to_controller() { + global $gfpdf; + + $model = $gfpdf->singleton->get_class( 'Model_Templates' ); + $controller = new Controller_Templates( $model ); + + $this->assertSame( $controller, $model->getController() ); + } +} diff --git a/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php b/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php new file mode 100644 index 000000000..c4d33a455 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php @@ -0,0 +1,87 @@ +controller = Controller_Uninstaller::get_instance(); + } + + public function test_get_instance_returns_singleton() { + $this->assertSame( $this->controller, Controller_Uninstaller::get_instance() ); + } + + public function test_get_short_title_returns_plugin_name() { + $this->assertSame( 'Gravity PDF', $this->controller->get_short_title() ); + } + + public function test_get_menu_icon_returns_gpdf_icon_class() { + $this->assertSame( 'gform-icon--gravity-pdf', $this->controller->get_menu_icon() ); + } + + public function test_method_is_overridden_always_returns_false() { + $this->assertFalse( $this->controller->method_is_overridden( 'any_method' ) ); + $this->assertFalse( $this->controller->method_is_overridden( '' ) ); + } + + public function test_current_user_can_uninstall_grants_admin_on_single_site() { + if ( is_multisite() ) { + $this->markTestSkipped( 'Single-site path only.' ); + } + + $admin = self::factory()->user->create( [ 'role' => 'administrator' ] ); + wp_set_current_user( $admin ); + + $this->assertTrue( $this->controller->current_user_can_uninstall() ); + } + + public function test_current_user_can_uninstall_denies_subscriber_on_single_site() { + if ( is_multisite() ) { + $this->markTestSkipped( 'Single-site path only.' ); + } + + $subscriber = self::factory()->user->create( [ 'role' => 'subscriber' ] ); + wp_set_current_user( $subscriber ); + + $this->assertFalse( $this->controller->current_user_can_uninstall() ); + } + + public function test_render_uninstall_outputs_nothing_when_capability_denied() { + $subscriber = self::factory()->user->create( [ 'role' => 'subscriber' ] ); + wp_set_current_user( $subscriber ); + + ob_start(); + $this->controller->render_uninstall(); + $output = ob_get_clean(); + + $this->assertSame( '', $output ); + } + + public function test_init_skips_registration_outside_uninstall_subview() { + $_GET = [ 'page' => 'gf_settings', 'subview' => 'something-else' ]; + + $this->controller->init(); + + $_GET = []; + + $this->assertTrue( true, 'init() returns silently when not on uninstall subview' ); + } +} From 703b125cd2c8516e366083fe9f82068c20733ac9 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 16:35:33 +1000 Subject: [PATCH 13/45] chore(phpunit): trim slop from controller characterization tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up slop review on commit ca38d9ea: - Drop redundant inline comments that restate the test name - Drop verbose assertTrue(true) messages — the test name is enough - Drop the namespace explanation docblock on Test_Controller_Activation (Controller_Activation.php already explains why it's global-namespace) - Drop the "cannot un-define constants" prose on the prevent_index test No behaviour change; suite still 1209 tests / 0 risky. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../integration/Controller/Test_Controller_Actions.php | 2 +- .../integration/Controller/Test_Controller_Activation.php | 3 --- .../Controller/Test_Controller_Form_Settings.php | 3 +-- .../integration/Controller/Test_Controller_Install.php | 2 +- .../phpunit/integration/Controller/Test_Controller_PDF.php | 6 ++---- .../integration/Controller/Test_Controller_Uninstaller.php | 4 +--- 6 files changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/phpunit/integration/Controller/Test_Controller_Actions.php b/tests/phpunit/integration/Controller/Test_Controller_Actions.php index 61d0f1916..7cdb3cc91 100644 --- a/tests/phpunit/integration/Controller/Test_Controller_Actions.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Actions.php @@ -85,7 +85,7 @@ public function test_route_notices_short_circuits_on_getting_started_page() { public function test_route_does_nothing_when_post_action_absent() { $this->controller->route(); - $this->assertTrue( true, 'route() returns silently when gfpdf_action POST is unset' ); + $this->assertTrue( true ); } public function test_route_dismisses_notice_when_dismiss_flag_set() { diff --git a/tests/phpunit/integration/Controller/Test_Controller_Activation.php b/tests/phpunit/integration/Controller/Test_Controller_Activation.php index 8fea067e8..385db8550 100644 --- a/tests/phpunit/integration/Controller/Test_Controller_Activation.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Activation.php @@ -8,9 +8,6 @@ use GFPDF\Tests\Integration\TestCase; /** - * Controller_Activation is the only top-level (no-namespace) class in the - * plugin because the deactivation hook fires before namespacing is loaded. - * * @package GFPDF\Controller * * @group controller diff --git a/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php b/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php index f89748d38..96af8185b 100644 --- a/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php @@ -106,10 +106,9 @@ public function test_clear_cached_pdf_settings_ignores_when_no_save_action_poste } public function test_maybe_save_pdf_settings_no_op_when_pid_missing() { - // No $_GET['pid'] set, so process_submission should NOT run; calling shouldn't error. $this->controller->maybe_save_pdf_settings(); - $this->assertTrue( true, 'maybe_save_pdf_settings returns silently with no pid' ); + $this->assertTrue( true ); } public function test_conditional_logic_is_value_match_returns_original_for_unrelated_fields() { diff --git a/tests/phpunit/integration/Controller/Test_Controller_Install.php b/tests/phpunit/integration/Controller/Test_Controller_Install.php index dc316c251..fcb09b4f1 100644 --- a/tests/phpunit/integration/Controller/Test_Controller_Install.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Install.php @@ -60,9 +60,9 @@ public function test_check_install_status_short_circuits_for_unauthenticated_req wp_set_current_user( 0 ); set_current_screen( 'edit.php' ); - // No capability, so the method exits early without touching options. $before = get_option( 'gfpdf_current_version' ); $this->controller->check_install_status(); + $this->assertSame( $before, get_option( 'gfpdf_current_version' ) ); } diff --git a/tests/phpunit/integration/Controller/Test_Controller_PDF.php b/tests/phpunit/integration/Controller/Test_Controller_PDF.php index 01dce0114..d2ab2c7fe 100644 --- a/tests/phpunit/integration/Controller/Test_Controller_PDF.php +++ b/tests/phpunit/integration/Controller/Test_Controller_PDF.php @@ -108,8 +108,6 @@ public function test_remove_pre_pdf_hooks_unregisters_kses_filters() { } public function test_prevent_index_defines_donotcachepage_constant() { - // Cannot un-define constants; this asserts only that the call doesn't throw - // and that the constant is defined after invocation (idempotent). $this->controller->prevent_index(); $this->assertTrue( defined( 'DONOTCACHEPAGE' ) ); @@ -121,7 +119,7 @@ public function test_process_pdf_endpoint_returns_silently_without_query_vars() $this->controller->process_pdf_endpoint(); - $this->assertTrue( true, 'returns silently when gpdf/pid/lid query vars are absent' ); + $this->assertTrue( true ); } public function test_process_legacy_pdf_endpoint_returns_silently_without_legacy_params() { @@ -129,7 +127,7 @@ public function test_process_legacy_pdf_endpoint_returns_silently_without_legacy $this->controller->process_legacy_pdf_endpoint(); - $this->assertTrue( true, 'returns silently when gf_pdf/fid/lid/template GET params are absent' ); + $this->assertTrue( true ); } public function test_sgoptimizer_html_minification_fix_emits_doing_it_wrong() { diff --git a/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php b/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php index c4d33a455..767ab568f 100644 --- a/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php @@ -77,11 +77,9 @@ public function test_render_uninstall_outputs_nothing_when_capability_denied() { public function test_init_skips_registration_outside_uninstall_subview() { $_GET = [ 'page' => 'gf_settings', 'subview' => 'something-else' ]; - $this->controller->init(); - $_GET = []; - $this->assertTrue( true, 'init() returns silently when not on uninstall subview' ); + $this->assertTrue( true ); } } From 1405795cf2cb51e3fff637dbbcb28ac70a0dfc25 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 16:48:27 +1000 Subject: [PATCH 14/45] test(phpunit): fill Model coverage gaps in Install/Uninstall/Templates/Settings Adds 19 characterization tests across four existing Model test files, targeting uncovered branches identified by Clover coverage of the Phase 4 controllers baseline. - Model_Install: register_rewrite_tags branches, maybe_flush_rewrite_rules, single-site early return in setup_multisite_template_location - Model_Uninstall: remove_plugin_transients, remove_folder_structure idempotence, deactivate_plugin basename normalization - Model_Templates: maybe_run_template_setup no-op, check_for_valid_pdf_templates invalid-filename branch, delete_template happy path - Model_Settings: style_capabilities, highlight_errors error/non-error branches, get_template_data, licensing_bulk_get_version_api_response guards, licensing_bulk_license_check empty-license-key branch, run_network_update_check single-site return Per-file line coverage uplift (Clover, integration suite): - Model_Install: 45.7% -> 58.1% - Model_Settings: 70.2% -> 76.5% - Model_Uninstall: 51.1% -> 53.3% - Model_Templates: 58.9% -> 59.6% Overall: 1209 -> 1228 tests (+19), 77.05% -> 77.29% line coverage, suite runtime 36.5s (under 38.5s Phase 0 baseline). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../integration/Model/Test_Installer.php | 35 +++++++++ .../integration/Model/Test_Model_Settings.php | 78 +++++++++++++++++++ .../integration/Model/Test_Templates.php | 41 ++++++++++ .../integration/Model/Test_Uninstaller.php | 36 +++++++++ 4 files changed, 190 insertions(+) diff --git a/tests/phpunit/integration/Model/Test_Installer.php b/tests/phpunit/integration/Model/Test_Installer.php index b51e0f827..40e633608 100644 --- a/tests/phpunit/integration/Model/Test_Installer.php +++ b/tests/phpunit/integration/Model/Test_Installer.php @@ -248,4 +248,39 @@ public function test_register_rewrite_rules() { $this->assertEquals( 'index.php?gpdf=1&pid=$matches[1]&lid=$matches[2]&action=$matches[3]', $wp_rewrite->extra_rules_top[ '^' . $gfpdf->data->permalink ] ); $this->assertEquals( 'index.php?gpdf=1&pid=$matches[1]&lid=$matches[2]&action=$matches[3]', $wp_rewrite->extra_rules_top[ '^' . $wp_rewrite->root . $gfpdf->data->permalink ] ); } + + public function test_register_rewrite_tags_appends_pdf_query_vars_when_gpdf_get_present() { + $_GET['gpdf'] = '1'; + $tags = $this->model->register_rewrite_tags( [ 'existing' ] ); + unset( $_GET['gpdf'] ); + + $this->assertSame( [ 'existing', 'gpdf', 'pid', 'lid', 'action' ], $tags ); + } + + public function test_register_rewrite_tags_returns_input_unchanged_without_pdf_query() { + $tags = $this->model->register_rewrite_tags( [ 'existing' ] ); + + $this->assertSame( [ 'existing' ], $tags ); + } + + public function test_maybe_flush_rewrite_rules_runs_without_error_when_rules_missing() { + update_option( 'rewrite_rules', [] ); + + $this->model->maybe_flush_rewrite_rules( [ '^some/missing/rule/' ] ); + + $this->assertTrue( true ); + } + + public function test_setup_multisite_template_location_returns_early_on_single_site() { + if ( is_multisite() ) { + $this->markTestSkipped( 'Single-site path only.' ); + } + + global $gfpdf; + $before = $gfpdf->data->multisite_template_location ?? null; + + $this->model->setup_multisite_template_location(); + + $this->assertSame( $before, $gfpdf->data->multisite_template_location ?? null ); + } } diff --git a/tests/phpunit/integration/Model/Test_Model_Settings.php b/tests/phpunit/integration/Model/Test_Model_Settings.php index 1d6ddff4a..d4e33ab9c 100644 --- a/tests/phpunit/integration/Model/Test_Model_Settings.php +++ b/tests/phpunit/integration/Model/Test_Model_Settings.php @@ -203,6 +203,84 @@ public function test_licensing_bulk_license_check_bad_status_code() { remove_filter( 'pre_http_request', $api_response ); } + public function test_style_capabilities_humanizes_gravity_forms_caps() { + $this->assertSame( 'Gravity Forms Edit Settings', $this->model->style_capabilities( 'gravityforms_edit_settings' ) ); + $this->assertSame( 'Manage Options', $this->model->style_capabilities( 'manage_options' ) ); + } + + public function test_highlight_errors_adds_gfield_error_class_to_matching_id() { + set_transient( + 'settings_errors', + [ + [ 'code' => 'foo', 'type' => 'error' ], + ] + ); + + $settings = [ + 'group' => [ + [ 'id' => 'foo' ], + [ 'id' => 'bar', 'class' => 'existing' ], + ], + ]; + + $out = $this->model->highlight_errors( $settings ); + delete_transient( 'settings_errors' ); + + $this->assertSame( 'gfield_error', $out['group'][0]['class'] ); + $this->assertSame( 'existing', $out['group'][1]['class'] ); + } + + public function test_highlight_errors_ignores_non_error_transient_entries() { + set_transient( + 'settings_errors', + [ + [ 'code' => 'foo', 'type' => 'updated' ], + ] + ); + + $settings = [ 'group' => [ [ 'id' => 'foo' ] ] ]; + + $out = $this->model->highlight_errors( $settings ); + delete_transient( 'settings_errors' ); + + $this->assertArrayNotHasKey( 'class', $out['group'][0] ); + } + + public function test_get_template_data_injects_template_list_and_default() { + $out = $this->model->get_template_data( [] ); + + $this->assertArrayHasKey( 'templateList', $out ); + $this->assertArrayHasKey( 'activeDefaultTemplate', $out ); + $this->assertIsArray( $out['templateList'] ); + } + + public function test_licensing_bulk_get_version_api_response_returns_non_array_unchanged() { + $this->assertSame( 'scalar', $this->model->licensing_bulk_get_version_api_response( 'scalar', [], 'plugin.php' ) ); + } + + public function test_licensing_bulk_get_version_api_response_skips_responses_without_slug() { + $response = [ (object) [ 'name' => 'no slug here' ] ]; + + $this->assertNull( $this->model->licensing_bulk_get_version_api_response( $response, [], 'plugin.php' ) ); + } + + public function test_licensing_bulk_license_check_returns_false_when_addons_have_no_license_key() { + $this->addon->init(); + do_action( 'init' ); + + $this->assertFalse( $this->model->licensing_bulk_license_check() ); + } + + public function test_run_network_update_check_returns_early_on_single_site() { + if ( is_multisite() ) { + $this->markTestSkipped( 'Single-site path only.' ); + } + + $this->model->run_network_update_check(); + + $this->assertTrue( true ); + } + public function test_licensing_bulk_license_check_bad_response() { $this->addon->init(); $this->addon1->init(); diff --git a/tests/phpunit/integration/Model/Test_Templates.php b/tests/phpunit/integration/Model/Test_Templates.php index 709a4e749..16a3d9d38 100644 --- a/tests/phpunit/integration/Model/Test_Templates.php +++ b/tests/phpunit/integration/Model/Test_Templates.php @@ -311,6 +311,47 @@ public function test_get_template_info() { $this->assertEquals( 'Zadani', $info[0]['template'] ); } + public function test_maybe_run_template_setup_no_op_when_no_headers() { + $this->model->maybe_run_template_setup( [] ); + + $this->assertTrue( true ); + } + + public function test_check_for_valid_pdf_templates_throws_on_invalid_filename() { + global $gfpdf; + + $invalid = $gfpdf->data->template_tmp_location . 'bad@name!.php'; + touch( $invalid ); + + try { + $this->model->check_for_valid_pdf_templates( [ $invalid ] ); + $thrown = false; + } catch ( Exception $e ) { + $thrown = true; + $this->assertStringContainsString( 'contains invalid characters', $e->getMessage() ); + } + + unlink( $invalid ); + + $this->assertTrue( $thrown ); + } + + public function test_delete_template_unlinks_files_for_template_id() { + global $gfpdf; + + $template_dir = $gfpdf->templates->get_template_path(); + $template_id = 'phpunit-fixture-template'; + $template_php = $template_dir . $template_id . '.php'; + + file_put_contents( $template_php, "assertFileExists( $template_php ); + + $gfpdf->templates->flush_template_transient_cache(); + $this->model->delete_template( $template_id ); + + $this->assertFileDoesNotExist( $template_php ); + } + /** * Check our unzipped directory is correctly cleaned up * diff --git a/tests/phpunit/integration/Model/Test_Uninstaller.php b/tests/phpunit/integration/Model/Test_Uninstaller.php index 6b29a9cb1..36517bbaf 100644 --- a/tests/phpunit/integration/Model/Test_Uninstaller.php +++ b/tests/phpunit/integration/Model/Test_Uninstaller.php @@ -186,4 +186,40 @@ public function test_remove_plugin_form_settings() { $gfpdf->gform->update_form( $form ); } } + + public function test_remove_plugin_transients_clears_settings_user_data_transient() { + set_transient( 'gfpdf_settings_user_data', [ 'key' => 'value' ], HOUR_IN_SECONDS ); + + $this->assertNotFalse( get_transient( 'gfpdf_settings_user_data' ) ); + + $this->model->remove_plugin_transients(); + + $this->assertFalse( get_transient( 'gfpdf_settings_user_data' ) ); + } + + public function test_remove_folder_structure_skips_paths_that_do_not_exist() { + add_filter( + 'gfpdf_uninstall_path', + static function () { + return [ '/path/that/definitely/does/not/exist/' . uniqid() ]; + } + ); + + $this->model->remove_folder_structure(); + remove_all_filters( 'gfpdf_uninstall_path' ); + + $this->assertTrue( true ); + } + + public function test_deactivate_plugin_defaults_basename_when_empty() { + $this->model->deactivate_plugin( '' ); + + $this->assertFalse( is_plugin_active( PDF_PLUGIN_BASENAME ) ); + } + + public function test_deactivate_plugin_wraps_string_basename_in_array() { + $this->model->deactivate_plugin( 'unknown/plugin.php' ); + + $this->assertFalse( is_plugin_active( 'unknown/plugin.php' ) ); + } } From 6a454d15dc0db66592007809bfd13749074bb0d6 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 16:49:52 +1000 Subject: [PATCH 15/45] chore(phpunit): drop before/after dance from multisite early-return test The setup_multisite_template_location single-site test was using a property-snapshot pattern with double `?? null` defensive checks. The method is `void` and returns early, so the simpler assertTrue(true) marker matches the pattern already used by the other no-op tests in this commit batch. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/phpunit/integration/Model/Test_Installer.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/phpunit/integration/Model/Test_Installer.php b/tests/phpunit/integration/Model/Test_Installer.php index 40e633608..10bf20c3d 100644 --- a/tests/phpunit/integration/Model/Test_Installer.php +++ b/tests/phpunit/integration/Model/Test_Installer.php @@ -276,11 +276,8 @@ public function test_setup_multisite_template_location_returns_early_on_single_s $this->markTestSkipped( 'Single-site path only.' ); } - global $gfpdf; - $before = $gfpdf->data->multisite_template_location ?? null; - $this->model->setup_multisite_template_location(); - $this->assertSame( $before, $gfpdf->data->multisite_template_location ?? null ); + $this->assertTrue( true ); } } From 41a293947491b1eb1c675aec044ef5688acebf52 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 17:01:19 +1000 Subject: [PATCH 16/45] chore(phpunit): drop assertTrue(true) coverage padding from characterization tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous Phase 4 Model and Controller commits added several `assertTrue(true)` tests to bump line coverage of trivial early-return guards. Those tests exercise the code path but assert nothing about behaviour — they only catch a thrown exception, which PHPUnit already catches implicitly. Either delete (when the branch has no observable effect) or rewrite to a real assertion. Dropped (Model_*): - Test_Installer::test_setup_multisite_template_location_returns_early_on_single_site - Test_Uninstaller::test_remove_folder_structure_skips_paths_that_do_not_exist - Test_Templates::test_maybe_run_template_setup_no_op_when_no_headers - Test_Model_Settings::test_run_network_update_check_returns_early_on_single_site Dropped (Controller_*): - Test_Controller_Form_Settings::test_maybe_save_pdf_settings_no_op_when_pid_missing - Test_Controller_PDF::test_process_pdf_endpoint_returns_silently_without_query_vars - Test_Controller_PDF::test_process_legacy_pdf_endpoint_returns_silently_without_legacy_params - Test_Controller_Actions::test_route_does_nothing_when_post_action_absent - Test_Controller_Uninstaller::test_init_skips_registration_outside_uninstall_subview Positive branches of the Controller_PDF endpoints are already covered by Test_Slow_PDF_Processes; route() dismissal path is already covered by test_route_dismisses_notice_when_dismiss_flag_set. Replaced (Model_Install): the no-op test_maybe_flush_rewrite_rules_runs_without_error_when_rules_missing rewritten as test_maybe_flush_rewrite_rules_triggers_flush_only_when_rule_absent, exercising both branches (sentinel rewrite_rules option preserved when the rule is present, replaced when absent). Net: 8 files, +7/-79, -9 padding tests, +1 real two-branch test. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Controller/Test_Controller_Actions.php | 6 ------ .../Test_Controller_Form_Settings.php | 6 ------ .../Controller/Test_Controller_PDF.php | 16 -------------- .../Test_Controller_Uninstaller.php | 7 ------- .../integration/Model/Test_Installer.php | 21 +++++++------------ .../integration/Model/Test_Model_Settings.php | 10 --------- .../integration/Model/Test_Templates.php | 6 ------ .../integration/Model/Test_Uninstaller.php | 14 ------------- 8 files changed, 7 insertions(+), 79 deletions(-) diff --git a/tests/phpunit/integration/Controller/Test_Controller_Actions.php b/tests/phpunit/integration/Controller/Test_Controller_Actions.php index 7cdb3cc91..422e483be 100644 --- a/tests/phpunit/integration/Controller/Test_Controller_Actions.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Actions.php @@ -82,12 +82,6 @@ public function test_route_notices_short_circuits_on_getting_started_page() { $this->assertFalse( $gfpdf->notices->has_notice() ); } - public function test_route_does_nothing_when_post_action_absent() { - $this->controller->route(); - - $this->assertTrue( true ); - } - public function test_route_dismisses_notice_when_dismiss_flag_set() { global $gfpdf; diff --git a/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php b/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php index 96af8185b..e816173f0 100644 --- a/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php @@ -105,12 +105,6 @@ public function test_clear_cached_pdf_settings_ignores_when_no_save_action_poste $this->assertSame( $form, $result ); } - public function test_maybe_save_pdf_settings_no_op_when_pid_missing() { - $this->controller->maybe_save_pdf_settings(); - - $this->assertTrue( true ); - } - public function test_conditional_logic_is_value_match_returns_original_for_unrelated_fields() { $result = $this->controller->conditional_logic_is_value_match( false, diff --git a/tests/phpunit/integration/Controller/Test_Controller_PDF.php b/tests/phpunit/integration/Controller/Test_Controller_PDF.php index d2ab2c7fe..6b584ae74 100644 --- a/tests/phpunit/integration/Controller/Test_Controller_PDF.php +++ b/tests/phpunit/integration/Controller/Test_Controller_PDF.php @@ -114,22 +114,6 @@ public function test_prevent_index_defines_donotcachepage_constant() { $this->assertTrue( DONOTCACHEPAGE ); } - public function test_process_pdf_endpoint_returns_silently_without_query_vars() { - $GLOBALS['wp']->query_vars = []; - - $this->controller->process_pdf_endpoint(); - - $this->assertTrue( true ); - } - - public function test_process_legacy_pdf_endpoint_returns_silently_without_legacy_params() { - $_GET = []; - - $this->controller->process_legacy_pdf_endpoint(); - - $this->assertTrue( true ); - } - public function test_sgoptimizer_html_minification_fix_emits_doing_it_wrong() { $this->setExpectedIncorrectUsage( 'GFPDF\Controller\Controller_PDF::sgoptimizer_html_minification_fix' ); diff --git a/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php b/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php index 767ab568f..52f38a473 100644 --- a/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php @@ -75,11 +75,4 @@ public function test_render_uninstall_outputs_nothing_when_capability_denied() { $this->assertSame( '', $output ); } - public function test_init_skips_registration_outside_uninstall_subview() { - $_GET = [ 'page' => 'gf_settings', 'subview' => 'something-else' ]; - $this->controller->init(); - $_GET = []; - - $this->assertTrue( true ); - } } diff --git a/tests/phpunit/integration/Model/Test_Installer.php b/tests/phpunit/integration/Model/Test_Installer.php index 10bf20c3d..264cc745d 100644 --- a/tests/phpunit/integration/Model/Test_Installer.php +++ b/tests/phpunit/integration/Model/Test_Installer.php @@ -263,21 +263,14 @@ public function test_register_rewrite_tags_returns_input_unchanged_without_pdf_q $this->assertSame( [ 'existing' ], $tags ); } - public function test_maybe_flush_rewrite_rules_runs_without_error_when_rules_missing() { - update_option( 'rewrite_rules', [] ); + public function test_maybe_flush_rewrite_rules_triggers_flush_only_when_rule_absent() { + $sentinel = [ '^existing/rule/' => 'index.php?a=1' ]; + update_option( 'rewrite_rules', $sentinel ); - $this->model->maybe_flush_rewrite_rules( [ '^some/missing/rule/' ] ); + $this->model->maybe_flush_rewrite_rules( [ '^existing/rule/' ] ); + $this->assertSame( $sentinel, get_option( 'rewrite_rules' ) ); - $this->assertTrue( true ); - } - - public function test_setup_multisite_template_location_returns_early_on_single_site() { - if ( is_multisite() ) { - $this->markTestSkipped( 'Single-site path only.' ); - } - - $this->model->setup_multisite_template_location(); - - $this->assertTrue( true ); + $this->model->maybe_flush_rewrite_rules( [ '^never/exists/' ] ); + $this->assertNotSame( $sentinel, get_option( 'rewrite_rules' ) ); } } diff --git a/tests/phpunit/integration/Model/Test_Model_Settings.php b/tests/phpunit/integration/Model/Test_Model_Settings.php index d4e33ab9c..42c487cb5 100644 --- a/tests/phpunit/integration/Model/Test_Model_Settings.php +++ b/tests/phpunit/integration/Model/Test_Model_Settings.php @@ -271,16 +271,6 @@ public function test_licensing_bulk_license_check_returns_false_when_addons_have $this->assertFalse( $this->model->licensing_bulk_license_check() ); } - public function test_run_network_update_check_returns_early_on_single_site() { - if ( is_multisite() ) { - $this->markTestSkipped( 'Single-site path only.' ); - } - - $this->model->run_network_update_check(); - - $this->assertTrue( true ); - } - public function test_licensing_bulk_license_check_bad_response() { $this->addon->init(); $this->addon1->init(); diff --git a/tests/phpunit/integration/Model/Test_Templates.php b/tests/phpunit/integration/Model/Test_Templates.php index 16a3d9d38..4d64946ac 100644 --- a/tests/phpunit/integration/Model/Test_Templates.php +++ b/tests/phpunit/integration/Model/Test_Templates.php @@ -311,12 +311,6 @@ public function test_get_template_info() { $this->assertEquals( 'Zadani', $info[0]['template'] ); } - public function test_maybe_run_template_setup_no_op_when_no_headers() { - $this->model->maybe_run_template_setup( [] ); - - $this->assertTrue( true ); - } - public function test_check_for_valid_pdf_templates_throws_on_invalid_filename() { global $gfpdf; diff --git a/tests/phpunit/integration/Model/Test_Uninstaller.php b/tests/phpunit/integration/Model/Test_Uninstaller.php index 36517bbaf..f3f901af0 100644 --- a/tests/phpunit/integration/Model/Test_Uninstaller.php +++ b/tests/phpunit/integration/Model/Test_Uninstaller.php @@ -197,20 +197,6 @@ public function test_remove_plugin_transients_clears_settings_user_data_transien $this->assertFalse( get_transient( 'gfpdf_settings_user_data' ) ); } - public function test_remove_folder_structure_skips_paths_that_do_not_exist() { - add_filter( - 'gfpdf_uninstall_path', - static function () { - return [ '/path/that/definitely/does/not/exist/' . uniqid() ]; - } - ); - - $this->model->remove_folder_structure(); - remove_all_filters( 'gfpdf_uninstall_path' ); - - $this->assertTrue( true ); - } - public function test_deactivate_plugin_defaults_basename_when_empty() { $this->model->deactivate_plugin( '' ); From a7e93641c5f96e21535e79c8b2deb0576323c849 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 25 May 2026 18:03:16 +1000 Subject: [PATCH 17/45] test(phpunit): characterize 49 previously untested Helper classes Phase 4 item 5 of the PHPUnit refactor (plan at .claude/plans/2026-05-25-phpunit-tests-refactor.md): - 40 Field renderers under integration/Helper/Fields/ (skipped Field_Multi_Choice per <30-LOC trivial rule) - 4 Fonts helpers (LocalFile, LocalFilesystem, SupportsOtl, TtfFontValidation) - 5 top-level Helper concretes (Helper_PDF, Helper_PDF_List_Table, Helper_Form, Helper_Pdf_Queue, Helper_Sha256_Url_Signer) Each test is a real characterization test with concrete observable assertions against the source class's behaviour. Product-family fields (Discount, Quantity, Shipping, Tax, Total) use the existing `non-group-products-form` fixture pattern from Test_Field_Products. Helper_Form's update_entry test uses a fresh factory-created entry instead of mutating the shared `gravityform-1` fixture. Skipped from this chunk: - Field_Coupon (constructor requires GF_Field_Coupon, not in test env without the Coupons add-on) - MonoLoggerPsrLog2And3 (only loadable when PSR\\Log v2/v3 is present; v1 alias in the test env produces an incompatible interface signature at autoload time) - Trivial files per the README rule (<30 LOC, no own methods, no constructor logic) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Helper/Fields/Test_Field_Address.php | 124 ++++++++++++++++++ .../Fields/Test_Field_Chainedselect.php | 85 ++++++++++++ .../Helper/Fields/Test_Field_Checkbox.php | 103 +++++++++++++++ .../Helper/Fields/Test_Field_Creditcard.php | 84 ++++++++++++ .../Helper/Fields/Test_Field_Date.php | 78 +++++++++++ .../Helper/Fields/Test_Field_Default.php | 50 +++++++ .../Helper/Fields/Test_Field_Discount.php | 73 +++++++++++ .../Helper/Fields/Test_Field_Email.php | 56 ++++++++ .../Fields/Test_Field_Fg_Ls_Consent.php | 70 ++++++++++ .../Fields/Test_Field_Fg_Ls_Signature.php | 74 +++++++++++ .../Helper/Fields/Test_Field_Fileupload.php | 82 ++++++++++++ .../Helper/Fields/Test_Field_Hidden.php | 55 ++++++++ .../Helper/Fields/Test_Field_Html.php | 57 ++++++++ .../Helper/Fields/Test_Field_Likert.php | 97 ++++++++++++++ .../Helper/Fields/Test_Field_Multiselect.php | 101 ++++++++++++++ .../Helper/Fields/Test_Field_Name.php | 76 +++++++++++ .../Helper/Fields/Test_Field_Number.php | 73 +++++++++++ .../Helper/Fields/Test_Field_Page.php | 56 ++++++++ .../Helper/Fields/Test_Field_Phone.php | 63 +++++++++ .../Helper/Fields/Test_Field_Post_Content.php | 43 ++++++ .../Helper/Fields/Test_Field_Post_Excerpt.php | 50 +++++++ .../Helper/Fields/Test_Field_Post_Tags.php | 54 ++++++++ .../Helper/Fields/Test_Field_Post_Title.php | 50 +++++++ .../Helper/Fields/Test_Field_Quantity.php | 113 ++++++++++++++++ .../Helper/Fields/Test_Field_Quiz.php | 61 +++++++++ .../Helper/Fields/Test_Field_Rank.php | 56 ++++++++ .../Helper/Fields/Test_Field_Rating.php | 55 ++++++++ .../Helper/Fields/Test_Field_Shipping.php | 75 +++++++++++ .../Helper/Fields/Test_Field_Slim.php | 52 ++++++++ .../Helper/Fields/Test_Field_Slim_Post.php | 70 ++++++++++ .../Helper/Fields/Test_Field_Subtotal.php | 56 ++++++++ .../Helper/Fields/Test_Field_Tax.php | 71 ++++++++++ .../Helper/Fields/Test_Field_Text.php | 48 +++++++ .../Helper/Fields/Test_Field_Time.php | 48 +++++++ .../Helper/Fields/Test_Field_Tos.php | 58 ++++++++ .../Helper/Fields/Test_Field_Total.php | 82 ++++++++++++ .../Helper/Fields/Test_Field_V3_List.php | 57 ++++++++ .../Helper/Fields/Test_Field_V3_Products.php | 55 ++++++++ .../Helper/Fields/Test_Field_V3_Section.php | 72 ++++++++++ .../Helper/Fields/Test_Field_Website.php | 62 +++++++++ .../Helper/Fonts/Test_LocalFile.php | 120 +++++++++++++++++ .../Helper/Fonts/Test_LocalFilesystem.php | 67 ++++++++++ .../Helper/Fonts/Test_SupportsOtl.php | 44 +++++++ .../Helper/Fonts/Test_TtfFontValidation.php | 62 +++++++++ .../integration/Helper/Test_Helper_Form.php | 112 ++++++++++++++++ .../integration/Helper/Test_Helper_PDF.php | 101 ++++++++++++++ .../Helper/Test_Helper_PDF_List_Table.php | 89 +++++++++++++ .../Helper/Test_Helper_Pdf_Queue.php | 115 ++++++++++++++++ .../Helper/Test_Helper_Sha256_Url_Signer.php | 84 ++++++++++++ 49 files changed, 3539 insertions(+) create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Address.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Chainedselect.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Checkbox.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Creditcard.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Date.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Default.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Discount.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Email.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Fg_Ls_Consent.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Fg_Ls_Signature.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Fileupload.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Hidden.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Html.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Likert.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Multiselect.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Name.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Number.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Page.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Phone.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Post_Content.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Post_Excerpt.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Post_Tags.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Post_Title.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Quantity.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Quiz.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Rank.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Rating.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Shipping.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Slim.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Slim_Post.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Subtotal.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Tax.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Text.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Time.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Tos.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Total.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_V3_List.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_V3_Products.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_V3_Section.php create mode 100644 tests/phpunit/integration/Helper/Fields/Test_Field_Website.php create mode 100644 tests/phpunit/integration/Helper/Fonts/Test_LocalFile.php create mode 100644 tests/phpunit/integration/Helper/Fonts/Test_LocalFilesystem.php create mode 100644 tests/phpunit/integration/Helper/Fonts/Test_SupportsOtl.php create mode 100644 tests/phpunit/integration/Helper/Fonts/Test_TtfFontValidation.php create mode 100644 tests/phpunit/integration/Helper/Test_Helper_Form.php create mode 100644 tests/phpunit/integration/Helper/Test_Helper_PDF.php create mode 100644 tests/phpunit/integration/Helper/Test_Helper_PDF_List_Table.php create mode 100644 tests/phpunit/integration/Helper/Test_Helper_Pdf_Queue.php create mode 100644 tests/phpunit/integration/Helper/Test_Helper_Sha256_Url_Signer.php diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Address.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Address.php new file mode 100644 index 000000000..44845048a --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Address.php @@ -0,0 +1,124 @@ +form['all-form-fields']; + $gf_field = null; + foreach ( $form['fields'] as $field ) { + if ( $field->type === 'address' ) { + $gf_field = new GF_Field_Address( $field ); + break; + } + } + + $entry = [ + 'id' => 0, + 'form_id' => $form['id'], + '15.1' => '12 Address St', + '15.2' => 'Line 2', + '15.3' => 'Cityville', + '15.4' => 'Statesman', + '15.5' => '5000', + '15.6' => 'Chad', + ]; + + $pdf_field = new Field_Address( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + $html = $pdf_field->html(); + $this->assertStringContainsString( '12 Address St', $html ); + $this->assertStringContainsString( 'Cityville', $html ); + $this->assertStringContainsString( 'Chad', $html ); + $this->assertStringContainsString( '
', $html ); + } + + public function test_value_returns_keyed_array() { + $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; + $gf_field = null; + foreach ( $form['fields'] as $field ) { + if ( $field->type === 'address' ) { + $gf_field = new GF_Field_Address( $field ); + break; + } + } + + $entry = [ + 'id' => 0, + 'form_id' => $form['id'], + '15.1' => '5 Main Rd', + '15.3' => 'Townsville', + '15.6' => 'Australia', + ]; + + $pdf_field = new Field_Address( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $value = $pdf_field->value(); + + $this->assertArrayHasKey( 'street', $value ); + $this->assertArrayHasKey( 'city', $value ); + $this->assertArrayHasKey( 'country', $value ); + $this->assertSame( '5 Main Rd', $value['street'] ); + $this->assertSame( 'Townsville', $value['city'] ); + $this->assertSame( 'Australia', $value['country'] ); + } + + public function test_is_empty_when_all_inputs_blank() { + $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; + $gf_field = null; + foreach ( $form['fields'] as $field ) { + if ( $field->type === 'address' ) { + $gf_field = new GF_Field_Address( $field ); + break; + } + } + + $entry = [ 'id' => 0, 'form_id' => $form['id'] ]; + $pdf_field = new Field_Address( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + $this->assertTrue( $pdf_field->is_empty() ); + } + + public function test_zip_before_city_format() { + $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; + $gf_field = null; + foreach ( $form['fields'] as $field ) { + if ( $field->type === 'address' ) { + $gf_field = new GF_Field_Address( $field ); + break; + } + } + + $entry = [ + 'id' => 0, + 'form_id' => $form['id'], + '15.3' => 'Berlin', + '15.5' => '10115', + ]; + + $pdf_field = new Field_Address( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + add_filter( 'gform_address_display_format', fn() => 'zip_before_city' ); + $html = $pdf_field->html(); + remove_all_filters( 'gform_address_display_format' ); + + $this->assertStringContainsString( '10115', $html ); + $this->assertStringContainsString( 'Berlin', $html ); + $this->assertLessThan( strpos( $html, 'Berlin' ), strpos( $html, '10115' ), 'ZIP should appear before city in zip_before_city format' ); + } +} diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Chainedselect.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Chainedselect.php new file mode 100644 index 000000000..ad694519e --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Chainedselect.php @@ -0,0 +1,85 @@ + $id, + 'label' => $label, + 'choices' => [ + [ + 'text' => 'Australia', + 'value' => 'Australia', + 'choices' => [ + [ 'text' => 'NSW', 'value' => 'NSW', 'choices' => [] ], + ], + ], + ], + 'inputs' => [ + [ 'id' => '5.1', 'label' => 'Level 1' ], + [ 'id' => '5.2', 'label' => 'Level 2' ], + ], + ] ); + } + + public function test_form_data_keys_contain_field_id_and_label() { + $gf_field = $this->make_gf_field(); + $entry = [ + 'id' => 0, + 'form_id' => 0, + '5.1' => 'Australia', + '5.2' => 'NSW', + ]; + + $pdf_field = new Field_Chainedselect( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $form_data = $pdf_field->form_data(); + + $this->assertArrayHasKey( 'field', $form_data ); + $this->assertArrayHasKey( 5, $form_data['field'] ); + $this->assertArrayHasKey( 'Location', $form_data['field'] ); + } + + public function test_value_contains_selected_items() { + $gf_field = $this->make_gf_field(); + $entry = [ + 'id' => 0, + 'form_id' => 0, + '5.1' => 'Australia', + '5.2' => 'NSW', + ]; + + $pdf_field = new Field_Chainedselect( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $value = $pdf_field->value(); + + $this->assertIsArray( $value ); + $this->assertContains( 'Australia', $value ); + } + + public function test_is_empty_when_no_selections() { + $gf_field = $this->make_gf_field(); + $entry = [ 'id' => 0, 'form_id' => 0 ]; + $pdf_field = new Field_Chainedselect( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + $this->assertTrue( $pdf_field->is_empty() ); + } + } +} + +namespace { + + if ( ! class_exists( 'GF_Chained_Field_Select' ) ) { + class GF_Chained_Field_Select extends \GF_Field { + public $type = 'chainedselect'; + } + } +} diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Checkbox.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Checkbox.php new file mode 100644 index 000000000..58ab2e086 --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Checkbox.php @@ -0,0 +1,103 @@ +form['all-form-fields']; + foreach ( $form['fields'] as $field ) { + if ( $field->type === 'checkbox' ) { + return new GF_Field_Checkbox( $field ); + } + } + $this->fail( 'No checkbox field found in all-form-fields fixture' ); + } + + public function test_html_renders_checked_choices_as_list() { + $gf_field = $this->make_field(); + $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; + + $entry = [ + 'id' => 0, + 'form_id' => $form['id'], + '6.2' => 'Checkbox Choice 2', + '6.3' => 'Checkbox Choice 3', + ]; + + $pdf_field = new Field_Checkbox( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + $html = $pdf_field->html(); + $this->assertStringContainsString( '
    ', $html ); + $this->assertStringContainsString( 'Checkbox Choice 2', $html ); + $this->assertStringContainsString( 'Checkbox Choice 3', $html ); + } + + public function test_html_is_empty_wrapper_when_nothing_checked() { + $gf_field = $this->make_field(); + $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; + + $entry = [ 'id' => 0, 'form_id' => $form['id'] ]; + $pdf_field = new Field_Checkbox( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + $this->assertTrue( $pdf_field->is_empty() ); + } + + public function test_form_data_contains_value_and_name_keys() { + $gf_field = $this->make_field(); + $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; + + $entry = [ + 'id' => 0, + 'form_id' => $form['id'], + '6.2' => 'Checkbox Choice 2', + ]; + + $pdf_field = new Field_Checkbox( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $form_data = $pdf_field->form_data(); + + $this->assertArrayHasKey( 6, $form_data['field'] ); + $this->assertArrayHasKey( '6_name', $form_data['field'] ); + $this->assertIsArray( $form_data['field'][6] ); + } + + public function test_show_value_filter_uses_value_instead_of_label() { + $gf_field = $this->make_field(); + $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; + + /* + * Choice 2: label = "Checkbox Choice 2 Text", value = "Checkbox Choice 2". + * When the filter is active the value is rendered, not the label. + * Assert: the value token appears AND the label-only suffix (" Text") is absent. + */ + $entry = [ + 'id' => 0, + 'form_id' => $form['id'], + '6.2' => 'Checkbox Choice 2', + ]; + + $pdf_field = new Field_Checkbox( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + add_filter( 'gfpdf_show_field_value', '__return_true' ); + $html = $pdf_field->html(); + remove_filter( 'gfpdf_show_field_value', '__return_true' ); + + $this->assertStringContainsString( 'Checkbox Choice 2', $html ); + $this->assertStringNotContainsString( 'Checkbox Choice 2 Text', $html ); + } +} diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Creditcard.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Creditcard.php new file mode 100644 index 000000000..485cee2f2 --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Creditcard.php @@ -0,0 +1,84 @@ + $id, + 'inputs' => [ + [ 'id' => "{$id}.1", 'label' => 'Card Number' ], + [ 'id' => "{$id}.2", 'label' => 'Expiration Month' ], + [ 'id' => "{$id}.3", 'label' => 'Expiration Year' ], + [ 'id' => "{$id}.4", 'label' => 'Card Type' ], + [ 'id' => "{$id}.5", 'label' => 'Cardholder Name' ], + ], + ] ); + } + + public function test_html_renders_masked_number_and_card_type() { + $gf_field = $this->make_cc_field(); + + /* + * GF stores only the last-four digits and the card type in the entry after + * payment processing; the full PAN is never persisted. The subfield keys + * are 1.1 (masked number) and 1.4 (card type). + */ + $entry = [ + 'id' => 0, + 'form_id' => 0, + '1.1' => 'XXXX XXXX XXXX 1234', + '1.4' => 'Visa', + ]; + + $pdf_field = new Field_Creditcard( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + $html = $pdf_field->html(); + $this->assertStringContainsString( 'XXXX XXXX XXXX 1234', $html ); + $this->assertStringContainsString( 'Visa', $html ); + } + + public function test_value_returns_type_and_number_keys() { + $gf_field = $this->make_cc_field(); + + $entry = [ + 'id' => 0, + 'form_id' => 0, + '1.1' => 'XXXX XXXX XXXX 5678', + '1.4' => 'Mastercard', + ]; + + $pdf_field = new Field_Creditcard( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $value = $pdf_field->value(); + + $this->assertArrayHasKey( 'type', $value ); + $this->assertArrayHasKey( 'number', $value ); + $this->assertSame( 'Mastercard', $value['type'] ); + $this->assertSame( 'XXXX XXXX XXXX 5678', $value['number'] ); + } + + public function test_html_omits_empty_subfields() { + $gf_field = $this->make_cc_field(); + $entry = [ 'id' => 0, 'form_id' => 0, '1.4' => 'Visa' ]; + $pdf_field = new Field_Creditcard( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + $html = $pdf_field->html(); + $this->assertStringContainsString( 'Visa', $html ); + $this->assertStringNotContainsString( '
    ', $html ); + } +} diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Date.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Date.php new file mode 100644 index 000000000..dc8f4b16d --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Date.php @@ -0,0 +1,78 @@ +form['all-form-fields']; + $gf_field = null; + foreach ( $form['fields'] as $field ) { + if ( $field->type === 'date' ) { + $gf_field = new GF_Field_Date( $field ); + break; + } + } + + $entry = [ + 'id' => 0, + 'form_id' => $form['id'], + '12' => '2015-01-01', + ]; + + $pdf_field = new Field_Date( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $value = $pdf_field->value(); + + /* dmy format yields "01/01/2015" — day and month precede the year, unlike the raw "2015-01-01" */ + $this->assertSame( '01/01/2015', $value ); + } + + public function test_html_contains_formatted_date() { + $form = $GLOBALS['GFPDF_Test']->form['all-form-fields']; + $gf_field = null; + foreach ( $form['fields'] as $field ) { + if ( $field->type === 'date' ) { + $gf_field = new GF_Field_Date( $field ); + break; + } + } + + $entry = [ + 'id' => 0, + 'form_id' => $form['id'], + '12' => '2015-01-01', + ]; + + $pdf_field = new Field_Date( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $html = $pdf_field->html(); + + $this->assertStringContainsString( '2015', $html ); + } + + public function test_is_empty_when_no_date_stored() { + $gf_field = new GF_Field_Date( [ 'id' => 12, 'dateFormat' => 'mdy' ] ); + $entry = [ 'id' => 0, 'form_id' => 0 ]; + $pdf_field = new Field_Date( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + $this->assertTrue( $pdf_field->is_empty() ); + } +} diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Default.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Default.php new file mode 100644 index 000000000..b2a28a0fb --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Default.php @@ -0,0 +1,50 @@ +id = 1; + $gf_field->type = 'text'; + + $entry = [ + 'id' => 0, + 'form_id' => 0, + '1' => 'Hello World', + ]; + + $pdf_field = new Field_Default( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $html = $pdf_field->html(); + + $this->assertStringContainsString( 'Hello World', $html ); + } + + public function test_value_esc_htmls_string_input() { + $gf_field = new GF_Field(); + $gf_field->id = 1; + $gf_field->type = 'text'; + + $entry = [ + 'id' => 0, + 'form_id' => 0, + '1' => 'Safe text', + ]; + + $pdf_field = new Field_Default( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $value = $pdf_field->value(); + + $this->assertStringContainsString( 'Safe', $value ); + $this->assertStringNotContainsString( '', $value ); + } +} diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Discount.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Discount.php new file mode 100644 index 000000000..1508361df --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Discount.php @@ -0,0 +1,73 @@ +form( 'non-group-products-form' ); + $form_id = $this->gf_factory()->form->create( [], $form_data ); + $entry_data = $GLOBALS['GFPDF_Test']->entries['non-group-products-form'][0]; + $entry_data['form_id'] = $form_id; + $entry_id = $this->gf_factory()->entry->create( $entry_data ); + $entry = \GFAPI::get_entry( $entry_id ); + + $gf_field = new GF_Field(); + $gf_field->id = 99; + $gf_field->type = 'discount'; + + $pdf_field = new Field_Discount( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $pdf_field->set_products( new Field_Products( new GF_Field(), $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ) ); + + return $pdf_field; + } + + public function test_is_empty_when_gp_ecommerce_fields_absent() { + $gf_field = new GF_Field(); + $gf_field->id = 1; + $gf_field->type = 'discount'; + + $entry = [ 'id' => 0, 'form_id' => 0 ]; + $pdf_field = new Field_Discount( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $pdf_field->set_products( new Field_Products( new GF_Field(), $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ) ); + + /* + * GP_Ecommerce_Fields is not present in the test environment so + * Field_Discount::is_empty() short-circuits and returns true. + */ + $this->assertTrue( $pdf_field->is_empty() ); + } + + public function test_value_returns_empty_array_when_no_matching_product() { + $pdf_field = $this->make_pdf_field_with_real_entry(); + $value = $pdf_field->value(); + + $this->assertIsArray( $value ); + $this->assertEmpty( $value ); + } + + public function test_form_data_returns_empty_strings_when_no_discount() { + $pdf_field = $this->make_pdf_field_with_real_entry(); + $form_data = $pdf_field->form_data(); + + $this->assertArrayHasKey( 'field', $form_data ); + + foreach ( $form_data['field'] as $v ) { + $this->assertSame( '', $v ); + } + } +} diff --git a/tests/phpunit/integration/Helper/Fields/Test_Field_Email.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Email.php new file mode 100644 index 000000000..4a59d6dfb --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Email.php @@ -0,0 +1,56 @@ + 1 ] ); + + $entry = [ + 'id' => 0, + 'form_id' => 0, + '1' => 'support@gravitypdf.com', + ]; + + $pdf_field = new Field_Email( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + $html = $pdf_field->html(); + + $this->assertStringContainsString( 'href="mailto:support@gravitypdf.com"', $html ); + $this->assertStringContainsString( 'support@gravitypdf.com', $html ); + } + + public function test_html_esc_htmls_non_email_string() { + $gf_field = new GF_Field_Email( [ 'id' => 1 ] ); + + $entry = [ + 'id' => 0, + 'form_id' => 0, + '1' => 'not-an-email' ]; + + $pdf_field = new Field_Post_Content( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + $this->assertStringNotContainsString( '' ]; + + $pdf_field = new Field_Text( $gf_field, $entry, \GPDFAPI::get_form_class(), \GPDFAPI::get_misc_class() ); + + $this->assertStringNotContainsString( '