Skip to content

[#647] Added 'DropzoneTrait' for multi-file drag-and-drop step.#652

Open
AlexSkrypnyk wants to merge 3 commits into
mainfrom
feature/647-dropzone-trait
Open

[#647] Added 'DropzoneTrait' for multi-file drag-and-drop step.#652
AlexSkrypnyk wants to merge 3 commits into
mainfrom
feature/647-dropzone-trait

Conversation

@AlexSkrypnyk
Copy link
Copy Markdown
Member

@AlexSkrypnyk AlexSkrypnyk commented Jun 1, 2026

Closes #647

Summary

Adds DropzoneTrait to the generic namespace, providing two When steps that simulate a real multi-file drag-and-drop gesture by dispatching a single native drop event whose dataTransfer.files contains all requested files simultaneously. Unlike Mink's attachFile, which writes files to a hidden <input type="file"> one at a time (sequential uploads), this trait reproduces the concurrent-upload code path that real users trigger - exposing race conditions in dedup maps, status-indicator clobbering, queue deadlocks, and server-side handlers that only manifest under the multi-file path. Fixture paths are resolved against the Mink files_path parameter and validated to prevent path-traversal escapes.

Changes

New trait - src/DropzoneTrait.php

  • When I drop the file :path on the :selector dropzone - single-file convenience step (delegates to the multi-file step with a one-row TableNode)
  • When I drop the following files on the :selector dropzone: - drops N files in a single native drop event; injects temporary off-screen <input type="file"> elements to satisfy browser security, collects the File objects into a DataTransfer, dispatches DragEvent('drop', ...) on the resolved CSS target, then removes the holder inputs
  • dropzoneResolvePath() - resolves each path against files_path, rejects missing files and paths that escape the configured base directory via realpath comparison

New tests - tests/behat/features/dropzone.feature

  • 4 positive @javascript @phpserver scenarios: single-file drop, multi-file drop (verifies a single drop event fires), drop on a non-default selector, two consecutive drops in one scenario
  • 3 negative @trait:DropzoneTrait scenarios: missing target element, missing single fixture file, missing file in a multi-file list

New fixture - tests/behat/fixtures/dropzone.html

  • Minimal HTML page with a .dropzone primary target and a #secondary-zone secondary target; JavaScript listeners append dropped filenames to separate output lists and increment a shared event counter, letting scenarios assert both which files landed where and how many drop events fired

Registration - tests/behat/bootstrap/FeatureContext.php

  • Imported and applied DropzoneTrait alongside the existing generic traits

Docs - STEPS.md, README.md

  • Regenerated by ahoy update-docs; DropzoneTrait entry added to the trait table in both files

Before / After

BEFORE (Mink attachFile - sequential, one event per file)
─────────────────────────────────────────────────────────
  attachFile("document.pdf")  →  upload A starts → upload A finishes
  attachFile("image.png")     →  upload B starts → upload B finishes
  attachFile("text.txt")      →  upload C starts → upload C finishes

  Event sequence: input:change, input:change, input:change
  Concurrent upload window: NONE (each file finishes before next starts)
  Race conditions reproduced: NO

AFTER (DropzoneTrait - all files in one drop event)
───────────────────────────────────────────────────
  dropzoneDropFiles(".dropzone", ["document.pdf", "image.png", "text.txt"])
    │
    ├── inject hidden <input id="holder_0"> → attachFile → File A
    ├── inject hidden <input id="holder_1"> → attachFile → File B
    ├── inject hidden <input id="holder_2"> → attachFile → File C
    │
    └── dt = new DataTransfer()
        dt.items.add(File A)
        dt.items.add(File B)
        dt.items.add(File C)
        target.dispatchEvent(new DragEvent('drop', { dataTransfer: dt }))
        (remove holder inputs)

  Event sequence: drop (dataTransfer.files.length === 3)
  Concurrent upload window: YES - all three files arrive together
  Race conditions reproduced: YES

DropzoneTrait Implementation

Overview

Added a new Behat trait that simulates real multi-file drag-and-drop gestures by firing a single native drop event with all files in a DataTransfer, enabling tests of concurrent-upload code paths that Mink's sequential attachFile cannot reproduce.

Step Definitions

Two new When steps (compliant with CONTRIBUTING.md):

  • When I drop the file :path on the :selector dropzone — single-file convenience wrapper
  • When I drop the following files on the :selector dropzone: — multi-file step accepting a table with one fixture path per row

Placeholders, phrasing (the following), action-verb When I drop, tuple format, and method naming (trait-prefixed) conform to the repository's step rules. No CONTRIBUTING.md violations found.

Implementation Details

  • New trait: src/DropzoneTrait.php
    • dropzoneDropFile(string $path, string $selector): delegates to the multi-file step.
    • dropzoneDropFiles(string $selector, TableNode $paths): resolves fixture paths, injects temporary off-screen <input type="file"> holders, uses Mink's attachFileToField to populate each holder, aggregates holder.files into a single DataTransfer, dispatches one browser-native DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }) on the resolved CSS target, and removes holders.
    • dropzoneResolvePath(string $path): trims and validates input, requires a configured Mink files_path, canonicalizes files_path with realpath() and validates it is a directory, constructs the full path, checks file existence, canonicalizes the fixture path and rejects paths outside the canonical files_path directory (prevents path traversal). Throws clear RuntimeExceptions for empty paths, missing/invalid files_path, missing fixtures, and out-of-bounds fixtures.
  • All DOM/JS interactions run via Session::executeScript; temporary inputs are positioned off-screen (fixed left -9999px, opacity 0).
  • Throws ElementNotFoundException when the CSS target is missing at resolution time and throws a JS Error if the target disappears between resolution and dispatch.

Tests & Fixtures

  • tests/behat/features/dropzone.feature: positive scenarios (single-file, multi-file verifying a single drop event, non-default selector, consecutive drops) and negative @trait scenarios (missing target, missing single fixture, missing file in multi-file list).
  • tests/behat/fixtures/dropzone.html: two drop zones, event counter, and JS handlers that read event.dataTransfer.files and append filenames.
  • tests/behat/bootstrap/FeatureContext.php: DropzoneTrait registered via use statement.

Documentation & Misc

  • README.md: added DropzoneTrait to the generic steps index.
  • STEPS.md: added full DropzoneTrait documentation and examples.
  • Docs regenerated via ahoy update-docs.

Scope & Acceptance

  • Trait only simulates the drop gesture; assertions about Dropzone behavior remain the consumer's responsibility.
  • No folder-drop support, no retry/wait semantics.
  • Acceptance criteria met: new src/DropzoneTrait.php with two #[When(...)] methods, robust fixture-path resolution and canonicalization, clear exceptions for missing targets/fixtures, feature tests (positive and negative), and documentation updates.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f9a341c2-094f-456b-b292-c6424e8c2227

📥 Commits

Reviewing files that changed from the base of the PR and between 7df4a20 and 247b414.

📒 Files selected for processing (1)
  • src/DropzoneTrait.php

Walkthrough

This PR introduces DropzoneTrait, a new Behat trait for testing real multi-file drag-and-drop operations by dispatching a single native drop event with multiple files in DataTransfer. The implementation includes fixture-path resolution, test infrastructure, comprehensive scenarios, and documentation.

Changes

DropzoneTrait: Multi-file drag-and-drop simulation

Layer / File(s) Summary
DropzoneTrait core implementation
src/DropzoneTrait.php
New trait with dropzoneDropFile (single-file wrapper), dropzoneDropFiles (multi-file drop with DataTransfer dispatch), and dropzoneResolvePath (fixture path validation against files_path base directory). Validates selectors and file existence; removes temporary file inputs after dispatch.
Test fixture and integration
tests/behat/fixtures/dropzone.html, tests/behat/bootstrap/FeatureContext.php
HTML fixture with primary and secondary drop zones, event counter, and drop/dragover handlers that render dropped file names. FeatureContext integrates DropzoneTrait via import and use statement.
Feature test scenarios
tests/behat/features/dropzone.feature
Positive scenarios validate single-file and multi-file drops, selector handling (default .dropzone and #secondary-zone), output rendering, and event counting. Negative scenarios assert failures when drop target or fixture files are missing.
Documentation updates
README.md, STEPS.md
README adds index entry; STEPS.md adds full DropzoneTrait section with step definitions for single-file and multi-file drop steps and behavior documentation.

Sequence Diagram

sequenceDiagram
  participant Scenario as Test Scenario
  participant Trait as DropzoneTrait
  participant Resolver as dropzoneResolvePath
  participant Mink as Mink Session
  participant Browser as Browser
  participant Fixture as Dropzone Fixture
  
  Scenario->>Trait: dropzoneDropFiles(selector, paths)
  Trait->>Trait: Validate target selector exists
  
  loop For each fixture path
    Trait->>Resolver: Resolve path against files_path
    Resolver-->>Trait: Absolute path (or exception)
    Trait->>Mink: Create hidden file input
    Mink->>Mink: Attach fixture file
  end
  
  Trait->>Browser: Build DataTransfer with all files
  Trait->>Browser: Dispatch DragEvent('drop')
  Browser->>Fixture: drop event received
  Fixture->>Fixture: Increment event counter
  Fixture->>Fixture: Render dropped file names
  
  Trait->>Mink: Remove temporary file inputs
  Trait-->>Scenario: Complete
Loading

🎯 3 (Moderate) | ⏱️ ~22 minutes

🐰 A dropzone springs to life with cheer,
Files tumble in, all gathered near,
One gentle drop, a single song,
Tests hum happy — nothing's wrong,
Behat hops forward, quick and clear.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and accurately summarizes the main change: adding a DropzoneTrait for multi-file drag-and-drop functionality.
Linked Issues check ✅ Passed All primary objectives from issue #647 are met: two When steps with tuple annotations, fixture-path resolution against files_path, clear exceptions for missing targets/files, multi-file drag-and-drop via single DragEvent, feature tests with positive/negative cases, trait subprocess tests, documentation regeneration, and linting.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #647: DropzoneTrait implementation, documentation updates, feature tests, and test fixtures. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/647-dropzone-trait

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/DropzoneTrait.php`:
- Around line 156-165: The code uses realpath($files_path) cast to string and
proceeds even if realpath returned FALSE, letting $base be empty and bypassing
the containment check; update the logic in DropzoneTrait around the
$base/$full_path/$resolved checks to first validate that realpath($files_path)
!== FALSE and throw a clear RuntimeException if it fails, then compute $base =
rtrim(realpath($files_path), DIRECTORY_SEPARATOR), build $full_path and use
realpath($full_path) to compute $resolved, and finally keep the existing
containment guard using str_starts_with($resolved, $base . DIRECTORY_SEPARATOR)
to ensure fixture files cannot escape files_path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 80fa656a-1a83-406f-860a-a57531eb1a64

📥 Commits

Reviewing files that changed from the base of the PR and between a230840 and 7df4a20.

📒 Files selected for processing (6)
  • README.md
  • STEPS.md
  • src/DropzoneTrait.php
  • tests/behat/bootstrap/FeatureContext.php
  • tests/behat/features/dropzone.feature
  • tests/behat/fixtures/dropzone.html

Comment thread src/DropzoneTrait.php Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

❌ Patch coverage is 90.47619% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.59%. Comparing base (a230840) to head (247b414).

Files with missing lines Patch % Lines
src/DropzoneTrait.php 90.47% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #652      +/-   ##
==========================================
- Coverage   96.66%   96.59%   -0.08%     
==========================================
  Files          45       46       +1     
  Lines        3481     3523      +42     
==========================================
+ Hits         3365     3403      +38     
- Misses        116      120       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

Add 'DropzoneTrait' with multi-file drag-and-drop step.

1 participant