From e607382e11d562b490f59a61307fee012da558a8 Mon Sep 17 00:00:00 2001 From: magdev Date: Sun, 1 Mar 2026 00:08:34 +0100 Subject: [PATCH] Add PHPUnit test suite with 64 unit tests (v1.1.1) PHPUnit 11 + Brain\Monkey for WordPress function mocking. Tests cover BlockRenderer (28), WidgetRenderer (9), NavWalker (14), and TemplateController (12). Includes functional WP_HTML_Tag_Processor stub, CI test job between lint and build-release, prebuild hook gating npm build on passing tests, and release package exclusions for test files. Co-Authored-By: Claude Opus 4.6 --- .gitea/workflows/release.yml | 34 +- .gitignore | 3 + CHANGELOG.md | 10 + CLAUDE.md | 53 +- README.md | 39 +- composer.json | 9 + composer.lock | 2038 ++++++++++++++++- package.json | 2 + phpunit.xml.dist | 21 + style.css | 2 +- tests/Stubs/WpBlock.php | 10 + tests/Stubs/WpHtmlTagProcessor.php | 135 ++ tests/Stubs/WpWidget.php | 10 + tests/Unit/Block/BlockRendererTest.php | 323 +++ tests/Unit/Block/WidgetRendererTest.php | 173 ++ tests/Unit/Template/NavWalkerTest.php | 224 ++ .../Unit/Template/TemplateControllerTest.php | 145 ++ tests/bootstrap.php | 15 + 18 files changed, 3234 insertions(+), 12 deletions(-) create mode 100644 phpunit.xml.dist create mode 100644 tests/Stubs/WpBlock.php create mode 100644 tests/Stubs/WpHtmlTagProcessor.php create mode 100644 tests/Stubs/WpWidget.php create mode 100644 tests/Unit/Block/BlockRendererTest.php create mode 100644 tests/Unit/Block/WidgetRendererTest.php create mode 100644 tests/Unit/Template/NavWalkerTest.php create mode 100644 tests/Unit/Template/TemplateControllerTest.php create mode 100644 tests/bootstrap.php diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 1b0a256..7d7dba5 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -24,10 +24,31 @@ jobs: run: | find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*" -print0 | xargs -0 -n1 php -l + test: + name: PHPUnit Tests + runs-on: ubuntu-latest + needs: [lint] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring, xml, dom + tools: composer:v2 + + - name: Install Composer dependencies + run: composer install --no-interaction + + - name: Run PHPUnit + run: composer exec -- phpunit + build-release: name: Build Release runs-on: ubuntu-latest - needs: [lint] + needs: [test] steps: - name: Checkout code uses: actions/checkout@v4 @@ -120,6 +141,9 @@ jobs: -x "${THEME_NAME}/*.log" \ -x "${THEME_NAME}/*.po~" \ -x "${THEME_NAME}/*.bak" \ + -x "${THEME_NAME}/tests/*" \ + -x "${THEME_NAME}/phpunit.xml.dist" \ + -x "${THEME_NAME}/.phpunit.cache/*" \ -x "${THEME_NAME}/views/.gitkeep" \ -x "${THEME_NAME}/assets/images/.gitkeep" \ -x "*.DS_Store" @@ -187,6 +211,14 @@ jobs: echo "src/ excluded: OK" fi + # Verify tests excluded + if unzip -l "releases/${THEME_NAME}-${VERSION}.zip" | grep -q "${THEME_NAME}/tests/"; then + echo "WARNING: tests/ directory should be excluded" + exit 1 + else + echo "tests/ excluded: OK" + fi + - name: Extract changelog for release notes id: changelog run: | diff --git a/.gitignore b/.gitignore index 050a9c5..88e3d4f 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,8 @@ npm-debug.log # Claude local settings .claude/settings.local.json +# PHPUnit cache +.phpunit.cache/ + # Build artifacts (releases directory) releases/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0217cbc..6852221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## [1.1.1] - 2026-02-28 + +### Added + +- **PHPUnit test suite**: 64 unit tests covering `BlockRenderer`, `WidgetRenderer`, `NavWalker`, and `TemplateController` classes with 107 assertions. Uses PHPUnit 11 and Brain\Monkey for WordPress function mocking. +- **Test infrastructure**: `WP_HTML_Tag_Processor` functional stub using DOMDocument for testing block renderer HTML manipulation outside WordPress. Empty stubs for `WP_Block` and `WP_Widget` type hints. +- **Build pipeline integration**: Tests run automatically before every `npm run build` via `prebuild` hook (`composer exec -- phpunit`). +- **CI test job**: New PHPUnit test step in Gitea CI workflow between lint and build-release. Tests must pass before release packages are built. +- **Release package exclusions**: `tests/`, `phpunit.xml.dist`, and `.phpunit.cache/` excluded from release ZIP packages with verification step. + ## [1.1.0] - 2026-02-28 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index ea466b6..82bd93f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w **Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file. -Current version is **v1.1.0**. See `PLAN.md` for details. +Current version is **v1.1.1**. See `PLAN.md` for details. ## Technical Stack @@ -234,6 +234,57 @@ Build steps (in order): ## Session History +### Session 20 — v1.1.1 PHPUnit Test Suite (2026-02-28) + +**Completed:** PHPUnit test suite with 64 unit tests and 107 assertions, CI integration, and build pipeline gating. + +**What was built:** + +- **PHPUnit 11 + Brain\Monkey 2.7**: Added as `require-dev` with PSR-4 `autoload-dev` mapping (`WPBootstrap\Tests\` → `tests/`). +- **WP_HTML_Tag_Processor stub** (`tests/Stubs/WpHtmlTagProcessor.php`): Functional DOMDocument-based replacement supporting `next_tag()` (by tag name or class_name), `add_class()` (idempotent), and `get_updated_html()`. Uses full HTML document wrapping for reliable body extraction. +- **BlockRendererTest** (28 tests): All 8 render methods — table classes, striped tables, button variants (default/bg/outline/gradient/unknown), button group flex, img-fluid, search input-group, blockquote+cite, pullquote, list-group, empty content returns. +- **WidgetRendererTest** (9 tests): Card structure, title heading, widget ID, type class extraction, generic filtering, h2→h4 regex, multiple h2 elements, empty content. +- **NavWalkerTest** (14 tests): Tree building (empty, single, flat, nested, multi-parent, orphans), node structure, classes, index reset, active detection (current-menu-item, ancestor, is_page, is_category, inactive). +- **TemplateControllerTest** (12 tests): Template resolution via ReflectionMethod for all page types (404, search, post default/full-width, page default/landing/full-width/hero/sidebar, archive, home, fallback). +- **Build pipeline**: `npm run test` and `prebuild` hook gate `npm run build` on passing tests. +- **CI workflow**: New `test` job between `lint` and `build-release` with PHP 8.3 + Composer. +- **Release exclusions**: `tests/`, `phpunit.xml.dist`, `.phpunit.cache/*` excluded from ZIP with verification step. + +**Architecture decisions:** + +- **Brain\Monkey over full WordPress bootstrap**: WordPress function mocking via `Functions\when()->justReturn()` and `Functions\when()->alias()` enables fast, isolated unit tests without a running WordPress installation. +- **DOMDocument stub over regex**: `WP_HTML_Tag_Processor` stub uses `DOMDocument`/`DOMXPath` for reliable HTML parsing. Full document wrapping (`...`) required because `LIBXML_HTML_NOIMPLIED` prevents `` creation in PHP 8.4, breaking `getElementsByTagName('body')`. +- **`is_admin()=true` constructor bypass**: BlockRenderer/WidgetRenderer constructors register WordPress filters. Mocking `is_admin()` to return true causes early exit, enabling direct method testing. +- **ReflectionMethod for private methods**: `TemplateController::resolveTemplate()` is private. Testing via reflection avoids refactoring production code for testability. +- **ContextBuilder and TwigService skipped**: Too many WordPress dependencies for practical unit testing — better suited for integration tests. + +**Key findings:** + +- `LIBXML_HTML_NOIMPLIED` with `DOMDocument::loadHTML()` on PHP 8.4 does not create a `` element, causing `getElementsByTagName('body')->item(0)` to return null. Solution: wrap input in full HTML document structure. +- Brain\Monkey's `Functions\when()->alias()` supports argument-dependent returns for functions like `is_singular()` that behave differently based on post type argument. +- `spl_object_id()` used in the stub's visited-node tracking enables sequential `next_tag()` advancement matching WordPress's forward-only API. + +**Files created:** + +- `phpunit.xml.dist` — PHPUnit configuration +- `tests/bootstrap.php` — Autoloader + stub loading +- `tests/Stubs/WpHtmlTagProcessor.php` — Functional DOMDocument-based stub +- `tests/Stubs/WpBlock.php` — Empty class stub +- `tests/Stubs/WpWidget.php` — Empty class stub +- `tests/Unit/Block/BlockRendererTest.php` — 28 tests +- `tests/Unit/Block/WidgetRendererTest.php` — 9 tests +- `tests/Unit/Template/NavWalkerTest.php` — 14 tests +- `tests/Unit/Template/TemplateControllerTest.php` — 12 tests + +**Files modified:** + +- `composer.json` — `require-dev`, `autoload-dev` +- `package.json` — `test`, `prebuild` scripts +- `.gitea/workflows/release.yml` — test job, exclusions, verification +- `.gitignore` — `.phpunit.cache/` +- `style.css` — version bump to 1.1.1 +- `CHANGELOG.md`, `README.md`, `CLAUDE.md` — documentation + ### Session 19 — v1.1.0 Block Renderer, Widget Renderer & Sidebar Post Layout (2026-02-28) **Completed:** Bootstrap 5 class injection for core blocks, sidebar widget card wrappers, widget SCSS styling, and sidebar-default post template. diff --git a/README.md b/README.md index aa61c30..4e45399 100644 --- a/README.md +++ b/README.md @@ -62,20 +62,41 @@ Activate the theme in **Appearance > Themes** in the WordPress admin. | Command | Description | | --- | --- | -| `npm run build` | Full production build (copy JS, compile SCSS, minify CSS) | +| `npm run build` | Full production build (runs tests, copies JS, compiles SCSS, minifies CSS) | +| `npm run test` | Run PHPUnit test suite | | `npm run dev` | Watch SCSS files and recompile on changes | | `npm run scss` | Compile SCSS only | | `npm run postcss` | Minify CSS with Autoprefixer and cssnano | | `composer install` | Install PHP dependencies (Twig) | +### Testing + +The theme includes a PHPUnit test suite with 64 unit tests and 107 assertions covering the core PHP classes: + +- **BlockRenderer** -- All 8 render methods (table, button, buttons, image, search, quote, pullquote, list) +- **WidgetRenderer** -- Card wrapping, heading downgrade, class extraction +- **NavWalker** -- Tree building, active item detection, orphan handling +- **TemplateController** -- Template resolution for all page types + +Tests use [Brain\Monkey](https://brain-wp.github.io/BrainMonkey/) for WordPress function mocking and a functional `WP_HTML_Tag_Processor` stub. + +```bash +# Run tests +composer exec -- phpunit + +# Tests also run automatically before every build +npm run build +``` + ### Build Pipeline -1. `copy:js` -- Copy Bootstrap JS bundle from `node_modules` to `assets/js/` -2. `copy:theme-js` -- Copy theme JS (dark-mode.js) from `src/js/` to `assets/js/` -3. `copy:icons` -- Copy Bootstrap Icons font files (`.woff`, `.woff2`) to `assets/fonts/` -4. `scss` -- Compile SCSS (`src/scss/`) to CSS (`assets/css/`) -5. `scss:rtl` -- Compile RTL stylesheet (`assets/css/rtl.css`) -6. `postcss` -- Autoprefixer + cssnano minification to `assets/css/style.min.css` +1. `test` -- Run PHPUnit test suite (automatic via `prebuild` hook) +2. `copy:js` -- Copy Bootstrap JS bundle from `node_modules` to `assets/js/` +3. `copy:theme-js` -- Copy theme JS (dark-mode.js) from `src/js/` to `assets/js/` +4. `copy:icons` -- Copy Bootstrap Icons font files (`.woff`, `.woff2`) to `assets/fonts/` +5. `scss` -- Compile SCSS (`src/scss/`) to CSS (`assets/css/`) +6. `scss:rtl` -- Compile RTL stylesheet (`assets/css/rtl.css`) +7. `postcss` -- Autoprefixer + cssnano minification to `assets/css/style.min.css` ## Architecture @@ -140,6 +161,9 @@ wp-bootstrap/ | +-- js/ Source JavaScript | +-- scss/ Source SCSS +-- styles/ Style variations (JSON) ++-- tests/ +| +-- Stubs/ WordPress class stubs for testing +| +-- Unit/ PHPUnit test cases +-- templates/ FSE templates (HTML) +-- views/ Twig templates (Bootstrap 5 HTML) | +-- base.html.twig @@ -158,6 +182,7 @@ wp-bootstrap/ - **Bootstrap 5.3+** CSS & JS (served locally) - **Dart Sass** for SCSS compilation - **PostCSS** with Autoprefixer and cssnano +- **PHPUnit 11** with Brain\Monkey for WordPress function mocking ## License diff --git a/composer.json b/composer.json index a24cbbb..85f84eb 100644 --- a/composer.json +++ b/composer.json @@ -14,11 +14,20 @@ "php": ">=8.3", "twig/twig": "^3.0" }, + "require-dev": { + "brain/monkey": "^2.6", + "phpunit/phpunit": "^11.0" + }, "autoload": { "psr-4": { "WPBootstrap\\": "inc/" } }, + "autoload-dev": { + "psr-4": { + "WPBootstrap\\Tests\\": "tests/" + } + }, "config": { "optimize-autoloader": true, "sort-packages": true diff --git a/composer.lock b/composer.lock index dff6ea5..a820012 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7f711ad3aa03001938265eb3bd2ad61c", + "content-hash": "68367269c6c895098182a77ad56e2bf3", "packages": [ { "name": "symfony/deprecation-contracts", @@ -321,7 +321,2041 @@ "time": "2026-01-23T21:00:41+00:00" } ], - "packages-dev": [], + "packages-dev": [ + { + "name": "antecedent/patchwork", + "version": "2.2.3", + "source": { + "type": "git", + "url": "https://github.com/antecedent/patchwork.git", + "reference": "8b6b235f405af175259c8f56aea5fc23ab9f03ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/8b6b235f405af175259c8f56aea5fc23ab9f03ce", + "reference": "8b6b235f405af175259c8f56aea5fc23ab9f03ce", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignas Rudaitis", + "email": "ignas.rudaitis@gmail.com" + } + ], + "description": "Method redefinition (monkey-patching) functionality for PHP.", + "homepage": "https://antecedent.github.io/patchwork/", + "keywords": [ + "aop", + "aspect", + "interception", + "monkeypatching", + "redefinition", + "runkit", + "testing" + ], + "support": { + "issues": "https://github.com/antecedent/patchwork/issues", + "source": "https://github.com/antecedent/patchwork/tree/2.2.3" + }, + "time": "2025-09-17T09:00:56+00:00" + }, + { + "name": "brain/monkey", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/Brain-WP/BrainMonkey.git", + "reference": "ea3aeb3d559ba3c0930b3f4d210b665a4c044d83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Brain-WP/BrainMonkey/zipball/ea3aeb3d559ba3c0930b3f4d210b665a4c044d83", + "reference": "ea3aeb3d559ba3c0930b3f4d210b665a4c044d83", + "shasum": "" + }, + "require": { + "antecedent/patchwork": "^2.1.17", + "mockery/mockery": "~1.3.6 || ~1.4.4 || ~1.5.1 || ^1.6.10", + "php": ">=5.6.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "phpcompatibility/php-compatibility": "^9.3.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.49 || ^9.6.30" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev", + "dev-version/1": "1.x-dev" + } + }, + "autoload": { + "files": [ + "inc/api.php" + ], + "psr-4": { + "Brain\\Monkey\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Giuseppe Mazzapica", + "email": "giuseppe.mazzapica@gmail.com", + "homepage": "https://gmazzap.me", + "role": "Developer" + } + ], + "description": "Mocking utility for PHP functions and WordPress plugin API", + "keywords": [ + "Monkey Patching", + "interception", + "mock", + "mock functions", + "mockery", + "patchwork", + "redefinition", + "runkit", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/Brain-WP/BrainMonkey/issues", + "source": "https://github.com/Brain-WP/BrainMonkey" + }, + "time": "2026-02-05T09:22:14+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": {}, diff --git a/package.json b/package.json index 8315b58..06a797f 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "copy:js": "copyfiles -f node_modules/bootstrap/dist/js/bootstrap.bundle.min.js node_modules/bootstrap/dist/js/bootstrap.bundle.min.js.map assets/js/", "copy:theme-js": "copyfiles -f src/js/dark-mode.js assets/js/", "copy:icons": "copyfiles -f node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2 assets/fonts/", + "test": "composer exec -- phpunit", + "prebuild": "npm run test", "build": "npm run copy:js && npm run copy:theme-js && npm run copy:icons && npm run scss && npm run scss:rtl && npm run postcss", "watch": "npm run copy:js && npm run scss:watch", "dev": "npm run watch" diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..a470caa --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + + tests/Unit + + + + + + inc + + + diff --git a/style.css b/style.css index 1a28921..8dd1cfb 100644 --- a/style.css +++ b/style.css @@ -7,7 +7,7 @@ Description: A modern WordPress Block Theme built from scratch with Bootstrap 5. Requires at least: 6.7 Tested up to: 6.7 Requires PHP: 8.3 -Version: 1.1.0 +Version: 1.1.1 License: GNU General Public License v2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html Text Domain: wp-bootstrap diff --git a/tests/Stubs/WpBlock.php b/tests/Stubs/WpBlock.php new file mode 100644 index 0000000..292dcd9 --- /dev/null +++ b/tests/Stubs/WpBlock.php @@ -0,0 +1,10 @@ +html = $html; + $this->doc = new \DOMDocument(); + $this->doc->encoding = 'UTF-8'; + + // Wrap in a full HTML document so is always present. + // This ensures get_updated_html() can reliably extract content. + @$this->doc->loadHTML( + '' + . $html + . '', + LIBXML_HTML_NODEFDTD + ); + + $this->xpath = new \DOMXPath($this->doc); + } + + /** + * Advance to the next matching tag. + * + * @param string|array|null $query Tag name (string) or ['class_name' => '...'] (array). + */ + public function next_tag($query = null): bool + { + $tagName = null; + $className = null; + + if (is_string($query)) { + $tagName = strtolower($query); + } elseif (is_array($query)) { + $tagName = isset($query['tag_name']) ? strtolower($query['tag_name']) : null; + $className = $query['class_name'] ?? null; + } + + // Build XPath — search within only. + $body = $this->doc->getElementsByTagName('body')->item(0); + if (!$body) { + return false; + } + + $xpathExpr = './/*'; + $conditions = []; + + if ($tagName !== null) { + $conditions[] = sprintf( + "translate(local-name(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = '%s'", + $tagName + ); + } + + if ($className !== null) { + $conditions[] = sprintf( + "contains(concat(' ', normalize-space(@class), ' '), ' %s ')", + $className + ); + } + + if ($conditions) { + $xpathExpr .= '[' . implode(' and ', $conditions) . ']'; + } + + $nodes = $this->xpath->query($xpathExpr, $body); + + foreach ($nodes as $node) { + $oid = spl_object_id($node); + if (!in_array($oid, $this->visited, true)) { + $this->current = $node; + $this->visited[] = $oid; + return true; + } + } + + $this->current = null; + return false; + } + + /** + * Add a CSS class to the current tag (idempotent). + */ + public function add_class(string $className): bool + { + if ($this->current === null) { + return false; + } + + $existing = $this->current->getAttribute('class'); + $classes = $existing ? array_filter(explode(' ', $existing)) : []; + + if (!in_array($className, $classes, true)) { + $classes[] = $className; + } + + $this->current->setAttribute('class', implode(' ', $classes)); + return true; + } + + /** + * Return the modified HTML. + */ + public function get_updated_html(): string + { + $body = $this->doc->getElementsByTagName('body')->item(0); + if (!$body) { + return $this->html; + } + + $html = ''; + foreach ($body->childNodes as $child) { + $html .= $this->doc->saveHTML($child); + } + + return $html; + } +} diff --git a/tests/Stubs/WpWidget.php b/tests/Stubs/WpWidget.php new file mode 100644 index 0000000..79c6910 --- /dev/null +++ b/tests/Stubs/WpWidget.php @@ -0,0 +1,10 @@ +justReturn(true); + Functions\when('wp_doing_ajax')->justReturn(false); + + $this->renderer = new BlockRenderer(); + } + + protected function tearDown(): void + { + Monkey\tearDown(); + parent::tearDown(); + } + + // ── core/table ────────────────────────────────────────────── + + public function testRenderTableAddsTableClass(): void + { + $html = '
A
1
'; + $block = ['attrs' => []]; + $result = $this->renderer->renderTable($html, $block); + + $this->assertStringContainsString('table', $this->classesOf('table', $result)); + } + + public function testRenderTableWithStripesAddsStripedClass(): void + { + $html = '
1
'; + $block = ['attrs' => ['className' => 'is-style-stripes']]; + $result = $this->renderer->renderTable($html, $block); + + $classes = $this->classesOf('table', $result); + $this->assertStringContainsString('table', $classes); + $this->assertStringContainsString('table-striped', $classes); + } + + public function testRenderTableEmptyContentReturnsEmpty(): void + { + $this->assertSame('', $this->renderer->renderTable('', [])); + } + + public function testRenderTableWithoutTableTagReturnsOriginal(): void + { + $html = '

No table here

'; + $this->assertSame($html, $this->renderer->renderTable($html, [])); + } + + // ── core/button ───────────────────────────────────────────── + + public function testRenderButtonAddsBtnPrimaryByDefault(): void + { + $html = ''; + $block = ['attrs' => []]; + $result = $this->renderer->renderButton($html, $block); + + $classes = $this->classesOf('a', $result); + $this->assertStringContainsString('btn', $classes); + $this->assertStringContainsString('btn-primary', $classes); + } + + public function testRenderButtonWithBackgroundColor(): void + { + $html = ''; + $block = ['attrs' => ['backgroundColor' => 'danger']]; + $result = $this->renderer->renderButton($html, $block); + + $classes = $this->classesOf('a', $result); + $this->assertStringContainsString('btn', $classes); + $this->assertStringContainsString('btn-danger', $classes); + } + + public function testRenderButtonOutlineStyle(): void + { + $html = ''; + $block = ['attrs' => ['className' => 'is-style-outline', 'textColor' => 'success']]; + $result = $this->renderer->renderButton($html, $block); + + $classes = $this->classesOf('a', $result); + $this->assertStringContainsString('btn', $classes); + $this->assertStringContainsString('btn-outline-success', $classes); + } + + public function testRenderButtonOutlineDefaultsToPrimary(): void + { + $html = ''; + $block = ['attrs' => ['className' => 'is-style-outline']]; + $result = $this->renderer->renderButton($html, $block); + + $this->assertStringContainsString('btn-outline-primary', $this->classesOf('a', $result)); + } + + public function testRenderButtonWithGradientOnlyAddsBtnBase(): void + { + $html = ''; + $block = ['attrs' => ['gradient' => 'vivid-cyan-blue']]; + $result = $this->renderer->renderButton($html, $block); + + $classes = $this->classesOf('a', $result); + $this->assertStringContainsString('btn', $classes); + $this->assertStringNotContainsString('btn-primary', $classes); + } + + public function testRenderButtonWithUnknownColorDefaultsToPrimary(): void + { + $html = ''; + $block = ['attrs' => ['backgroundColor' => 'custom-teal']]; + $result = $this->renderer->renderButton($html, $block); + + $this->assertStringContainsString('btn-primary', $this->classesOf('a', $result)); + } + + public function testRenderButtonEmptyContentReturnsEmpty(): void + { + $this->assertSame('', $this->renderer->renderButton('', [])); + } + + // ── core/buttons ──────────────────────────────────────────── + + public function testRenderButtonsAddsFlexClasses(): void + { + $html = ''; + $block = ['attrs' => []]; + $result = $this->renderer->renderButtons($html, $block); + + $classes = $this->classesOfFirst('.wp-block-buttons', $result); + $this->assertStringContainsString('d-flex', $classes); + $this->assertStringContainsString('flex-wrap', $classes); + $this->assertStringContainsString('gap-2', $classes); + } + + public function testRenderButtonsEmptyContentReturnsEmpty(): void + { + $this->assertSame('', $this->renderer->renderButtons('', [])); + } + + // ── core/image ────────────────────────────────────────────── + + public function testRenderImageAddsImgFluid(): void + { + $html = '
Photo
'; + $block = ['attrs' => []]; + $result = $this->renderer->renderImage($html, $block); + + $this->assertStringContainsString('img-fluid', $this->classesOf('img', $result)); + } + + public function testRenderImageEmptyContentReturnsEmpty(): void + { + $this->assertSame('', $this->renderer->renderImage('', [])); + } + + // ── core/search ───────────────────────────────────────────── + + public function testRenderSearchAddsBootstrapClasses(): void + { + $html = ''; + + $block = ['attrs' => []]; + $result = $this->renderer->renderSearch($html, $block); + + $this->assertStringContainsString('input-group', $result); + $this->assertStringContainsString('form-control', $result); + $this->assertStringContainsString('btn-primary', $result); + } + + public function testRenderSearchEmptyContentReturnsEmpty(): void + { + $this->assertSame('', $this->renderer->renderSearch('', [])); + } + + // ── core/quote ────────────────────────────────────────────── + + public function testRenderQuoteAddsBlockquoteClass(): void + { + $html = '

Quote text

'; + $block = ['attrs' => []]; + $result = $this->renderer->renderQuote($html, $block); + + $this->assertStringContainsString('blockquote', $this->classesOf('blockquote', $result)); + } + + public function testRenderQuoteWithCiteAddsFooterClass(): void + { + $html = '

Quote

Author
'; + $block = ['attrs' => []]; + $result = $this->renderer->renderQuote($html, $block); + + $this->assertStringContainsString('blockquote-footer', $result); + } + + public function testRenderQuoteWithoutCite(): void + { + $html = '

Quote only

'; + $block = ['attrs' => []]; + $result = $this->renderer->renderQuote($html, $block); + + $this->assertStringContainsString('blockquote', $this->classesOf('blockquote', $result)); + $this->assertStringNotContainsString('blockquote-footer', $result); + } + + public function testRenderQuoteEmptyContentReturnsEmpty(): void + { + $this->assertSame('', $this->renderer->renderQuote('', [])); + } + + // ── core/pullquote ────────────────────────────────────────── + + public function testRenderPullquoteAddsBlockquoteClass(): void + { + $html = '

Pull

'; + $block = ['attrs' => []]; + $result = $this->renderer->renderPullquote($html, $block); + + $this->assertStringContainsString('blockquote', $this->classesOf('blockquote', $result)); + } + + public function testRenderPullquoteWithCiteAddsFooterClass(): void + { + $html = '

Pull

Source
'; + $block = ['attrs' => []]; + $result = $this->renderer->renderPullquote($html, $block); + + $this->assertStringContainsString('blockquote-footer', $result); + } + + public function testRenderPullquoteEmptyContentReturnsEmpty(): void + { + $this->assertSame('', $this->renderer->renderPullquote('', [])); + } + + // ── core/list ─────────────────────────────────────────────── + + public function testRenderListGroupAddsClasses(): void + { + $html = ''; + $block = ['attrs' => ['className' => 'is-style-list-group']]; + $result = $this->renderer->renderList($html, $block); + + $this->assertStringContainsString('list-group', $this->classesOf('ul', $result)); + $this->assertStringContainsString('list-group-item', $result); + } + + public function testRenderListGroupOrderedList(): void + { + $html = '
  1. 1
  2. 2
'; + $block = ['attrs' => ['className' => 'is-style-list-group', 'ordered' => true]]; + $result = $this->renderer->renderList($html, $block); + + $this->assertStringContainsString('list-group', $this->classesOf('ol', $result)); + } + + public function testRenderListWithoutGroupStyleReturnsUnchanged(): void + { + $html = ''; + $block = ['attrs' => []]; + $result = $this->renderer->renderList($html, $block); + + $this->assertStringNotContainsString('list-group', $result); + } + + public function testRenderListEmptyContentReturnsEmpty(): void + { + $this->assertSame('', $this->renderer->renderList('', [])); + } + + // ── helpers ───────────────────────────────────────────────── + + /** + * Extract the class attribute value of the first matching tag. + */ + private function classesOf(string $tagName, string $html): string + { + $doc = new \DOMDocument(); + @$doc->loadHTML('' . $html . '', LIBXML_HTML_NODEFDTD); + $elements = $doc->getElementsByTagName($tagName); + + return $elements->length > 0 ? ($elements->item(0)->getAttribute('class') ?? '') : ''; + } + + /** + * Extract classes from the first element matching a CSS selector-style class. + */ + private function classesOfFirst(string $selector, string $html): string + { + $className = ltrim($selector, '.'); + $doc = new \DOMDocument(); + @$doc->loadHTML('' . $html . '', LIBXML_HTML_NODEFDTD); + $xpath = new \DOMXPath($doc); + $body = $doc->getElementsByTagName('body')->item(0); + if (!$body) { + return ''; + } + $nodes = $xpath->query(sprintf( + ".//*[contains(concat(' ', normalize-space(@class), ' '), ' %s ')]", + $className + ), $body); + + return $nodes->length > 0 ? ($nodes->item(0)->getAttribute('class') ?? '') : ''; + } +} diff --git a/tests/Unit/Block/WidgetRendererTest.php b/tests/Unit/Block/WidgetRendererTest.php new file mode 100644 index 0000000..4b95789 --- /dev/null +++ b/tests/Unit/Block/WidgetRendererTest.php @@ -0,0 +1,173 @@ +justReturn(true); + Functions\when('wp_doing_ajax')->justReturn(false); + + $this->renderer = new WidgetRenderer(); + } + + protected function tearDown(): void + { + Monkey\tearDown(); + parent::tearDown(); + } + + // ── wrapWidgetInCard ──────────────────────────────────────── + + public function testWrapWidgetInCardSetsCardStructure(): void + { + Functions\when('esc_attr')->returnArg(); + + $params = $this->makeParams('block-2', 'widget mb-4 widget_block'); + $result = $this->renderer->wrapWidgetInCard($params); + + $this->assertStringContainsString('card', $result[0]['before_widget']); + $this->assertStringContainsString('card-body', $result[0]['before_widget']); + $this->assertSame('', $result[0]['after_widget']); + } + + public function testWrapWidgetInCardSetsCardTitle(): void + { + Functions\when('esc_attr')->returnArg(); + + $params = $this->makeParams('block-3', 'widget mb-4 widget_block'); + $result = $this->renderer->wrapWidgetInCard($params); + + $this->assertStringContainsString('assertStringContainsString('card-title', $result[0]['before_title']); + $this->assertSame('', $result[0]['after_title']); + } + + public function testWrapWidgetInCardPreservesWidgetId(): void + { + Functions\when('esc_attr')->returnArg(); + + $params = $this->makeParams('search-1', 'widget mb-4 widget_search'); + $result = $this->renderer->wrapWidgetInCard($params); + + $this->assertStringContainsString('id="search-1"', $result[0]['before_widget']); + } + + public function testWrapWidgetInCardPreservesWidgetTypeClasses(): void + { + Functions\when('esc_attr')->returnArg(); + + $params = $this->makeParams('block-5', 'widget mb-4 widget_block wp-block-heading'); + $result = $this->renderer->wrapWidgetInCard($params); + + // widget_block and wp-block-heading are kept; widget and mb-4 are removed. + $this->assertStringContainsString('widget_block', $result[0]['before_widget']); + $this->assertStringContainsString('wp-block-heading', $result[0]['before_widget']); + } + + public function testWrapWidgetInCardFiltersGenericClasses(): void + { + Functions\when('esc_attr')->returnArg(); + + $params = $this->makeParams('block-2', 'widget mb-4 widget_block'); + $result = $this->renderer->wrapWidgetInCard($params); + + // The card wrapper adds its own "widget" class, but the extracted + // classes should not include the generic "widget" or "mb-4". + // Count occurrences: "card mb-3 widget " + "widget_block" should have exactly one "widget". + preg_match_all('/\bwidget\b/', $result[0]['before_widget'], $matches); + $this->assertCount(1, $matches[0], 'Generic "widget" class should appear once (from card wrapper), not duplicated'); + } + + public function testWrapWidgetInCardWithNoExistingClasses(): void + { + Functions\when('esc_attr')->returnArg(); + + $params = [ + [ + 'widget_id' => 'text-1', + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ], + ]; + + $result = $this->renderer->wrapWidgetInCard($params); + + $this->assertStringContainsString('card', $result[0]['before_widget']); + $this->assertStringContainsString('id="text-1"', $result[0]['before_widget']); + } + + // ── processBlockWidgetContent ─────────────────────────────── + + public function testProcessBlockWidgetContentReplacesH2WithH4(): void + { + $content = '

Categories

  • Cat A
'; + $widget = new \WP_Widget(); + + $result = $this->renderer->processBlockWidgetContent($content, [], $widget); + + $this->assertStringContainsString('

', $result); + $this->assertStringContainsString('

', $result); + $this->assertStringNotContainsString('assertStringNotContainsString('', $result); + } + + public function testProcessBlockWidgetContentPreservesOtherH2(): void + { + $content = '

Keep me

'; + $widget = new \WP_Widget(); + + $result = $this->renderer->processBlockWidgetContent($content, [], $widget); + + // h2 without wp-block-heading class should remain. + $this->assertStringContainsString('

', $result); + } + + public function testProcessBlockWidgetContentEmptyReturnsEmpty(): void + { + $widget = new \WP_Widget(); + $this->assertSame('', $this->renderer->processBlockWidgetContent('', [], $widget)); + } + + public function testProcessBlockWidgetContentMultipleH2(): void + { + $content = '

First

Text

Second

'; + $widget = new \WP_Widget(); + + $result = $this->renderer->processBlockWidgetContent($content, [], $widget); + + $this->assertSame(0, substr_count($result, 'assertSame(2, substr_count($result, ' $widgetId, + 'before_widget' => sprintf('
', $widgetId, $classes), + 'after_widget' => '
', + 'before_title' => '', + ], + ]; + } +} diff --git a/tests/Unit/Template/NavWalkerTest.php b/tests/Unit/Template/NavWalkerTest.php new file mode 100644 index 0000000..852cbef --- /dev/null +++ b/tests/Unit/Template/NavWalkerTest.php @@ -0,0 +1,224 @@ +justReturn(false); + Functions\when('is_category')->justReturn(false); + + $this->walker = new NavWalker(); + } + + protected function tearDown(): void + { + Monkey\tearDown(); + parent::tearDown(); + } + + // ── Tree structure ────────────────────────────────────────── + + public function testBuildTreeWithEmptyArray(): void + { + $this->assertSame([], $this->walker->buildTree([])); + } + + public function testBuildTreeWithSingleItem(): void + { + $items = [$this->makeItem(10, 'Home', '/')]; + $tree = $this->walker->buildTree($items); + + $this->assertCount(1, $tree); + $this->assertSame('Home', $tree[0]['title']); + $this->assertSame('/', $tree[0]['url']); + } + + public function testBuildTreeFlatItemsAllTopLevel(): void + { + $items = [ + $this->makeItem(1, 'A', '/a'), + $this->makeItem(2, 'B', '/b'), + $this->makeItem(3, 'C', '/c'), + ]; + + $tree = $this->walker->buildTree($items); + + $this->assertCount(3, $tree); + foreach ($tree as $node) { + $this->assertEmpty($node['children']); + } + } + + public function testBuildTreeWithChildren(): void + { + $items = [ + $this->makeItem(10, 'Home', '/'), + $this->makeItem(20, 'About', '/about'), + $this->makeItem(30, 'Team', '/about/team', parent: 20), + $this->makeItem(40, 'Jobs', '/about/jobs', parent: 20), + ]; + + $tree = $this->walker->buildTree($items); + + $this->assertCount(2, $tree); + $this->assertSame('About', $tree[1]['title']); + $this->assertCount(2, $tree[1]['children']); + $this->assertSame('Team', $tree[1]['children'][0]['title']); + $this->assertSame('Jobs', $tree[1]['children'][1]['title']); + } + + public function testBuildTreeWithMultipleParents(): void + { + $items = [ + $this->makeItem(1, 'A', '/a'), + $this->makeItem(2, 'B', '/b'), + $this->makeItem(3, 'A1', '/a/1', parent: 1), + $this->makeItem(4, 'B1', '/b/1', parent: 2), + ]; + + $tree = $this->walker->buildTree($items); + + $this->assertCount(2, $tree); + $this->assertCount(1, $tree[0]['children']); + $this->assertCount(1, $tree[1]['children']); + $this->assertSame('A1', $tree[0]['children'][0]['title']); + $this->assertSame('B1', $tree[1]['children'][0]['title']); + } + + public function testBuildTreeOrphansAreDropped(): void + { + $items = [ + $this->makeItem(1, 'Root', '/'), + $this->makeItem(2, 'Orphan', '/orphan', parent: 999), + ]; + + $tree = $this->walker->buildTree($items); + + $this->assertCount(1, $tree); + $this->assertSame('Root', $tree[0]['title']); + } + + public function testBuildTreeNodeStructure(): void + { + $items = [$this->makeItem(42, 'Page', '/page', target: '_blank', classes: ['menu-item', 'custom'])]; + $tree = $this->walker->buildTree($items); + + $node = $tree[0]; + $this->assertSame(42, $node['id']); + $this->assertSame('Page', $node['title']); + $this->assertSame('/page', $node['url']); + $this->assertSame('_blank', $node['target']); + $this->assertSame('menu-item custom', $node['classes']); + $this->assertIsBool($node['active']); + $this->assertIsArray($node['children']); + } + + public function testBuildTreeClassesJoined(): void + { + $items = [$this->makeItem(1, 'X', '/x', classes: ['nav-item', '', 'menu-item'])]; + $tree = $this->walker->buildTree($items); + + // Empty strings are filtered out by array_filter. + $this->assertSame('nav-item menu-item', $tree[0]['classes']); + } + + public function testBuildTreeIndexIsReset(): void + { + $items = [ + $this->makeItem(50, 'A', '/a'), + $this->makeItem(100, 'B', '/b'), + ]; + + $tree = $this->walker->buildTree($items); + + // array_values resets keys to 0-indexed. + $this->assertArrayHasKey(0, $tree); + $this->assertArrayHasKey(1, $tree); + $this->assertArrayNotHasKey(50, $tree); + } + + // ── Active detection ──────────────────────────────────────── + + public function testBuildTreeSetsActiveViaCurrentMenuItem(): void + { + $items = [$this->makeItem(1, 'Active', '/active', classes: ['current-menu-item'])]; + $tree = $this->walker->buildTree($items); + + $this->assertTrue($tree[0]['active']); + } + + public function testBuildTreeSetsActiveViaAncestor(): void + { + $items = [$this->makeItem(1, 'Parent', '/parent', classes: ['current-menu-ancestor'])]; + $tree = $this->walker->buildTree($items); + + $this->assertTrue($tree[0]['active']); + } + + public function testBuildTreeSetsActiveViaIsPage(): void + { + Functions\when('is_page')->alias(fn(int $id): bool => $id === 42); + + $items = [$this->makeItem(1, 'Contact', '/contact', object: 'page', objectId: 42)]; + $tree = $this->walker->buildTree($items); + + $this->assertTrue($tree[0]['active']); + } + + public function testBuildTreeSetsActiveViaIsCategory(): void + { + Functions\when('is_category')->alias(fn(int $id): bool => $id === 7); + + $items = [$this->makeItem(1, 'News', '/news', object: 'category', objectId: 7)]; + $tree = $this->walker->buildTree($items); + + $this->assertTrue($tree[0]['active']); + } + + public function testBuildTreeNotActive(): void + { + $items = [$this->makeItem(1, 'Inactive', '/inactive')]; + $tree = $this->walker->buildTree($items); + + $this->assertFalse($tree[0]['active']); + } + + // ── Helper ────────────────────────────────────────────────── + + private function makeItem( + int $id, + string $title, + string $url, + int $parent = 0, + string $target = '', + array $classes = [], + string $object = 'page', + int $objectId = 0, + ): object { + return (object) [ + 'ID' => $id, + 'title' => $title, + 'url' => $url, + 'target' => $target, + 'classes' => $classes, + 'menu_item_parent' => $parent, + 'object' => $object, + 'object_id' => $objectId ?: $id, + ]; + } +} diff --git a/tests/Unit/Template/TemplateControllerTest.php b/tests/Unit/Template/TemplateControllerTest.php new file mode 100644 index 0000000..a442a29 --- /dev/null +++ b/tests/Unit/Template/TemplateControllerTest.php @@ -0,0 +1,145 @@ +justReturn(true); + Functions\when('wp_doing_ajax')->justReturn(false); + Functions\when('add_action')->justReturn(true); + + // Default: all WP conditionals return false. + Functions\when('is_404')->justReturn(false); + Functions\when('is_search')->justReturn(false); + Functions\when('is_singular')->justReturn(false); + Functions\when('is_page')->justReturn(false); + Functions\when('is_archive')->justReturn(false); + Functions\when('is_home')->justReturn(false); + Functions\when('is_category')->justReturn(false); + Functions\when('get_page_template_slug')->justReturn(''); + + $this->controller = new TemplateController(); + + $this->resolveTemplate = new ReflectionMethod($this->controller, 'resolveTemplate'); + $this->resolveTemplate->setAccessible(true); + } + + protected function tearDown(): void + { + Monkey\tearDown(); + parent::tearDown(); + } + + private function resolve(): ?string + { + return $this->resolveTemplate->invoke($this->controller); + } + + // ── Template resolution ───────────────────────────────────── + + public function testResolveTemplate404(): void + { + Functions\when('is_404')->justReturn(true); + + $this->assertSame('pages/404.html.twig', $this->resolve()); + } + + public function testResolveTemplateSearch(): void + { + Functions\when('is_search')->justReturn(true); + + $this->assertSame('pages/search.html.twig', $this->resolve()); + } + + public function testResolveTemplateSinglePostDefault(): void + { + Functions\when('is_singular')->alias(fn($type = '') => $type === 'post'); + + $this->assertSame('pages/single-sidebar.html.twig', $this->resolve()); + } + + public function testResolveTemplateSinglePostFullWidth(): void + { + Functions\when('is_singular')->alias(fn($type = '') => $type === 'post'); + Functions\when('get_page_template_slug')->justReturn('page-full-width'); + + $this->assertSame('pages/single.html.twig', $this->resolve()); + } + + public function testResolveTemplatePageDefault(): void + { + Functions\when('is_page')->justReturn(true); + + $this->assertSame('pages/page.html.twig', $this->resolve()); + } + + public function testResolveTemplatePageLanding(): void + { + Functions\when('is_page')->justReturn(true); + Functions\when('get_page_template_slug')->justReturn('page-landing'); + + $this->assertSame('pages/landing.html.twig', $this->resolve()); + } + + public function testResolveTemplatePageFullWidth(): void + { + Functions\when('is_page')->justReturn(true); + Functions\when('get_page_template_slug')->justReturn('page-full-width'); + + $this->assertSame('pages/full-width.html.twig', $this->resolve()); + } + + public function testResolveTemplatePageHero(): void + { + Functions\when('is_page')->justReturn(true); + Functions\when('get_page_template_slug')->justReturn('page-hero'); + + $this->assertSame('pages/hero.html.twig', $this->resolve()); + } + + public function testResolveTemplatePageSidebar(): void + { + Functions\when('is_page')->justReturn(true); + Functions\when('get_page_template_slug')->justReturn('page-sidebar'); + + $this->assertSame('pages/page-sidebar.html.twig', $this->resolve()); + } + + public function testResolveTemplateArchive(): void + { + Functions\when('is_archive')->justReturn(true); + + $this->assertSame('pages/archive.html.twig', $this->resolve()); + } + + public function testResolveTemplateHome(): void + { + Functions\when('is_home')->justReturn(true); + + $this->assertSame('pages/index.html.twig', $this->resolve()); + } + + public function testResolveTemplateFallback(): void + { + // All conditionals already return false in setUp. + $this->assertSame('pages/index.html.twig', $this->resolve()); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..c286e44 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,15 @@ +