You've already forked wc-bootstrap
Add test infrastructure for isolated unit testing without WordPress/WooCommerce: - 27 tests (54 assertions) covering TemplateOverride and WooCommerceExtension - Brain\Monkey for WordPress function mocking, class stubs for TwigService and WC_Product - PHPUnit test job added to Gitea CI pipeline between lint and build-release - Test artifacts excluded from release packages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
539 lines
37 KiB
Markdown
539 lines
37 KiB
Markdown
# WooCommerce Bootstrap
|
|
|
|
**Author:** Marco Grätsch
|
|
**Author URL:** <https://src.bundespruefstelle.ch/magdev>
|
|
**Author Email:** <magdev3.0@gmail.com>
|
|
**Repository URL:** <ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git>
|
|
**Issues URL:** <ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git/issues>
|
|
|
|
## Project Overview
|
|
|
|
This child theme of wp-bootstrap (`../wp-bootstrap/` or <https://src.bundespruefstelle.ch/magdev/wp-bootstrap>) extends wp-bootstrap and adds overrides for all theme files of the WooCommerce plugin (`../../plugins/woocommerce/` or <https://github.com/woocommerce/woocommerce.git>). All theme files from the plugin are converted to Bootstrap 5 structures and styles.
|
|
|
|
## Technical Stack
|
|
|
|
- **Language:** PHP 8.3.x
|
|
- **Framework:** Latest WordPress Theme API
|
|
- **Template Engine:** Twig 3.0 (via Composer)
|
|
- **WordPress Base Theme:** wp-bootstrap
|
|
- **Frontend:** Bootstrap 5 JavaScript & Vanilla JavaScript
|
|
- **Styling:** Bootstrap 5 & Custom CSS (if necessary)
|
|
- **Dependency Management:** Composer (PHP), npm (JS/CSS)
|
|
- **Internationalization:** WordPress i18n (.pot/.po/.mo files)
|
|
|
|
### Security Best Practices
|
|
|
|
- All user inputs are sanitized (integers for quantities/prices)
|
|
- Nonce verification on form submissions
|
|
- Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`)
|
|
- Direct file access prevention via `ABSPATH` check
|
|
- XSS-safe DOM construction in JavaScript (no `innerHTML` with user data)
|
|
- `json_encode|raw` in JS context is correct (not XSS) -- Twig auto-escaping would break JSON
|
|
- `styles|raw` in PDF exports is backend-generated static CSS, documented with comments
|
|
|
|
### Translation Ready
|
|
|
|
All user-facing strings use:
|
|
|
|
```php
|
|
__('Text to translate', 'wc-bootstrap')
|
|
_e('Text to translate', 'wc-bootstrap')
|
|
```
|
|
|
|
Text domain: `wc-bootstrap`
|
|
|
|
#### Text Domain Strategy
|
|
|
|
<!-- Choose ONE of these strategies based on your project needs: -->
|
|
|
|
**Option A -- Plugin domain (recommended when plugin provides Twig functions):**
|
|
All strings in theme templates use the plugin's `'woocommerce'` domain. This is necessary when the plugin registers `__()` / `_n()` as Twig functions with a default domain, because `load_child_theme_textdomain()` does not resolve at Twig render time.
|
|
|
|
**Rule:** Always use `'woocommerce'` as the text domain in all Twig templates and PHP files.
|
|
|
|
**Option B -- Theme domain (when theme handles its own Twig rendering):**
|
|
Theme templates use the theme's own `'wc-bootstrap'` domain. Ensure `load_child_theme_textdomain()` is called before Twig rendering.
|
|
|
|
#### Available Translations
|
|
|
|
<!-- List your supported locales here -->
|
|
|
|
- `en_US` -- English (base language, .pot template)
|
|
- `de_DE` -- German (Germany, formal)
|
|
- `de_CH` -- German (Switzerland, formal)
|
|
- `fr_FR` -- French (France)
|
|
|
|
Translation file naming convention: `wc-bootstrap-{locale}.po` (e.g., `wc-bootstrap-de_CH.po`)
|
|
|
|
Compiled `.mo` files are built by the CI/CD pipeline during releases. For local development:
|
|
|
|
```bash
|
|
for po in languages/wc-bootstrap-*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
|
```
|
|
|
|
#### Updating Translations (Fast JSON Workflow)
|
|
|
|
##### Step 1 -- Regenerate `.pot` files
|
|
|
|
wp-bootstrap (PHP sources only):
|
|
|
|
```bash
|
|
docker exec woocommerce-wordpress wp i18n make-pot \
|
|
/var/www/html/wp-content/themes/wp-bootstrap \
|
|
/var/www/html/wp-content/themes/wp-bootstrap/languages/wp-bootstrap.pot \
|
|
--allow-root
|
|
```
|
|
|
|
Child theme (PHP + Twig). Use a Twig extractor script that scans `.html.twig` files for `__()`, `_e()`, `_n()`, `_x()` calls, generates `twig-extracted.pot`, then merges with WP-CLI PHP extraction:
|
|
|
|
```bash
|
|
# Inside languages/ directory:
|
|
python3 twig-extractor.py # generates twig-extracted.pot
|
|
docker exec woocommerce-wordpress wp i18n make-pot \
|
|
/var/www/html/wp-content/themes/wc-bootstrap \
|
|
/var/www/html/wp-content/themes/wc-bootstrap/languages/php-extracted.pot \
|
|
--allow-root
|
|
msgcat --use-first php-extracted.pot twig-extracted.pot \
|
|
-o wc-bootstrap.pot
|
|
rm php-extracted.pot
|
|
```
|
|
|
|
##### Step 2 -- Merge new strings into all `.po` files
|
|
|
|
```bash
|
|
for locale in de_CH de_DE fr_FR; do
|
|
msgmerge --update --backup=none --no-fuzzy-matching \
|
|
wc-bootstrap-${locale}.po wc-bootstrap.pot
|
|
done
|
|
```
|
|
|
|
##### Step 3 -- Translate and apply
|
|
|
|
Extract untranslated strings per locale into JSON, translate (via AI or manual), then apply with `languages/patch-po.py`.
|
|
|
|
German variant derivation rules:
|
|
|
|
- `de_CH` from `de_DE`: replace `ss` where Swiss German uses `ss` instead of `ß`
|
|
- `de_DE_informal` from `de_DE`: `Sie` -> `du`, `Ihr` -> `dein`, etc.
|
|
- `de_CH_informal`: combine both transformations
|
|
|
|
##### Step 4 -- Validate and compile
|
|
|
|
```bash
|
|
for po in languages/wc-bootstrap-*.po; do msgfmt --statistics "$po" -o /dev/null; done
|
|
for po in languages/wc-bootstrap-*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
|
```
|
|
|
|
### Important Git Notes
|
|
|
|
- Default branch while development is `dev`
|
|
- Create releases from branch `main` after merging branch `dev`
|
|
- Tags should use format `vX.X.X` (e.g., `v1.1.22`), start with v0.1.0
|
|
- Use annotated tags (`-a`) not lightweight tags
|
|
- **ALWAYS push tags to origin** -- CI/CD triggers on tag push
|
|
- Commit messages should follow the established format with Claude Code attribution
|
|
- `.claude/settings.local.json` changes are typically local-only (stash before rebasing)
|
|
|
|
### CRITICAL -- Release Workflow
|
|
|
|
On every new version, ALWAYS execute this complete workflow:
|
|
|
|
```bash
|
|
# 1. Commit changes to dev branch
|
|
git add <files>
|
|
git commit -m "Description of changes (vX.X.X)"
|
|
|
|
# 2. Merge dev to main
|
|
git checkout main
|
|
git merge dev --no-edit
|
|
|
|
# 3. Create annotated tag
|
|
git tag -a vX.X.X -m "Version X.X.X - Brief description"
|
|
|
|
# 4. Push everything to origin
|
|
git push origin dev main vX.X.X
|
|
|
|
# 5. Switch back to dev for continued development
|
|
git checkout dev
|
|
```
|
|
|
|
Never skip any of these steps. The release is not complete until all branches and the tag are pushed to origin.
|
|
|
|
### CRITICAL -- Cross-Project Release Order
|
|
|
|
When a release touches both the plugin and the theme, ALWAYS release in this order:
|
|
|
|
1. **Theme first** -- full release workflow (commit -> merge -> tag -> push)
|
|
2. **Plugin second** -- update plugin's Dockerfile/config to reference the new theme version, then full release workflow
|
|
|
|
Reason: the plugin's build references the theme version. If the plugin is released first with a new theme version number, the CI build will attempt to download a theme release that does not yet exist and fail.
|
|
|
|
---
|
|
|
|
## For AI Assistants
|
|
|
|
When starting a new session on this project:
|
|
|
|
1. Read this CLAUDE.md file first
|
|
2. Semantic versioning follows the `MAJOR.MINOR.BUGFIX` pattern
|
|
3. Check git log for recent changes
|
|
4. Verify you're on the `dev` branch before making changes
|
|
5. Run `git submodule update --init --recursive` if lib/ is empty (only if submodules present)
|
|
6. Run `composer install` if vendor/ is missing
|
|
7. Test changes before committing
|
|
8. Follow commit message format with Claude Code attribution
|
|
9. Update this session history section with learnings
|
|
10. Never commit backup files (`*.po~`, `*.bak`, etc.) - check `git status` before committing
|
|
11. Follow markdown linting rules (see below)
|
|
|
|
Always refer to this document when starting work on this project.
|
|
|
|
### Markdown Linting Rules
|
|
|
|
1. **MD031**: Blank lines before and after fenced code blocks
|
|
2. **MD056**: Table separators must have matching column counts
|
|
3. **MD009**: No trailing spaces
|
|
4. **MD012**: No multiple consecutive blank lines
|
|
5. **MD040**: Fenced code blocks should have a language specified
|
|
6. **MD032**: Lists surrounded by blank lines
|
|
7. **MD034**: Bare URLs in angle brackets or markdown link syntax
|
|
8. **MD013**: Disabled (line length)
|
|
9. **MD024**: `siblings_only` mode
|
|
|
|
## Known Pitfalls & Key Learnings
|
|
|
|
Recurring bugs and non-obvious behaviours discovered across sessions. **Read this before starting any task.**
|
|
|
|
### Template Override Mechanism
|
|
|
|
- Plugin's `Template` class uses Twig `FilesystemLoader`. The `inc/TemplateOverride.php` hooks `init` at priority 20 (after plugin init at 0) and uses `prependPath()` to give child theme templates priority.
|
|
- Plugin templates in `../../plugins/woocommerce/templates/` are overridden by matching files in `templates/`.
|
|
|
|
### Twig Compatibility
|
|
|
|
- **`|values` filter does NOT exist in Twig 3.x** -- use loop-based sum instead. Twig 4.x only.
|
|
- **Twig autoescape + WordPress escape filters = double encoding** -- register all `esc_*` filters with `['is_safe' => ['html']]` option in the plugin's `Template.php`.
|
|
- **`wp i18n make-pot` does NOT scan Twig templates** -- any string used exclusively in `.html.twig` files must be manually added to the `.pot` file.
|
|
- **`#, fuzzy` silently skips translations at runtime** -- always remove fuzzy flags after verifying translations.
|
|
- **`|nl2br|esc_html` is wrong filter order** -- `nl2br` outputs `<br>` tags, then `esc_html` escapes them to `<br>`. Correct: `|esc_html|nl2br`.
|
|
- **`function() is defined` is semantically wrong** -- always evaluates truthy since the function is called regardless. Use `{% if function() %}` directly.
|
|
- **HTML entities in translated strings get double-encoded** -- `…` in `__()` becomes `&hellip;`. Use Unicode `…` directly or append `|raw` for trusted filter output.
|
|
- **Filter chains producing HTML need `|raw`** -- e.g., `term_description()|wptexturize|wpautop|do_shortcode|raw`.
|
|
|
|
### Bootstrap 5 vs Plugin CSS Conflicts
|
|
|
|
- Plugin CSS may define its own grid systems that override Bootstrap's flexbox `row row-cols-*`. Add theme CSS overrides as needed.
|
|
- **CSS dependency chain**: `woocommerce` -> child theme overrides. Ensures correct cascade.
|
|
- jQuery `.show()`/`.hide()` cannot override Bootstrap `!important` (`d-none`). Toggle both class and inline style.
|
|
- `overflow: visible !important` on `.wp-block-navigation__container` is essential for dropdowns inside block theme navigation.
|
|
- **WooCommerce `shop_table` borders conflict with Bootstrap `.table`** -- reset with `.woocommerce table.shop_table { border: 0 }` and cell `border-left/right: 0`.
|
|
- **WooCommerce gallery JS requires modifier classes** -- `--with-images`, `--without-images`, `--columns-N` on `.woocommerce-product-gallery` + `style="opacity: 0"` for fade-in. Without these, zoom and photoswipe won't initialize.
|
|
- **WooCommerce dark mode select backgrounds** -- `woocommerce.css` sets `select { background-color: var(--wc-form-color-background, #fff) }`. The custom property is never defined for dark mode, so it falls back to `#fff`. Override with `[data-bs-theme="dark"] .woocommerce select`.
|
|
- **Select2/SelectWoo dark mode** -- `select2.css` hardcodes `#fff` on selection containers and dropdowns. Needs overrides for `.select2-selection`, `.select2-dropdown`, `.select2-search__field`, and highlighted options.
|
|
- **WooCommerce notice CSS specificity** -- Uses `border-top: 3px solid`, `background-color: #f6f5f8`, and icon font `::before` at specificity `0,1,0`. Must use `.woocommerce .woocommerce-*` AND `.alert.woocommerce-*` (both specificity `0,2,0`) to cover notices both inside `.woocommerce` wrappers and rendered directly by Twig templates.
|
|
- **WooCommerce notice focus ring** -- `woocommerce.js:focus_populate_live_region()` adds `tabindex="-1"` and calls `.focus()` on the first `.woocommerce-message[role="alert"]` after 500ms for screen reader announcement. The default browser focus ring appears white in dark mode. Suppress with `outline: 0; box-shadow: none` since the focus is non-interactive.
|
|
- **WooCommerce form input specificity** -- `.woocommerce form .form-row .input-text` (specificity `0,3,1`) sets `background-color: var(--wc-form-color-background, #fff)`. This beats theme checkout rules at `0,2,1`. Needs `[data-bs-theme="dark"]` override at `0,4,0`.
|
|
- **Bootstrap `table-light` breaks dark mode** -- Forces a light background on `<thead>` regardless of `data-bs-theme`. Remove it and let Bootstrap's default table styling handle theming.
|
|
- **WooCommerce float layout fights Bootstrap grid** -- `div.product div.images/summary` have `float:left/right; width:48%` in `woocommerce-layout.css`. Override with `float: none; width: 100%`.
|
|
- **Bootstrap `g-*` gutters add negative top margin** -- `g-4` sets both `--bs-gutter-x` and `--bs-gutter-y`; the `.row` gets `margin-top: calc(-1 * var(--bs-gutter-y))` pulling it upward. Use `gx-*` for horizontal-only gutters when vertical gap isn't desired.
|
|
|
|
### WooCommerce Variation JS
|
|
|
|
- **NEVER use HTML `disabled` attribute on the add-to-cart button.** WC's `add-to-cart-variation.js` only manages CSS classes (`disabled`, `wc-variation-selection-needed`) via jQuery `.addClass()`/`.removeClass()`. It never removes the HTML `disabled` attribute. The HTML attribute prevents click events from firing, making the button permanently unclickable.
|
|
- Use CSS classes `disabled wc-variation-selection-needed` instead. WC's `onAddToCart` handler checks `$button.is('.disabled')` (CSS class), not the HTML property.
|
|
- WC variation form requires `data-product_id` and `data-product_variations` on `<form class="variations_form">`. Without these, the JS doesn't initialize.
|
|
- Attribute select `name` and `data-attribute_name` must use `sanitize_title()` on the attribute name (lowercase, hyphens) to match the keys in the variation JSON data.
|
|
|
|
### WooCommerce Variation Select CSS Specificity
|
|
|
|
- WC's `.woocommerce div.product form.cart .variations select` at specificity `(0,4,3)` uses `background` shorthand — this resets BOTH `background-color` and `background-image`. A dark mode override must exceed `(0,4,3)` AND override `background-image` with `var(--bs-form-select-bg-img)` for Bootstrap's dark-mode-aware chevron SVG.
|
|
- Previous selector `[data-bs-theme="dark"] .variations .form-select` at `(0,3,0)` lost the specificity battle.
|
|
|
|
### WooCommerce Gallery Data Integrity
|
|
|
|
- `_product_image_gallery` post meta may contain IDs pointing to product variations or other non-attachment posts instead of image attachments. `wp_get_attachment_url()` returns empty/false for these.
|
|
- Always guard thumbnail rendering with `{% if thumb_url %}` after calling `wp_get_attachment_url(image_id)`.
|
|
- The main product image should be prepended to the gallery strip so users can switch back to it after viewing gallery images.
|
|
|
|
### Double Heading Prevention
|
|
|
|
- Parent theme (`wp-bootstrap`) conditionally skips its `<h1>` when `post.title` is empty.
|
|
- Plugin passes empty `post.title` to parent theme's Twig system.
|
|
- Child theme layout templates always render their own `<h1>` with `page_title` context.
|
|
|
|
### Theme Wrapping (`_wrapped` / `_theme_wrapped`)
|
|
|
|
- `_wrapped` is set in `base.html.twig` when `_theme_wrapped` is true (parent theme provides page shell).
|
|
- **Structural blocks** (wrapper, breadcrumbs, sidebar, `<h1>`) are suppressed when wrapped.
|
|
- **Functional blocks** (`{% block scripts %}`) must ALWAYS render -- they contain AJAX handlers, JS initialization etc.
|
|
|
|
### Plugin JS Compatibility
|
|
|
|
- Theme must preserve plugin's JS-bound CSS class names (e.g., repeater fields, interactive widgets).
|
|
- Theme uses plugin's global JS objects for AJAX URLs, nonces, and i18n strings.
|
|
- Bootstrap 5 `data-bs-toggle` attributes replace plugin's custom dropdown mechanisms.
|
|
|
|
### Security
|
|
|
|
- Security audit checklist (OWASP Top-10): open redirect validation, `|esc_url`/`|esc_attr`/`|esc_js` on all dynamic output, `|wp_kses_post` for rich text, no `innerHTML` with dynamic data.
|
|
|
|
## Multi-Project Architecture
|
|
|
|
This child theme is part of a three-project ecosystem. Each project is a separate git repo with its own `CLAUDE.md` and should be worked on in **separate Claude Code sessions**.
|
|
|
|
### Projects
|
|
|
|
| Project | Type | Location | Repository |
|
|
| ------- | ---- | -------- | ---------- |
|
|
| woocommerce | Plugin (read-only) | `wp-content/plugins/woocommerce/` | <https://github.com/woocommerce/woocommerce.git> |
|
|
| wp-bootstrap | Parent Theme | `wp-content/themes/wp-bootstrap/` | <https://src.bundespruefstelle.ch/magdev/wp-bootstrap> |
|
|
| wc-bootstrap | Child Theme | `wp-content/themes/wc-bootstrap/` | <ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git> |
|
|
|
|
### Dependency Chain
|
|
|
|
```txt
|
|
wp-bootstrap (parent theme, Bootstrap 5 FSE + Twig rendering)
|
|
+-- wc-bootstrap (child theme, overrides plugin Twig templates with Bootstrap 5)
|
|
+-- woocommerce (plugin, provides post types, logic, base Twig templates)
|
|
```
|
|
|
|
### Important Constraints
|
|
|
|
- **WooCommerce plugin is read-only.** We have no control over its source code. All customizations happen in the child theme via template overrides and hooks.
|
|
- **Docker environment:** Container name is `woocommerce`. Use `docker exec woocommerce ...` for commands and `docker exec woocommerce apache2ctl graceful` to clear OPcache after PHP changes.
|
|
|
|
### Cross-Project Workflow
|
|
|
|
1. **One session per project.** Each session reads its own `CLAUDE.md` and stays within its own git repo.
|
|
2. **Plugin changes first.** When a change affects templates consumed by the theme, make the plugin change first, then switch to the theme session.
|
|
3. **Communicate changes explicitly.** When switching sessions, describe what changed upstream (new variables, renamed classes, new templates, etc.).
|
|
4. **Shared Docker environment.** All three projects will be bind-mounted into the same Docker container once it is set up, so changes will be visible live without rebuilds.
|
|
|
|
### Template Contract (Plugin -> Theme)
|
|
|
|
The child theme overrides plugin Twig templates by prepending its own `templates/` directory to the Twig `FilesystemLoader` via `TemplateOverride` class (hooks `init` priority 20, after plugin init at priority 0).
|
|
|
|
#### Twig Template Locations
|
|
|
|
- **Plugin base templates:** `wp-content/plugins/woocommerce/templates/`
|
|
- **Theme overrides:** `wp-content/themes/wc-bootstrap/templates/` (mirrors plugin structure)
|
|
- **Resolution order:** Theme templates take priority over plugin templates (Twig `prependPath`)
|
|
|
|
### Theme Contract (Parent -> Child)
|
|
|
|
The child theme inherits from `wp-bootstrap` via WordPress `Template: wp-bootstrap` declaration.
|
|
|
|
- **Bootstrap 5:** CSS framework and JS loaded by parent theme
|
|
- **Bootstrap Icons:** Web font available via parent theme's SCSS build
|
|
- **Twig rendering:** Parent theme's `TwigService` handles frontend rendering
|
|
- **Dark mode:** Parent handles via `data-bs-theme` attribute; child theme inherits
|
|
- **Style variations:** Color palettes available from parent, bridged to Bootstrap CSS custom properties
|
|
|
|
## Architecture Decisions
|
|
|
|
- **My Account layout** uses `col-lg-auto` (nav) + `col-lg` (content) so the nav auto-sizes to its content and the main area fills remaining space. Requires CSS resets for WooCommerce's float-based layout (`float: none; width: auto/100%`) and `max-width: none` on `.woocommerce-account main .woocommerce` to override the plugin's `max-width: 1000px`.
|
|
- **My Account navigation** uses `offcanvas-lg offcanvas-start` responsive pattern: full sidebar with icons on desktop (sticky via `position-sticky`), offcanvas slide-in on mobile with toggle button. Icons are mapped per endpoint via a Twig hash.
|
|
- **My Account dashboard** uses card-based quick actions (`row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3`) with `bg-primary-subtle` icon containers that adapt to dark mode. Each action card is an `<a class="card">` for full-card clickability.
|
|
- **My Account forms** (edit-account, edit-address, lost-password, reset-password) are wrapped in `card shadow-sm` sections with icon headers for visual consistency.
|
|
- **Edit forms** use card-based section grouping (e.g., "Personal information" + "Password change" as separate cards) instead of `<fieldset>/<legend>`.
|
|
- **Detail pages** use 8+4 column layout with sticky sidebar.
|
|
- **Cards** use `<article class="card h-100">` with `stretched-link`.
|
|
- **Form layout** uses centered `col-lg-8 col-xl-7` with card + shadow for auth forms.
|
|
- **Mobile patterns**: offcanvas for search filters; navbar uses Bootstrap `offcanvas-lg` responsive pattern.
|
|
- **Email templates**: use custom CSS class prefix (not Bootstrap) to avoid email client conflicts.
|
|
- **PDF exports**: inline styles aligned with Bootstrap color palette (no external CSS in PDF renderers).
|
|
|
|
## Version History
|
|
|
|
Current version: **v0.1.6**
|
|
|
|
## Session History
|
|
|
|
### 2026-03-01 — v0.1.6 Add PHPUnit Test Suite
|
|
|
|
**Scope:** Added PHPUnit test infrastructure with Brain\Monkey for WordPress function mocking, covering both PHP classes (`TemplateOverride`, `WooCommerceExtension`). Added test job to Gitea CI pipeline.
|
|
|
|
**Files created (7):**
|
|
|
|
- `phpunit.xml.dist` — PHPUnit 11 configuration (bootstrap, test suite, source coverage)
|
|
- `tests/bootstrap.php` — Loads Composer autoloader and class stubs
|
|
- `tests/Stubs/TwigService.php` — `WPBootstrap\Twig\TwigService` stub with injectable render callback and singleton reset
|
|
- `tests/Stubs/WcProduct.php` — Minimal `\WC_Product` stub with `get_id()`
|
|
- `tests/Unit/TemplateOverrideTest.php` — 9 tests: hook registration, Twig resolution, buffer stack (including nesting), context passing, global `$product` injection, exception fallback
|
|
- `tests/Unit/Twig/WooCommerceExtensionTest.php` — 18 tests: function/filter registration, `callFunction()` whitelist enforcement, output capture wrappers, `setupProductData`
|
|
|
|
**Files modified (4):**
|
|
|
|
- `composer.json` — Added `require-dev` (phpunit ^11.0, brain/monkey ^2.6) and `autoload-dev` PSR-4 mapping
|
|
- `.gitea/workflows/release.yml` — Added `test` job between `lint` and `build-release`; excluded `tests/`, `phpunit.xml.dist`, `.phpunit.cache` from release packages
|
|
- `.gitignore` — Changed `.phpunit.cache/test-results` to `.phpunit.cache/`
|
|
- `style.css` — Version bump 0.1.5 → 0.1.6
|
|
|
|
**Key decisions:**
|
|
|
|
- **Brain\Monkey over WP_Mock:** Lighter weight, better patchwork integration, same pattern as wp-bootstrap parent theme
|
|
- **TwigService stub with injectable callback:** Allows tests to control render output without full Twig environment; includes `reset()` for test isolation
|
|
- **`@` error suppression for `error_log` in fallback test:** PHP internal functions can't be mocked by Brain\Monkey without patchwork.json config; suppressing is simpler than adding patchwork config for a single test
|
|
- **No patchwork.json:** Avoids complexity; only `error_log` needed mocking and the `@` operator suffices for the test assertion
|
|
|
|
**Test results:** 27 tests, 54 assertions, all passing
|
|
|
|
### 2026-03-01 — v0.1.5 Fix 10 Known Bugs
|
|
|
|
**Scope:** Fixed all 10 bugs from KNOWN_BUGS.md — catalog page features (title, breadcrumbs, categories, filters, sort dropdown), single product fixes (variable form, gallery, related products, grouped products), and downloads page.
|
|
|
|
**Files changed (17):**
|
|
|
|
- `inc/Twig/WooCommerceExtension.php` — Added `woocommerce_page_title` and `wc_get_customer_available_downloads` to `ALLOWED_FUNCTIONS`; added `wc_setup_product_data()` method, `wp_reset_postdata` Twig function, and `sanitize_title` Twig filter
|
|
- `templates/single-product/add-to-cart/variable.html.twig` — Added `data-product_id` and `data-product_variations` attributes to form; applied `sanitize_title` filter on attribute names for variation matching
|
|
- `templates/single-product/add-to-cart/variation-add-to-cart-button.html.twig` — Removed HTML `disabled` attribute, replaced with CSS classes `disabled wc-variation-selection-needed` (WC JS only manages CSS classes, never the HTML attribute)
|
|
- `templates/single-product/product-image.html.twig` — Prepend main image to gallery thumbnail strip; added `{% if thumb_url %}` guard for invalid attachment IDs
|
|
- `templates/single-product/product-thumbnails.html.twig` — New empty override to suppress WC's default full-size gallery images
|
|
- `templates/single-product/related.html.twig` — Added `wc_setup_product_data()` before each product render + `wp_reset_postdata()` after loop
|
|
- `templates/single-product/up-sells.html.twig` — Same setup_postdata fix as related.html.twig
|
|
- `templates/single-product/add-to-cart/grouped.html.twig` — Rewrote to compute `quantites_required`/`show_add_to_cart_button` in loop (matching WC PHP); moved hidden input outside conditional; added `has_options()` and `is_sold_individually()` checks
|
|
- `templates/loop/header.html.twig` — Replaced `page_title` context variable with direct `fn('woocommerce_page_title', false)` call
|
|
- `woocommerce/archive-product.php` — Added `woocommerce_breadcrumb()` call before shop loop header
|
|
- `functions.php` — Changed loop columns from 4 to 3; added product gallery JS conditional enqueue
|
|
- `templates/loop/loop-start.html.twig` — Changed default columns from 4 to 3
|
|
- `assets/css/wc-bootstrap.css` — Replaced duplicate ordering select rules with `appearance: none`; increased dark mode variation select specificity to `(0,5,1)` to beat WC's `(0,4,3)` background shorthand
|
|
- `templates/content-product-cat.html.twig` — Renamed to `content-product_cat.html.twig` (WC uses underscore)
|
|
- `assets/js/product-gallery.js` — New: thumbnail click-to-swap, gallery fade-in, active state highlighting
|
|
- `templates/myaccount/downloads.html.twig` — Replaced `fn('WC').customer.get_downloadable_products()` with direct `fn('wc_get_customer_available_downloads', get_current_user_id())`
|
|
- `style.css` — Version bump 0.1.4 → 0.1.5
|
|
|
|
**Key decisions:**
|
|
|
|
- **`wc_setup_product_data()` over PHP bridge:** Adding a Twig function that sets `$GLOBALS['product']` + `setup_postdata()` is simpler and more maintainable than creating PHP bridge files for related/upsells rendering
|
|
- **`json_encode|esc_attr` over `wc_esc_json`:** Avoids adding a custom filter; `esc_attr` performs the same HTML entity encoding as `wc_esc_json`
|
|
- **3 columns default:** With sidebar taking `col-lg-3`, 3 product columns in `col-lg-9` gives better card proportions than 4
|
|
- **Vanilla JS gallery:** Lightweight click-to-swap handler instead of WC's built-in flexslider/photoswipe (which requires specific PHP setup for `wp_get_attachment_image_src` data attributes)
|
|
- **CSS class `disabled` over HTML `disabled` attribute:** WC's `add-to-cart-variation.js` `onShow`/`onHide` only toggle CSS classes (`disabled`, `wc-variation-selection-needed`). The `onAddToCart` handler checks `.is('.disabled')`. Using the HTML `disabled` attribute prevents click events entirely and WC JS never removes it.
|
|
- **CSS specificity `(0,5,1)` for dark mode selects:** WC's `.woocommerce div.product form.cart .variations select` at `(0,4,3)` uses `background` shorthand which resets `background-color` and `background-image`. Must exceed this specificity AND override `background-image` for Bootstrap's dark-mode SVG chevron.
|
|
|
|
**Key findings (WC variation JS):**
|
|
|
|
- WC's `add-to-cart-variation.js` event flow: `onChange` → `check_variations` → `findMatchingVariations` → `found_variation` → `onFoundVariation` (300ms setTimeout) → `show_variation` → `onShow` (removes CSS class only)
|
|
- `onShow` calls `$button.removeClass('disabled wc-variation-selection-needed')` — never touches the HTML `disabled` attribute/property
|
|
- `onAddToCart` checks `$button.is('.disabled')` — CSS class, not HTML attribute
|
|
- Gallery `_product_image_gallery` meta may contain IDs of product variations or posts instead of image attachments — always guard with `{% if thumb_url %}` after `wp_get_attachment_url()`
|
|
|
|
### 2026-03-01 — v0.1.4 Security Audit & Performance Fixes
|
|
|
|
**Scope:** Cross-theme security audit (12 findings), all fixed. Covers fn() whitelist, notice data escaping, search query escaping, per-request context caching, shared render helper, and unused constant removal.
|
|
|
|
**Files changed (6):**
|
|
|
|
- `inc/Twig/WooCommerceExtension.php` — Added `ALLOWED_FUNCTIONS` whitelist to `callFunction()`. Only 6 functions (`WC`, `_n`, `get_pagenum_link`, `wc_review_ratings_enabled`, `wc_get_product_category_list`, `wc_get_product_tag_list`) are permitted.
|
|
- `templates/notices/success.html.twig` — `notice.data|raw` → `notice.data|wp_kses_post`
|
|
- `templates/notices/error.html.twig` — `notice.data|raw` → `notice.data|wp_kses_post`
|
|
- `templates/notices/notice.html.twig` — `notice.data|raw` → `notice.data|wp_kses_post`
|
|
- `templates/product-searchform.html.twig` — Added `|esc_attr` on `get_search_query()` value
|
|
- `functions.php` — Removed unused `WC_BOOTSTRAP_VERSION`/`WC_BOOTSTRAP_URL` constants. Added `wc_bootstrap_get_theme_context()` with static caching and `wc_bootstrap_render_in_page_shell()` helper. Refactored 3 render functions to use shared helpers.
|
|
|
|
**Key decisions:**
|
|
|
|
- **fn() whitelist defense-in-depth**: Template files are static PHP, not user-editable, so exploitation requires file write access. Whitelist added anyway as defense-in-depth to prevent `exec()`, `system()`, etc. if template context were ever compromised.
|
|
- **`|wp_kses_post` over `|raw` for data attributes**: WooCommerce sanitizes notice data, but belt-and-suspenders approach prevents XSS if upstream behavior changes.
|
|
- **Static variable caching over transients**: Per-request `static $cached_context` is sufficient since WooCommerce pages build context once. No transient overhead or invalidation needed.
|
|
|
|
### 2026-02-28 — My Account Bootstrap 5 Polish
|
|
|
|
**Scope:** Redesigned 8 my-account Twig templates + CSS overrides to feel like a polished Bootstrap 5 application.
|
|
|
|
**Files changed (10):**
|
|
|
|
- `templates/myaccount/navigation.html.twig` — Added endpoint icon map, `offcanvas-lg` responsive pattern, sticky sidebar
|
|
- `templates/myaccount/dashboard.html.twig` — Replaced plain `<p>` tags with welcome card (avatar + greeting) and 5 quick-action cards in responsive grid
|
|
- `templates/myaccount/view-order.html.twig` — Replaced `<mark>` tags with summary card using `list-group-flush` and `components/status-badge.html.twig`; notes wrapped in card
|
|
- `templates/myaccount/form-edit-account.html.twig` — Wrapped in two card sections (Personal info + Password change) with icons
|
|
- `templates/myaccount/form-edit-address.html.twig` — Wrapped in card with dynamic icon (`bi-receipt` billing / `bi-truck` shipping)
|
|
- `templates/myaccount/form-lost-password.html.twig` — Wrapped in card with `bi-key` icon
|
|
- `templates/myaccount/form-reset-password.html.twig` — Wrapped in card with `bi-shield-lock` icon
|
|
- `templates/myaccount/lost-password-confirmation.html.twig` — Added `text-body-secondary` styling and "Back to login" button
|
|
- `templates/myaccount/my-account.html.twig` — Changed grid from `col-lg-3`/`col-lg-9` to `col-lg-auto`/`col-lg`
|
|
- `assets/css/wc-bootstrap.css` — Reset WooCommerce float layout, override `max-width: 1000px`, avatar rounding, card hover lift
|
|
|
|
**Design decisions:**
|
|
|
|
- **Nav column auto-sizing (`col-lg-auto`)**: Fixed-width columns (e.g., `col-lg-3` = 285px) caused label overflow with icons. Auto-sizing lets the nav take exactly the space it needs across locales while the content fills the rest.
|
|
- **WooCommerce layout overrides require matching specificity**: The plugin uses `.woocommerce-account .woocommerce-MyAccount-navigation` (specificity `0,2,0`) — single-class selectors don't override. Also `.woocommerce-account main .woocommerce` sets `max-width: 1000px` (specificity `0,2,1`) which must be reset to `none`.
|
|
- **`offcanvas-lg` over `d-none d-lg-block`**: Bootstrap's responsive offcanvas natively handles the desktop/mobile switch without duplicating nav markup. The toggle button uses `d-lg-none` visibility.
|
|
- **`bg-primary-subtle` for icon containers**: These Bootstrap 5.3 contextual utilities automatically adapt to dark mode, unlike hardcoded colors.
|
|
- **Welcome message restructured**: Separated greeting from logout link instead of using WooCommerce's default inline-linked `__()` string. This gives full control over card layout and avoids translated strings containing HTML structure assumptions.
|
|
- **Templates NOT changed** (already well-done): `orders.html.twig`, `my-address.html.twig`, `form-login.html.twig`, `payment-methods.html.twig`, `form-add-payment-method.html.twig`, `downloads.html.twig`
|
|
|
|
### 2026-02-28 — Single Product Bootstrap 5 Layout + Template Quirks Audit
|
|
|
|
**Scope:** Created Bootstrap 5 two-column layout for single product pages. Then audited all ~90 Twig templates for WooCommerce CSS quirks, Twig escaping bugs, and missing Bootstrap styling.
|
|
|
|
**Single product layout (3 files):**
|
|
|
|
- `woocommerce/content-single-product.php` — Bridge file for `wc_get_template_part()` interception
|
|
- `templates/content-single-product.html.twig` — Two-column `row gx-4 gx-lg-5` grid (images left, summary right)
|
|
- `assets/css/wc-bootstrap.css` — Float/width reset, sale badge positioning, shop_table border reset, gallery opacity fallback
|
|
|
|
**Template quirks audit (14 files fixed):**
|
|
|
|
- `order/order-details.html.twig` — Fixed `|nl2br|esc_html` filter order (was escaping `<br>` tags)
|
|
- `single-product/product-image.html.twig` — Added WC gallery modifier classes + opacity:0 for JS init
|
|
- `brands/brand-description.html.twig` — Added `|raw` to HTML-producing filter chain
|
|
- `single-product/up-sells.html.twig`, `cart/cross-sells.html.twig`, `single-product/related.html.twig` — Fixed `…` double-encoding in headings
|
|
- `myaccount/dashboard.html.twig` — Removed duplicate deprecated hook fires
|
|
- `product-searchform.html.twig` — Replaced `…` entity with Unicode `…`
|
|
- `cart/cart-totals.html.twig`, `checkout/review-order.html.twig`, `checkout/form-login.html.twig`, `checkout/terms.html.twig` — Removed wrong `function() is defined` guards
|
|
- `wc-base.html.twig` — Added `|esc_attr` on notification type in class attribute
|
|
- `global/form-login.html.twig` — Added `is defined` guard on `hidden` variable
|
|
- `single-product/add-to-cart/variation.html.twig` — Added `role="alert"` for accessibility
|
|
- `cart/mini-cart.html.twig` — Changed remove link to Bootstrap `btn btn-sm btn-outline-danger`
|
|
- `cart/cart-shipping.html.twig` — Made `form-check` class conditional on multiple shipping methods
|
|
|
|
**Infrastructure:**
|
|
|
|
- Created `.claude/settings.json` with allowed commands (git, docker, composer, npm, etc.)
|
|
|
|
### 2026-02-28 — v0.1.0 Changelog & Re-Release
|
|
|
|
**Scope:** Added CHANGELOG.md entry for v0.1.0 and re-released the tag.
|
|
|
|
**Files changed (1):**
|
|
|
|
- `CHANGELOG.md` — Added `[0.1.0]` section documenting all features and fixes between v0.0.1 and v0.1.0
|
|
|
|
### 2026-02-28 — v0.1.1 Dark Mode & Notice CSS Bugfixes
|
|
|
|
**Scope:** Fixed dark mode select backgrounds, WooCommerce notice styling conflicts, and product card image overflow.
|
|
|
|
**Files changed (2):**
|
|
|
|
- `assets/css/wc-bootstrap.css` — Dark mode overrides for native `<select>` and Select2/SelectWoo widgets; bumped notice selectors to `.woocommerce .woocommerce-*` (specificity `0,2,0`) to beat `woocommerce.css`; suppressed WooCommerce icon font `::before` on notices; added checkout form focus color for dark mode
|
|
- `templates/content-product.html.twig` — Added `overflow-hidden` to product card `<article>` for border-radius clipping
|
|
|
|
**Key learnings:**
|
|
|
|
- WooCommerce CSS sets `select { background-color: var(--wc-form-color-background, #fff) }` — the custom property is never defined for dark mode, so it falls back to white. Override with `[data-bs-theme="dark"] .woocommerce select`.
|
|
- Select2/SelectWoo hardcodes `#fff` backgrounds in `select2.css` — needs comprehensive overrides for selection containers, dropdowns, search fields, and highlighted options.
|
|
- WooCommerce notice CSS uses `border-top: 3px solid`, `background-color: #f6f5f8`, and WooCommerce icon font `::before` at specificity `0,1,0`. Single-class overrides don't win — must use `.woocommerce .woocommerce-*` (specificity `0,2,0`) and explicitly reset `border-top`.
|
|
- Never set `background-image` without `background-repeat: no-repeat` / `background-size` / `background-position` — SVGs will tile.
|
|
|
|
### 2026-02-28 — v0.1.2 Dark Mode Deep Fixes (Tables, Inputs, Notices)
|
|
|
|
**Scope:** Fixed dark mode rendering issues across checkout, thank-you, cart, and account pages — white table headers, white form inputs, notice focus rings, and missing card wrappers.
|
|
|
|
**Files changed (7):**
|
|
|
|
- `assets/css/wc-bootstrap.css` — Added `[data-bs-theme="dark"]` override for `.input-text` and `textarea` (WooCommerce specificity `0,3,1` beats theme at `0,2,1`); added `.alert.woocommerce-*` compound selectors for notice overrides outside `.woocommerce` wrapper; added focus ring suppression for programmatically focused notices
|
|
- `templates/order/order-details.html.twig` — Wrapped product table in `card shadow-sm` with `card-header`; removed `table-light` from `<thead>`
|
|
- `templates/checkout/thankyou.html.twig` — Added `d-flex align-items-center` to success alerts to fix icon/text line wrap
|
|
- `templates/checkout/review-order.html.twig` — Removed `table-light` from `<thead>`
|
|
- `templates/cart/cart.html.twig` — Removed `table-light` from `<thead>`
|
|
- `templates/myaccount/orders.html.twig` — Removed `table-light` from `<thead>`
|
|
- `templates/myaccount/payment-methods.html.twig` — Removed `table-light` from `<thead>`
|
|
|
|
**Key findings:**
|
|
|
|
- WooCommerce's `.woocommerce form .form-row .input-text` at specificity `0,3,1` sets `background-color: var(--wc-form-color-background, #fff)` which beats theme rules. The `textarea` generated by `woocommerce_form_field()` has class `input-text`, so it matches this rule. Needs `[data-bs-theme="dark"]` override at `0,4,0`.
|
|
- WooCommerce's `focus_populate_live_region()` in `woocommerce.js` adds `tabindex="-1"` and calls `.focus()` on `.woocommerce-message[role="alert"]` after 500ms for screen reader accessibility. The default browser focus ring appears white in dark mode.
|
|
- Notice overrides using `.woocommerce .woocommerce-*` descendant selectors don't match notices rendered by Twig templates where `.alert` and `.woocommerce-message` are on the same element without a `.woocommerce` wrapper ancestor. Both `.woocommerce .woocommerce-*` and `.alert.woocommerce-*` patterns needed.
|
|
- Bootstrap's `table-light` class forces a light background on `<thead>` regardless of dark mode. Remove it entirely — let the card-header or default table styling handle visual separation.
|