Skip to content

P3.5: Feed Me integration tests — real class-string event dispatch verified#121

Draft
sjelfull wants to merge 22 commits into
p3.3-cp-ui-batch-childrenfrom
p3.5-feed-me-integration-tests
Draft

P3.5: Feed Me integration tests — real class-string event dispatch verified#121
sjelfull wants to merge 22 commits into
p3.3-cp-ui-batch-childrenfrom
p3.5-feed-me-integration-tests

Conversation

@sjelfull
Copy link
Copy Markdown
Owner

@sjelfull sjelfull commented May 15, 2026

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 — adds craftcms/feed-me ^6.0 to require-dev so CI installs it and the integration test runs everywhere.
  • src/services/FeedMeListener.php — idempotency guards $initialized + $registered upgraded from private to private static. This is a real production-correctness fix, not just a test-isolation patch: Yii's Event::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). With private, each new instance saw $registered = false and 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 original Feature/FeedMeListenerTest.php here 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:
    1. Listener registers handlers on craft\feedme\services\Process (probed via reflection on Yii's Event::$_events)
    2. Triggering Process::EVENT_BEFORE_PROCESS_FEED opens an audit batch with metadata.source = 'feed-me'
    3. Triggering Process::EVENT_AFTER_PROCESS_FEED closes it
    4. Audit rows written between fire→fire inherit the batch's parentId via the AuditRecorder auto-attach hook
    5. After-without-before is a silent no-op
    6. End-to-end CSV import: a 3-row CSV → real FeedModel saved via Feeds::saveFeed() → driven through FeedImport::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 right parentId, batch closed, cache cleared. This is the full production code path.

Test results

  • 178 passed / 1 skipped / 760 assertions (+15 from new E2E test)
  • ECS clean
  • PHPStan clean (60 files, no errors)

Notable findings

The E2E test surfaced a latent quirk worth flagging:

audit_log.snapshot is 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 SQL LIKE queries 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)

sjelfull and others added 22 commits May 14, 2026 10:48
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
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.
@sjelfull
Copy link
Copy Markdown
Owner Author

This change is part of the following stack:

Change managed by git-spice.

This was referenced May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant