diff --git a/.github/workflows/phpunit.tests.yml b/.github/workflows/phpunit.tests.yml index 9424ecab3..19639e18d 100644 --- a/.github/workflows/phpunit.tests.yml +++ b/.github/workflows/phpunit.tests.yml @@ -53,6 +53,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 + # Fixtures must go through static::load_fixtures() on HasGfpdfFixtures; + # the shared $GLOBALS['GFPDF_Test'] global and direct GFAPI form/entry + # creates are gone, and this gate prevents them from coming back. + - name: Fail on banned fixture patterns + run: | + if grep -rnE "\\\$GLOBALS\\[['\\\"]GFPDF_Test['\\\"]\\]|GFAPI::add_(form|entry)\\b" tests/phpunit/integration/ --include="*.php"; then + echo "::error::Banned fixture pattern found in tests/phpunit/integration/. Use static::load_fixtures() instead." + exit 1 + fi + - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -110,24 +120,69 @@ jobs: - name: Install / Setup Gravity PDF + WordPress if: ${{ matrix.report }} - run: yarn wp-env:integration start --xdebug=debug + run: yarn wp-env:integration start --xdebug=coverage - name: Run PHPUnit tests if: ${{ ! matrix.report }} run: | - yarn test:php --do-not-cache-result --verbose + yarn test:php --do-not-cache-result --verbose --log-junit=/var/www/html/wp-content/plugins/gravity-pdf/tmp/junit/phpunit-integration-php${{ matrix.php }}.xml # Multisite suite reuses the already-running php 8.3 container instead of # spinning up its own matrix cell + cold wp-env start. - name: Run Multisite PHPUnit tests if: ${{ matrix.php == '8.3' && ! matrix.report }} run: | - yarn test:php:multisite --verbose + yarn test:php:multisite --verbose --log-junit=/var/www/html/wp-content/plugins/gravity-pdf/tmp/junit/phpunit-multisite-php${{ matrix.php }}.xml - - name: Generate Code Coverage Report for PHP + # The yarn test:php wrapper fails under xdebug coverage with + # "RecursiveDirectoryIterator on src/templates" (PHPUnit 9.6 + Xdebug 3 + # working-dir resolution). Invoke phpunit directly inside the container. + - name: Generate Code Coverage Report for PHP (single-site) + if: ${{ matrix.report }} + run: | + yarn wp-env:integration run wordpress bash -c ' + cd /var/www/html/wp-content/plugins/gravity-pdf && + vendor/bin/phpunit \ + -c tools/phpunit/config.xml \ + --do-not-cache-result \ + --verbose \ + --log-junit=tmp/junit/phpunit-coverage-php${{ matrix.php }}.xml \ + --coverage-clover=tmp/coverage/report-xml/php-coverage1.xml + ' + + - name: Generate Code Coverage Report for PHP (multisite) + if: ${{ matrix.report }} + run: | + yarn wp-env:integration run wordpress bash -c ' + cd /var/www/html/wp-content/plugins/gravity-pdf && + vendor/bin/phpunit \ + -c tools/phpunit/config-multisite.xml \ + --do-not-cache-result \ + --verbose \ + --log-junit=tmp/junit/phpunit-coverage-multisite-php${{ matrix.php }}.xml \ + --coverage-clover=tmp/coverage/report-xml/php-coverage-multisite.xml + ' + + - name: Enforce coverage floor if: ${{ matrix.report }} - run: | - yarn test:php --do-not-cache-result --verbose --coverage-clover=/var/www/html/wp-content/plugins/gravity-pdf/tmp/coverage/report-xml/php-coverage1.xml + run: | + php tools/phpunit/coverage-gate.php \ + tmp/coverage/report-xml/php-coverage1.xml \ + tmp/coverage/report-xml/php-coverage-multisite.xml + + - name: Upload PHPUnit JUnit timings + uses: actions/upload-artifact@v6 + if: always() + with: + name: phpunit-junit-php${{ matrix.php }}${{ matrix.report && '-coverage' || '' }} + path: tmp/junit/*.xml + + - name: Upload PHPUnit coverage XML + uses: actions/upload-artifact@v6 + if: ${{ matrix.report && always() }} + with: + name: phpunit-coverage-clover + path: tmp/coverage/report-xml/*.xml - name: Code Coverage Upload uses: codecov/codecov-action@v6 diff --git a/tests/phpunit/COVERAGE_BASELINE.md b/tests/phpunit/COVERAGE_BASELINE.md new file mode 100644 index 000000000..ef105630a --- /dev/null +++ b/tests/phpunit/COVERAGE_BASELINE.md @@ -0,0 +1,348 @@ +# 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 was the CI gate enforced by `tools/phpunit/coverage-gate.php` at the end of Phase 0. See the Phase 4 revision below for the current floor. + +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. 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`. + +## Phase 4 revision (2026-05-25) + +After Phase 4 closed the bulk of the coverage gap — characterization tests for Exceptions, Statics, 11 untested controllers, 5 model gaps, 49 helpers (including all `Helper/Fields/`), Views, and the `Helper/Log` + `Helper/Mpdf` mirror work — re-running the same methodology yields: + +| Metric | Phase 0 | Phase 4 | Δ | +| --- | ---: | ---: | ---: | +| Test count | 1119 | **1424** | +305 | +| Assertions | 4088 | **21314** | +17226 | +| Wall-clock (coverage mode) | 47s | **42s** | −5s | +| Wall-clock (no coverage) | 41.7s | **31.7s** | −10s | + +| `src/` subdirectory | Files | Stmts covered / total | Line coverage | Δ vs Phase 0 | +| :--- | ---: | :--- | ---: | ---: | +| `src/Statics/` | 4 | 327 / 347 | **94.24%** | +11.53 pp | +| `src/Helper/Mpdf/` | 3 | 41 / 44 | **93.18%** | +18.18 pp | +| `src/Rest/` | 2 | 546 / 588 | **92.86%** | 0 | +| `src/Helper/Fields/` | 60 | 1541 / 1783 | **86.43%** | +13.78 pp | +| `src/Controller/` | 19 | 937 / 1100 | **85.18%** | +3.73 pp | +| `src/Helper/` (top level) | 39 | 3386 / 4026 | **84.10%** | +0.44 pp | +| `src/Model/` | 11 | 1975 / 2385 | **82.81%** | +1.55 pp | +| `src/Helper/Fonts/` | 5 | 31 / 38 | **81.58%** | 0 | +| Plugin root | 3 | 209 / 289 | **72.32%** | 0 | +| `src/Helper/Log/` | 3 | 118 / 170 | **69.41%** | 0 | +| `src/View/` | 35 | 686 / 1054 | **65.09%** | +3.68 pp | +| `src/Helper/Licensing/` | 1 | 168 / 298 | **56.38%** | 0 | +| `src/` root | 3 | 290 / 557 | **52.06%** | 0 | +| `src/Exceptions/` | 11 | 11 / 22 | **50.00%** | +27.27 pp | +| `src/templates/` | 9 | 97 / 307 | **31.60%** | 0 | +| **OVERALL** | **208** | **10363 / 13008** | **79.67%** | **+3.34 pp** | + +The CI gate in `tools/phpunit/coverage-gate.php` was ratcheted to **79.67%** at the close of Phase 4. The follow-up pass below raised it to **80.06%**. + +## Phase 4 follow-up (2026-05-25) + +Critical-class gap-fill pass on `Helper/Licensing/EDD_SL_Plugin_Updater`, `Helper_Abstract_Addon`, and `Model_PDF`: + +| Metric | Phase 4 | Follow-up | Δ | +| --- | ---: | ---: | ---: | +| Test count | 1424 | **1471** | +47 | +| Assertions | 21314 | **21970** | +656 | +| Wall-clock (coverage mode) | 42s | 55s | +13s | +| Wall-clock (no coverage) | 31.7s | 38.2s | +6.5s | + +Coverage-mode wall-clock grew because the new tests exercise hot paths under xdebug instrumentation; the no-coverage delta is the truer signal of suite weight. + +| `src/` subdirectory | Files | Stmts covered / total | Line coverage | Δ vs Phase 4 | +| :--- | ---: | :--- | ---: | ---: | +| `src/Helper/Licensing/` | 1 | 176 / 298 | **59.06%** | +2.68 pp | +| `src/Helper/` (top level) | 39 | 3428 / 4026 | **85.15%** | +1.05 pp | +| `src/Model/` | 11 | 1976 / 2385 | **82.85%** | +0.04 pp | +| **OVERALL** | **208** | **10414 / 13008** | **80.06%** | **+0.39 pp** | + +All other subdirectories unchanged. + +The CI gate was ratcheted to **80.06%** at the close of the follow-up. See follow-up #2 below for a recalibration to **79.95%** after observing run-to-run xdebug variance. + +Remaining gaps after the follow-up pass: + +- **Helper/Licensing** (40.9 pp gap, 122 statements) — remaining uncovered code is `get_version_from_remote` failure modes that require deeper mocking, and `show_changelog`'s `install_plugin_information()` path (calls `exit;`). +- **Model_PDF** still has ~17% uncovered, mostly in seldom-exercised paths like `get_quiz_results`/`get_poll_results`/`get_survey_results` add-on integration branches. +- **Helper/Log** (30.6 pp gap, 52 statements) — `Log/Logger::get_monolog()` has the PSR-Log v2/v3 detection branches; `MonoLoggerPsrLog2And3` itself cannot be exercised at runtime in this test env (see commit `b2cce9ed`). +- **src/ root** (47.9 pp gap, 267 statements) — `bootstrap.php` activation paths are genuinely hard to characterize without rewriting the bootstrap as a class. +- **View** (34.9 pp gap, 368 statements) — most remaining uncovered Views are HTML-partial paths; out of scope per the plan. +- **Exceptions** (50.0 pp gap, 11 statements) — the hierarchy test pins inheritance for all subclasses but doesn't construct each one. Tiny absolute gap; not worth a dedicated pass. + +## Phase 4 follow-up #2 (2026-05-25) + +Second gap-fill pass on `Helper/Licensing/EDD_SL_Plugin_Updater` — 8 new tests covering the `show_changelog` permission-denial `wp_die` path, `get_version_from_remote` WP_Error branch, `request_recently_failed` non-numeric value branch, direct `log_failed_request`, the explicit-cache-key paths in `get_cached_version_info`/`set_version_info_cache`/`delete_version_info_cache`, and `get_repo_api_data`'s cached-return branch. + +| Metric | Follow-up | Follow-up #2 | Δ | +| --- | ---: | ---: | ---: | +| Test count | 1471 | **1479** | +8 | +| Assertions | 21970 | **22078** | +108 | + +| `src/` subdirectory | Files | Stmts covered / total | Line coverage | Δ vs Follow-up | +| :--- | ---: | :--- | ---: | ---: | +| `src/Helper/Licensing/` | 1 | 179 / 298 | **60.07%** | +1.01 pp | +| **OVERALL** | **208** | **10406 / 13008** | **80.00%** | -0.06 pp (see note) | + +**Note on the overall delta:** the previous follow-up's 80.06% measurement was at the high end of natural xdebug coverage variance. Re-measuring the same source/tests pre-this-change yields **79.97%** (3 statement drift in Helper top-level + Statics), and three runs with the new tests applied land at 80.00–80.01%. The Helper/Licensing **+3 statements / +1.01 pp** is reproducible; the overall floor sits ~0.05 pp below the previously reported figure once natural variance is accounted for. + +The CI gate in `tools/phpunit/coverage-gate.php` is recalibrated to **79.95%** — a ~0.05 pp safety margin below the worst observed run with the new tests included, still well above the Phase 4 baseline of 79.67%. See follow-up #3 below for a further ratchet to **80.25%** after a third gap-fill pass. + +## Phase 4 follow-up #3 (2026-05-25) + +Third gap-fill pass targeting the remaining items on the follow-up #2 punch list: `Model_PDF` quiz/poll/survey add-on integration branches, `Helper/Log/Logger` `setup_gravityforms_logging` early-return + ERROR-level branches, and `src/bootstrap.php` plugin-meta/admin-message/asset-registration paths. + +| Metric | Follow-up #2 | Follow-up #3 | Δ | +| --- | ---: | ---: | ---: | +| Test count | 1479 | **1500** | +21 | +| Assertions | 22078 | **22368** | +290 | + +| `src/` subdirectory | Files | Stmts covered / total | Line coverage | Δ vs Follow-up #2 | +| :--- | ---: | :--- | ---: | ---: | +| `src/` root | 3 | 336 / 557 | **60.32%** | +8.26 pp | +| `src/Model/` | 11 | 1977 / 2385 | **82.89%** | +0.04 pp | +| `src/Helper/Log/` | 3 | 119 / 170 | **70.00%** | +0.59 pp | +| **OVERALL** | **208** | **10456 / 13008** | **80.38%** | **+0.38 pp** | + +The biggest single gain is `src/bootstrap.php` (+46 statements): tests for `plugin_action_links`, `plugin_row_meta`, `add_body_class`, `tinymce_styles`, `register_assets`, `get_config_data`, and `add_admin_messages` exercise paths previously reachable only via real WordPress page loads. + +`Model_PDF` and `Helper/Log/Logger` gains are smaller in absolute terms because their remaining uncovered branches require third-party dependencies (Gravity Forms add-on data sources, PSR-Log v2/v3 libraries) that aren't loaded in this test bootstrap — see the standing note about `MonoLoggerPsrLog2And3` (commit `b2cce9ed`). + +Two consecutive coverage runs land at 80.37–80.38% (2-statement drift in `Statics/`). The CI gate is ratcheted to **80.25%** — ~0.10 pp safety margin below the worst observed run, still well above the previous floor of 79.95%. See follow-up #4 below for a methodology fix that surfaces an additional ~1.20 pp of coverage that was being measured but discarded. + +## Phase 4 follow-up #4 (2026-05-25) — measurement methodology + +The Phase 0–follow-up #3 baselines all measured **single-site PHPUnit coverage only**, even though the project ships a separate multisite PHPUnit suite (`tools/phpunit/config-multisite.xml`) that the integration job also runs. Tests that guard with `markTestSkipped( ! is_multisite() )` (the 7 `test_show_update_notification_*` cases, the multisite-only branch of `is_non_active_multisite`, and the `set_version_info_cache` multisite skip) execute under the multisite suite but never contributed to the coverage-gate measurement. + +Resolution: + +1. `tools/phpunit/coverage-gate.php` and `tools/phpunit/coverage-baseline.php` now accept multiple Clover paths and **union per-line counts** (a line is "covered" if any input has `count > 0`). Helper extracted to `tools/phpunit/coverage-merge-lib.php`. +2. `.github/workflows/phpunit.tests.yml` runs `phpunit -c tools/phpunit/config-multisite.xml --coverage-clover=...` alongside the single-site coverage step and passes both clovers to the gate. +3. Local reproduction: see the updated methodology section below. + +| `src/` subdirectory | Single-site only | Single + multisite (union) | Δ | +| :--- | ---: | ---: | ---: | +| `Helper/Licensing/` | 60.07% | **96.31%** | +36.24 pp | +| `Model/` | 82.89% | **84.07%** | +1.18 pp | +| `Helper/` (top level) | 84.92% | **85.17%** | +0.25 pp | +| `Statics/` | 93.66% | **94.52%** | +0.86 pp | +| `Helper/Log/` | 70.00% | **73.91%** | +3.91 pp | +| `Controller/` | 85.18% | **85.27%** | +0.09 pp | +| `Helper/Mpdf/` | 93.18% | 93.18% | 0 | +| `Helper/Fields/` | 86.43% | 86.43% | 0 | +| `Helper/Fonts/` | 81.58% | 81.58% | 0 | +| **OVERALL** | 80.37% | **81.57%** | **+1.20 pp** | + +The bulk of the gain is `show_update_notification` in `EDD_SL_Plugin_Updater` — 7 multisite-only tests that were always pinning behavior, just not measured. + +Remaining genuine `Helper/Licensing` gaps after the union (11 statements): + +- `check_update` non-object-input branch (`L80`) — easy add later. +- `show_changelog` happy path (`L495–L510`) — calls `install_plugin_information()` → `exit;`, untestable without process forking. + +The CI gate is ratcheted to **81.45%** — ~0.10 pp safety margin below the merged measurement, well above the previous floor of 80.25%. + +**Statement-total drift note:** the union baseline reports **12 998** total statements vs single-site's **13 008**. The merge script uses the first input's `` set as the canonical statement list; lines that xdebug instruments as statements in one bootstrap but as different element types in the other contribute small drift. Worth knowing when comparing absolute statement counts across follow-ups; the ratios are stable. + +## Playwright (e2e) baseline + +| Metric | Value (local 2026-05-25, post `yarn build` + `composer install`) | +| --- | --- | +| Passed | 87 | +| Failed | 0 | +| Did not run | 0 | +| Wall-clock | 2.2m (135s) | + +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. + +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 + +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. Run both +# single-site and multisite under coverage to capture multisite-only tests. +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 && + vendor/bin/phpunit \ + -c tools/phpunit/config-multisite.xml \ + --do-not-cache-result \ + --coverage-clover=tmp/coverage/report-xml/multisite.xml \ + --log-junit=tmp/junit/phpunit-coverage-ms.xml +' + +# Pass both clovers to the gate + breakdown — they union per-line. +php tools/phpunit/coverage-gate.php \ + tmp/coverage/report-xml/baseline.xml \ + tmp/coverage/report-xml/multisite.xml + +php tools/phpunit/coverage-baseline.php \ + tmp/coverage/report-xml/baseline.xml \ + tmp/coverage/report-xml/multisite.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 + +```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/tests/phpunit/Concerns/HasGfpdfFixtures.php b/tests/phpunit/Concerns/HasGfpdfFixtures.php new file mode 100644 index 000000000..1f0e940cd --- /dev/null +++ b/tests/phpunit/Concerns/HasGfpdfFixtures.php @@ -0,0 +1,157 @@ +form() / $this->entry() / $this->entries(), + * and inherits cleanup via tear_down_after_class() (wired in TestCase / AjaxTestCase). + */ +trait HasGfpdfFixtures { + + /** + * Form key → entry-fixture filename. The original bootstrap mixed + * -entries.json and -entry.json suffixes, so a map is the source of truth. + */ + private static $entry_filenames = [ + 'all-form-fields' => 'all-form-fields-entries.json', + 'gravityform-1' => 'gravityform-1-entries.json', + 'repeater-empty-form' => 'repeater-empty-entry.json', + 'repeater-consent-form' => 'repeater-consent-entry.json', + 'non-group-products-form' => 'non-group-products-form-entries.json', + ]; + + /** + * Per-class fixture cache, keyed by class name (late static binding). + * + * Shape: [ 'Test_Foo' => [ 'forms' => [ key => array ], 'entries' => [ key => array[] ] ] ]. + * + * Protected (not private) so subclasses can patch entries after load_fixtures + * — e.g. Test_Form_Data rewrites file-upload URLs to match the per-class form's + * upload directory before tests run. + */ + protected static $fixture_caches = []; + + /** + * Class-scoped fixture loader. Call from set_up_before_class(). + * + * @param string[] $forms Form keys; each loads tools/phpunit/data/forms/.json. + * @param string[] $entries Entry-set keys; the parent form must be in $forms + * (entries are created against the just-loaded form's ID). + */ + protected static function load_fixtures( array $forms = [], array $entries = [] ) { + $factory = new \GF_UnitTest_Factory(); + $class = static::class; + $cache = self::$fixture_caches[ $class ] ?? [ 'forms' => [], 'entries' => [] ]; + + foreach ( $forms as $key ) { + $cache['forms'][ $key ] = $factory->form->import_fixture_and_get( "$key.json" ); + } + + foreach ( $entries as $key ) { + if ( ! isset( $cache['forms'][ $key ] ) ) { + throw new \LogicException( "Cannot load entry set '$key' before its parent form." ); + } + if ( ! isset( self::$entry_filenames[ $key ] ) ) { + throw new \LogicException( "No entry fixture mapping for '$key'." ); + } + + $cache['entries'][ $key ] = $factory->entry->import_many_and_get( + self::$entry_filenames[ $key ], + $cache['forms'][ $key ]['id'] + ); + } + + self::$fixture_caches[ $class ] = $cache; + } + + /** + * Deletes class-scoped fixtures from the database. Call from tear_down_after_class(). + * + * Forms+entries created by load_fixtures() outlive WP's per-test transaction + * (GFAPI writes go to non-transactional tables), so without this each class + * leaks its fixtures into subsequent classes. + */ + protected static function cleanup_class_fixtures() { + $class = static::class; + if ( ! isset( self::$fixture_caches[ $class ] ) ) { + return; + } + $cache = self::$fixture_caches[ $class ]; + + foreach ( $cache['entries'] as $entries ) { + foreach ( $entries as $entry ) { + \GFAPI::delete_entry( $entry['id'] ); + } + } + foreach ( $cache['forms'] as $form ) { + \GFAPI::delete_form( $form['id'] ); + } + unset( self::$fixture_caches[ $class ] ); + } + + /** + * Returns the form fixture stored under $key (declared via load_fixtures). + * + * @param string $key Form key. + * + * @return array + */ + protected function form( $key ) { + $cache = self::$fixture_caches[ static::class ]['forms'] ?? []; + if ( ! isset( $cache[ $key ] ) ) { + $available = implode( ', ', array_keys( $cache ) ) ?: '(none)'; + $this->fail( "Form fixture '$key' is not loaded. Available in " . static::class . ": $available" ); + } + + return $cache[ $key ]; + } + + /** + * Returns one of the entry fixtures stored under $key. + * + * @param string $key Entry-set key (same key as the parent form). + * @param int $index Zero-based index into the entry list. + * + * @return array + */ + protected function entry( $key, $index = 0 ) { + $cache = self::$fixture_caches[ static::class ]['entries'] ?? []; + if ( ! isset( $cache[ $key ][ $index ] ) ) { + $this->fail( "Entry fixture '$key'[$index] is not loaded in " . static::class . '.' ); + } + + return $cache[ $key ][ $index ]; + } + + /** + * Returns the full entry list for $key (for foreach/array_column use). + * + * @param string $key Entry-set key. + * + * @return array[] + */ + protected function entries( $key ) { + $cache = self::$fixture_caches[ static::class ]['entries'] ?? []; + if ( ! isset( $cache[ $key ] ) ) { + $this->fail( "Entry fixture set '$key' is not loaded in " . static::class . '.' ); + } + + return $cache[ $key ]; + } + + /** + * Returns the Gravity PDF Router (DI container). + * + * @return \GFPDF\Router + */ + protected function gfpdf() { + global $gfpdf; + + return $gfpdf; + } +} diff --git a/tests/phpunit/Concerns/UsesFactory.php b/tests/phpunit/Concerns/UsesFactory.php new file mode 100644 index 000000000..40f73b45f --- /dev/null +++ b/tests/phpunit/Concerns/UsesFactory.php @@ -0,0 +1,25 @@ +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..191daddc7 --- /dev/null +++ b/tests/phpunit/README.md @@ -0,0 +1,179 @@ +# 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 +│ └── 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 +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. + +## Base class + +| 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: +- `static::load_fixtures( [ 'all-form-fields' ], [ 'all-form-fields' ] )` — class-scoped + loader. Call from `set_up_before_class()` to declare the forms/entries this + test class needs. Forms+entries are created via the factory once per class + and cleaned up in `tear_down_after_class()`. +- `$this->form( 'all-form-fields' )` — form fixture (per-class cache; falls back + to the legacy `$GLOBALS['GFPDF_Test']` global until the migration is complete). +- `$this->entry( 'all-form-fields', 0 )` — entry fixture, same lookup order. +- `$this->gfpdf()` — the `GFPDF\Router` DI container (same as the `$gfpdf` global). +- `$this->assertFixturesIntact()` — automatically called from `set_up()` to catch + cross-test mutation of the legacy global. Override `set_up()` only if you call + `parent::set_up()`. Removed when the legacy global is removed (Phase D of the + fixtures-to-factory migration). + +`\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 + +```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`) so contributors can run a slice. + +## Fixture access + +Each test class declares the fixtures it needs in `set_up_before_class()` via +`static::load_fixtures( $form_keys, $entry_keys )`. The loader creates them via +the factory, stores them in a per-class cache, and the base class's +`tear_down_after_class()` deletes them when the class finishes. + +Available fixture keys (each loads `tools/phpunit/data/forms/.json`): + +| Key | Entries available | +| :--- | :--- | +| `all-form-fields` | 7 | +| `form-settings` | — | +| `gravityform-1` | 3 | +| `non-group-products-form` | 1 | +| `repeater-empty-form` | 1 | +| `repeater-consent-form` | 1 | + +**Legacy fallback (temporary):** the accessors also fall back to +`$GLOBALS['GFPDF_Test']` populated by `tools/phpunit/bootstrap.php::create_stubs()`, +which loads all seven forms + five entry batches once per suite. This is the +coexistence shim for the fixtures-to-factory migration; it is removed in +Phase D once every file has been converted to `load_fixtures()`. New tests +should not rely on it. + +**Never call `GFAPI::add_form()` / `GFAPI::add_entry()` directly from a test body.** +Use the factory for ad-hoc per-test forms/entries: + +```php +$form_id = $this->gf_factory()->form->create( [], $form ); +$entry['form_id'] = $form_id; +$entry_id = $this->gf_factory()->entry->create( $entry ); +``` + +**Class-scoped vs instance-scoped:** prefer class-scoped via `load_fixtures()`. Use +instance-scoped (per-test) only when the test mutates the fixture. See +`Model/Test_Slow_PDF_Processes.php` for an example of class-scoped expensive setup +(font copies). + +## 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..b718d7b13 --- /dev/null +++ b/tests/phpunit/integration/AjaxTestCase.php @@ -0,0 +1,26 @@ +gfpdf()->data->form_settings = []; + } + + public static function tear_down_after_class() { + static::cleanup_class_fixtures(); + parent::tear_down_after_class(); + } +} 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..422e483be --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Actions.php @@ -0,0 +1,119 @@ +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_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..385db8550 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Activation.php @@ -0,0 +1,64 @@ +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/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/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/unit-tests/Controller/Test_Controller_Export_Entries.php b/tests/phpunit/integration/Controller/Test_Controller_Export_Entries.php similarity index 73% 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..241ae33a1 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,9 +18,15 @@ * @group controller * @group export */ -class Test_Controller_Export_Entries extends WP_UnitTestCase { +class Test_Controller_Export_Entries extends TestCase { + + public static function set_up_before_class() { + parent::set_up_before_class(); + static::load_fixtures( [ 'all-form-fields' ], [ 'all-form-fields' ] ); + } + public function test_add_pdfs_to_export_fields() { - $form = apply_filters( 'gform_export_fields', $GLOBALS['GFPDF_Test']->form['all-form-fields'] ); + $form = apply_filters( 'gform_export_fields', $this->form( 'all-form-fields' ) ); $field_ids = array_column( $form['fields'], 'id' ); @@ -41,21 +47,21 @@ public function test_get_export_field_unrelated_value() { } public function test_get_export_field_empty_pdf_value_if_failed_conditional_logic() { - $form_id = $GLOBALS['GFPDF_Test']->form['all-form-fields']['id']; - $entry = $GLOBALS['GFPDF_Test']->entries['all-form-fields'][0]; + $form_id = $this->form( 'all-form-fields' )['id']; + $entry = $this->entry( 'all-form-fields' ); $field_id = 'gpdf_555ad84787d7e'; $this->assertEmpty( apply_filters( 'gform_export_field_value', 'item', $form_id, $field_id, $entry ) ); } public function test_get_export_field_pdf_value() { - $form_id = $GLOBALS['GFPDF_Test']->form['all-form-fields']['id']; - $entry = $GLOBALS['GFPDF_Test']->entries['all-form-fields'][0]; + $form_id = $this->form( 'all-form-fields' )['id']; + $entry = $this->entry( 'all-form-fields' ); $field_id = 'gpdf_556690c67856b'; $this->assertStringContainsString( 'http://example.org/?gpdf=1', apply_filters( 'gform_export_field_value', 'item', $form_id, $field_id, $entry ) ); } public function test_get_export_field_empty_value() { - $form_id = $GLOBALS['GFPDF_Test']->form['all-form-fields']['id']; + $form_id = $this->form( 'all-form-fields' )['id']; $field_id = 'gpdf_555ad84787d7e'; $value = 'item'; $this->assertSame( $value, apply_filters( 'gform_export_field_value', $value, $form_id, $field_id, [] ) ); 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..e816173f0 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Form_Settings.php @@ -0,0 +1,158 @@ +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_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..8dae6eb8c --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Install.php @@ -0,0 +1,102 @@ +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' ); + + $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 ); + + /* Multisite gates activate_plugins behind super admin; without the promotion + check_install_status() short-circuits on the capability check. No-op on + single-site. */ + grant_super_admin( $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..6b584ae74 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_PDF.php @@ -0,0 +1,162 @@ +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() { + $this->controller->prevent_index(); + + $this->assertTrue( defined( 'DONOTCACHEPAGE' ) ); + $this->assertTrue( DONOTCACHEPAGE ); + } + + 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/unit-tests/Controller/Test_Controller_Pdf_Queue.php b/tests/phpunit/integration/Controller/Test_Controller_Pdf_Queue.php similarity index 81% 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..c7822f07c 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 @@ 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->data->form_settings = []; $gfpdf->data->form_settings[ $form['id'] ] = $form['gfpdf_form_settings']; @@ -293,10 +298,11 @@ public function test_queue_async_form_submission_tasks() { $this->assertCount( 4, $queue ); - $this->assertStringContainsString( 'create-pdf-1-1', $queue[0][0]['id'] ); - $this->assertStringContainsString( 'create-pdf-1-1', $queue[1][0]['id'] ); - $this->assertStringContainsString( 'send-notification-1-1', $queue[2][0]['id'] ); - $this->assertStringContainsString( 'send-notification-1-1', $queue[3][0]['id'] ); + $prefix = "{$form['id']}-{$entry['id']}"; + $this->assertStringContainsString( "create-pdf-$prefix", $queue[0][0]['id'] ); + $this->assertStringContainsString( "create-pdf-$prefix", $queue[1][0]['id'] ); + $this->assertStringContainsString( "send-notification-$prefix", $queue[2][0]['id'] ); + $this->assertStringContainsString( "send-notification-$prefix", $queue[3][0]['id'] ); } /** @@ -309,7 +315,7 @@ public function test_queue_async_resend_notification_tasks() { $form = $results['form']; $form['notifications']['54bca349732b8']['isActive'] = true; - foreach( $GLOBALS['GFPDF_Test']->entries['all-form-fields'] as $entry ) { + foreach ( $this->entries( 'all-form-fields' ) as $entry ) { foreach ( $form['notifications'] as $notification ) { $this->controller->maybe_disable_submission_notifications( false, $notification, $form, $entry ); } @@ -321,13 +327,17 @@ public function test_queue_async_resend_notification_tasks() { $this->assertCount( 21, $queue ); - $this->assertStringContainsString( 'create-pdf-1-1', $queue[0][0]['id'] ); - $this->assertStringContainsString( 'create-pdf-1-1', $queue[1][0]['id'] ); - $this->assertStringContainsString( 'send-notification-1-1', $queue[2][0]['id'] ); + $entries = $this->entries( 'all-form-fields' ); + $first_entry = "{$form['id']}-{$entries[0]['id']}"; + $last_entry = "{$form['id']}-{$entries[6]['id']}"; - $this->assertStringContainsString( 'create-pdf-1-7', $queue[18][0]['id'] ); - $this->assertStringContainsString( 'create-pdf-1-7', $queue[19][0]['id'] ); - $this->assertStringContainsString( 'send-notification-1-7', $queue[20][0]['id'] ); + $this->assertStringContainsString( "create-pdf-$first_entry", $queue[0][0]['id'] ); + $this->assertStringContainsString( "create-pdf-$first_entry", $queue[1][0]['id'] ); + $this->assertStringContainsString( "send-notification-$first_entry", $queue[2][0]['id'] ); + + $this->assertStringContainsString( "create-pdf-$last_entry", $queue[18][0]['id'] ); + $this->assertStringContainsString( "create-pdf-$last_entry", $queue[19][0]['id'] ); + $this->assertStringContainsString( "send-notification-$last_entry", $queue[20][0]['id'] ); } /** @@ -386,7 +396,20 @@ public function test_cleanup_pdfs() { * @since 6.12.6 */ public function test_queue_cleanup() { - global $gfpdf; + global $gfpdf, $wp_settings_errors; + + /* + * Wipe state that other tests leak and that quietly breaks settings_sanitize: + * - $wp_settings_errors: prior add_settings_error calls flip update_settings into the empty-output branch (line 1188). + * - gfpdf_settings_user_data transient + $_GET keys: trigger get_settings to return transient instead of DB. + */ + $wp_settings_errors = []; + delete_transient( 'gfpdf_settings_user_data' ); + unset( $_GET['page'], $_GET['subview'] ); + + /* Seed gfpdf_settings deterministically and reload the in-memory cache. */ + update_option( 'gfpdf_settings', [ 'background_processing' => 'No' ] ); + $gfpdf->options->set_plugin_settings(); /* setup page */ $_POST['option_page'] = 'gfpdf_settings'; 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/unit-tests/Controller/Test_Controller_Settings.php b/tests/phpunit/integration/Controller/Test_Controller_Settings.php similarity index 95% rename from tests/phpunit/unit-tests/Controller/Test_Controller_Settings.php rename to tests/phpunit/integration/Controller/Test_Controller_Settings.php index 64b4a62ff..7bd0d2eb9 100644 --- a/tests/phpunit/unit-tests/Controller/Test_Controller_Settings.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Settings.php @@ -6,7 +6,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 @@ -20,7 +20,7 @@ * @group controller * @group settings */ -class Test_Controller_Settings extends WP_UnitTestCase { +class Test_Controller_Settings extends TestCase { /** * @var Controller_Settings */ 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/unit-tests/Controller/Test_Controller_System_Report.php b/tests/phpunit/integration/Controller/Test_Controller_System_Report.php similarity index 97% rename from tests/phpunit/unit-tests/Controller/Test_Controller_System_Report.php rename to tests/phpunit/integration/Controller/Test_Controller_System_Report.php index 33c718921..044c06cb4 100644 --- a/tests/phpunit/unit-tests/Controller/Test_Controller_System_Report.php +++ b/tests/phpunit/integration/Controller/Test_Controller_System_Report.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 system-report */ -class Test_Controller_System_Report extends WP_UnitTestCase { +class Test_Controller_System_Report extends TestCase { public function set_up() { parent::set_up(); 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..52f38a473 --- /dev/null +++ b/tests/phpunit/integration/Controller/Test_Controller_Uninstaller.php @@ -0,0 +1,78 @@ +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 ); + } + +} diff --git a/tests/phpunit/unit-tests/Controller/Test_Controller_Upgrade_Routines.php b/tests/phpunit/integration/Controller/Test_Controller_Upgrade_Routines.php similarity index 91% rename from tests/phpunit/unit-tests/Controller/Test_Controller_Upgrade_Routines.php rename to tests/phpunit/integration/Controller/Test_Controller_Upgrade_Routines.php index d619a4e72..a9e57d266 100644 --- a/tests/phpunit/unit-tests/Controller/Test_Controller_Upgrade_Routines.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Upgrade_Routines.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 upgrade */ -class Test_Controller_Upgrade_Routines extends WP_UnitTestCase { +class Test_Controller_Upgrade_Routines extends TestCase { /** * @var \GFPDF\Helper\Helper_Options_Fields diff --git a/tests/phpunit/unit-tests/Controller/Test_Controller_Webhooks.php b/tests/phpunit/integration/Controller/Test_Controller_Webhooks.php similarity index 80% rename from tests/phpunit/unit-tests/Controller/Test_Controller_Webhooks.php rename to tests/phpunit/integration/Controller/Test_Controller_Webhooks.php index 9b6b15a23..6440e3461 100644 --- a/tests/phpunit/unit-tests/Controller/Test_Controller_Webhooks.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Webhooks.php @@ -2,7 +2,7 @@ namespace GFPDF\Controller; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -18,14 +18,19 @@ * @group controller * @group webhook */ -class Test_Controller_Webhooks extends WP_UnitTestCase { +class Test_Controller_Webhooks extends TestCase { + + public static function set_up_before_class() { + parent::set_up_before_class(); + static::load_fixtures( [ 'all-form-fields' ], [ 'all-form-fields' ] ); + } /** * Test we add the PDF URLs to the Webhook request data when the request type is "all_fields" */ public function test_webhook_request_data_all_fields() { $feed = [ 'meta' => [ 'requestBodyType' => 'all_fields' ] ]; - $entry = $GLOBALS['GFPDF_Test']->entries['all-form-fields'][0]; + $entry = $this->entry( 'all-form-fields' ); $request_data = $entry; $request_data = apply_filters( 'gform_webhooks_request_data', $request_data, $feed, $entry ); @@ -42,7 +47,7 @@ public function test_webhook_request_data_all_fields() { */ public function test_webhook_request_data_select_fields() { $feed = [ 'meta' => [ 'requestBodyType' => 'select_fields' ] ]; - $entry = $GLOBALS['GFPDF_Test']->entries['all-form-fields'][0]; + $entry = $this->entry( 'all-form-fields' ); $request_data = $entry; $request_data = apply_filters( 'gform_webhooks_request_data', $request_data, $feed, $entry ); diff --git a/tests/phpunit/unit-tests/Controller/Test_Controller_Zapier.php b/tests/phpunit/integration/Controller/Test_Controller_Zapier.php similarity index 90% rename from tests/phpunit/unit-tests/Controller/Test_Controller_Zapier.php rename to tests/phpunit/integration/Controller/Test_Controller_Zapier.php index 73163a7e2..0ee7a8447 100644 --- a/tests/phpunit/unit-tests/Controller/Test_Controller_Zapier.php +++ b/tests/phpunit/integration/Controller/Test_Controller_Zapier.php @@ -2,7 +2,7 @@ namespace GFPDF\Controller; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -18,13 +18,18 @@ * @group controller * @group zapier */ -class Test_Controller_Zapier extends WP_UnitTestCase { +class Test_Controller_Zapier extends TestCase { /** * @var Controller_Zapier */ protected $controller; + public static function set_up_before_class() { + parent::set_up_before_class(); + static::load_fixtures( [ 'all-form-fields' ], [ 'all-form-fields' ] ); + } + public function set_up() { parent::set_up(); @@ -32,7 +37,7 @@ public function set_up() { } public function test_add_zapier_support_active_pdfs() { - $entry = $GLOBALS['GFPDF_Test']->entries['all-form-fields'][0]; + $entry = $this->entry( 'all-form-fields' ); $body = $this->controller->add_zapier_support( [], [], $entry ); $this->assertCount( 8, $body ); @@ -59,7 +64,7 @@ public function test_add_zapier_support_active_pdfs() { } public function test_add_zapier_support_conditional_logic() { - $entry = $GLOBALS['GFPDF_Test']->entries['all-form-fields'][0]; + $entry = $this->entry( 'all-form-fields' ); $entry[7] = 'Albania'; \GFAPI::update_entry( $entry ); 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/Helper/Fields/Test_Field_Address.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Address.php new file mode 100644 index 000000000..24ebd2634 --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Address.php @@ -0,0 +1,125 @@ +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 = $this->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 = $this->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 = $this->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..86c32b4a3 --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Checkbox.php @@ -0,0 +1,104 @@ +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 = $this->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 = $this->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 = $this->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 = $this->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/unit-tests/Helper/Fields/Test_Field_Consent.php b/tests/phpunit/integration/Helper/Fields/Test_Field_Consent.php similarity index 90% rename from tests/phpunit/unit-tests/Helper/Fields/Test_Field_Consent.php rename to tests/phpunit/integration/Helper/Fields/Test_Field_Consent.php index 5e3765e85..74f7443be 100644 --- a/tests/phpunit/unit-tests/Helper/Fields/Test_Field_Consent.php +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Consent.php @@ -4,7 +4,7 @@ namespace GFPDF\Helper\Fields; -use WP_UnitTestCase; +use GFPDF\Tests\Integration\TestCase; /** * @package Gravity PDF @@ -16,7 +16,13 @@ * @group helper * @group fields */ -class Test_Field_Consent extends WP_UnitTestCase { +class Test_Field_Consent extends TestCase { + + public static function set_up_before_class() { + parent::set_up_before_class(); + static::load_fixtures( [ 'repeater-consent-form' ] ); + } + public $form; @@ -27,7 +33,7 @@ class Test_Field_Consent extends WP_UnitTestCase { public function set_up() { parent::set_up(); - $this->form = $GLOBALS['GFPDF_Test']->form['repeater-consent-form']; + $this->form = $this->form( 'repeater-consent-form' ); foreach ( $this->form['fields'] as $field ) { if ( $field->type === 'consent' ) { 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..b793ed9fe --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Date.php @@ -0,0 +1,79 @@ +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 = $this->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..cccc476be --- /dev/null +++ b/tests/phpunit/integration/Helper/Fields/Test_Field_Discount.php @@ -0,0 +1,79 @@ +form( 'non-group-products-form' ); + $form_id = $this->gf_factory()->form->create( [], $form_data ); + $entry_data = $this->entry( 'non-group-products-form' ); + $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( '