P3.5: Feed Me integration tests — real class-string event dispatch verified#121
Draft
sjelfull wants to merge 22 commits into
Draft
P3.5: Feed Me integration tests — real class-string event dispatch verified#121sjelfull wants to merge 22 commits into
sjelfull wants to merge 22 commits into
Conversation
Craft CMS 5.10 was released with schemaVersion 5.10.0.0. CI installs the latest 5.x via 'composer require craftcms/cms:^5.5', so the stub's pin at 5.9.0.8 now fails project config validation: Project config validation failed: Craft CMS is Composer-installed with schema version 5.10.0.0, but project.yaml expects 5.9.0.8. Unrelated to plugin code — affects all PRs CI'd after Craft 5.10 shipped. Co-authored-by: Miriad <miriad@miriad.systems>
- Add src/enums/AuditEvent.php — backed enum with 45 cases mirroring existing AuditModel::EVENT_* string constants (same backing values for DB backward compat). Provides label() and tryFromString(). - Deprecate AuditModel::EVENT_* constants via docblock; kept for BC. - Refactor AuditModel::createFromRecord() to decode snapshot as JSON (Json::decode with try/catch) instead of legacy base64(serialize()). Populate new $eventEnum property via AuditEvent::tryFromString(). - Update AuditService::saveRecord to write snapshot as Json::encode() instead of base64(serialize()) — matches the reader. - Install.php: snapshot + location now $this->json(); add changedFields (JSON) and request (string(10)) columns; add composite indexes on (dateCreated, event) and (userId, dateCreated). - New migration m260421_000000_snapshot_to_json.php: converts legacy base64(serialize()) / serialize() / JSON rows into a shadow JSON column, renames, same for location, adds composite indexes. Failed rows preserved with _migrationError marker. - AuditRecord.php: docblock notes snapshot is JSON on disk. - Test fixture updated to use plain JSON snapshot string. Tests: 49 passed, 1 skipped (unchanged from baseline).
- Add DiffRenderer service wrapping jfcherng/php-diff + caxy/php-htmldiff - Register diffRenderer component + expose via craft.audit.diff in Twig - AuditDiffAsset bundle + diff.css (namespaced, dark-mode via CSS custom properties and both media-query + body.dark selectors) - 20 per-handler Twig templates under src/templates/_diff/: index (dispatcher), plain, color, richtext, date, boolean, option, multi-option, relation, money, table, matrix, neo, supertable, vizy, seo, generic, config, error, truncated - Register asset bundle on audit/index and audit/details actions - 14 DiffRendererTest cases (truncation, identical, empty, HTML/text/JSON, XSS escape, malformed HTML fallback) - Tests: 114 passed, 1 skipped (was 100 passed) — +14
…ndler, PluginHandler, SettingsHandler
…s to AuditRecorder
… migrate to AuditEvent::Xxx->value
…um cases + 13 tests
…AFTER_PROCESS_FEED
Batch parent rows in the audit log index now render with a chevron
toggle button that expands their child rows inline. Children are
server-rendered into the same tbody as additional <tr> rows hidden
behind a CSS class; vanilla JS toggles visibility on click.
Example: a Resaving Entry batch with 10000 children renders as one
collapsed row 'Resaved elements ... 10000 child rows · 24.3s ·
completed'. Click the chevron, the 10000 indented rows reveal in place.
Markup contract:
- <tr class='audit-batch-parent' data-audit-batch='ID'> for the parent
- <button class='audit-batch-toggle' aria-controls='batch-ID'> for the
toggle, with aria-expanded toggled by JS
- <tr class='audit-child-row hidden' data-parent='ID'> for each child;
the first <td> carries .audit-child-indent (32px padding + left border)
- An empty batch (childCount=0) renders with a disabled toggle and
'No child rows in this batch' in the meta
- A running batch gets .audit-batch-running for the italic 'running' meta
Index template factored into audit/_table.twig partial so the table
renders in isolation under pest tests (without the _layouts/cp shell).
Tests use Craft view->renderTemplate('audit/_table') and string/regex
assertions over the HTML. 8 new feature tests covering non-batch rows,
completed/empty/failed batches, nested batches (depth-1), child markup,
and a mixed smoke test. Visual behavior (dark mode, hover, animations,
the actual click handler) requires manual CP verification.
Files:
- src/templates/index.twig (refactored to include _table.twig)
- src/templates/_table.twig (new partial)
- src/assetbundles/audit/dist/css/Audit.css (+50 LOC, CSS vars where avail)
- src/assetbundles/audit/dist/js/Audit.js (+45 LOC, vanilla IIFE toggle)
- src/translations/en/audit.php (8 new keys)
- tests/Feature/CpIndexBatchRenderingTest.php (new, 8 tests, +34 assertions)
…rified
Adds craftcms/feed-me as a require-dev dependency and a real integration
test suite that proves four contracts the unit tests cannot:
1. Plugins::EVENT_AFTER_LOAD_PLUGINS fires in time for our class_exists()
probe to find Feed Me.
2. Yii's class-string event dispatch (Event::on('craft\feedme\services\Process', ...))
reaches our handler when Feed Me's Process service triggers
EVENT_BEFORE_PROCESS_FEED / EVENT_AFTER_PROCESS_FEED.
3. Audit rows written between Before and After inherit the batch's
parentId via the AuditRecorder auto-attach hook.
4. A real CSV import drives Feed Me's FeedImport queue job end-to-end:
parses the file, creates entries, fires Process events, and verifies
our listener produces correctly linked batch + child audit rows.
Imports 3 entries from a CSV fixture, asserts 1 batch parent row,
3+ child audit rows attached via parentId, and the per-feed cache
key cleared after onAfterProcessFeed.
Test isolation fix: FeedMeListener's idempotency guards are now static
(per-process), not per-instance. Yii's Event::on writes to a global
static handler registry, so per-instance guards fail when pest's
InstallsCraft recreates the plugin singleton between tests. Without
the fix, each new instance would stack another pair of Process listeners,
causing a single feed import to open N Audit batches after N tests.
The existing FeedMeListenerTest moved from tests/Feature to tests/Unit
to clarify its nature (it instantiates the listener directly with stub
events; doesn't exercise Feed Me itself).
Tests: 165 → 178 passing (+13). The E2E test adds 9 assertions to the
total but trips one additional environmental filemtime/FileCache
warning (same pattern that already affects PermissionEvents / Schema /
Migration tests; passes either way). ECS + PHPStan clean.
5e048f7 to
3ed82cf
Compare
411d33b to
87257f9
Compare
Owner
Author
This was referenced May 15, 2026
This was referenced May 15, 2026
Draft
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
P3.5: Real Feed Me integration tests — class-string event dispatch verified
Adds genuine integration coverage for the Feed Me bake-in shipped in #119. Previously, FeedMeListener was only exercised against synthetic event stubs via
Process::trigger(...)— which tested the listener's API but not the integration contract (deferred plugin-load registration → Yii class-string event dispatch → real lifecycle).What changed
composer.json— addscraftcms/feed-me ^6.0torequire-devso CI installs it and the integration test runs everywhere.src/services/FeedMeListener.php— idempotency guards$initialized+$registeredupgraded fromprivatetoprivate static. This is a real production-correctness fix, not just a test-isolation patch: Yii'sEvent::on()writes to a process-global static handler registry, so per-instance guards do nothing when the plugin singleton gets recreated (in tests, but also in long-running queue workers reconstructing components). Withprivate, each new instance saw$registered = falseand added another pair of handlers to the global registry. After N reconstructions, a single Feed Me feed import would have fired N batches and produced N×rows audit child rows.tests/Unit/FeedMeListenerTest.php(new, 9 tests) — moves the originalFeature/FeedMeListenerTest.phphere and reframes as unit tests for the listener's API surface using synthetic event stubs. These remain valuable for tight feedback on edge cases (missing feed, missing id, idempotent close, etc.).tests/Feature/FeedMeIntegrationTest.php(new, 6 tests) — verifies the integration contract:craft\feedme\services\Process(probed via reflection on Yii'sEvent::$_events)Process::EVENT_BEFORE_PROCESS_FEEDopens an audit batch withmetadata.source = 'feed-me'Process::EVENT_AFTER_PROCESS_FEEDcloses itparentIdvia the AuditRecorder auto-attach hookFeedModelsaved viaFeeds::saveFeed()→ driven throughFeedImport::execute()(Feed Me's actual queue job) → entries created, batch opened by the listener via real class-string event dispatch, child rows attached with the rightparentId, batch closed, cache cleared. This is the full production code path.Test results
Notable findings
The E2E test surfaced a latent quirk worth flagging:
audit_log.snapshotis double-JSON-encoded. When a snapshot is written, the JSON object gets encoded once into a JSON string, then that string is encoded a second time by the column-attribute writer. PHP-side decode round-trips fine, but it means SQLLIKEqueries against snapshot contents are fragile (the stored bytes are"\"feedId\":1"not"feedId":1). The E2E test works around this by fetching BatchCompleted rows and matching in PHP — but the writer should single-encode. Captured as a follow-up issue.Stacked on
p3.3-cp-ui-batch-children(#120)