Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions tests/Js/OutsideViewportTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

namespace Behat\Mink\Tests\Driver\Js;

use Behat\Mink\Element\NodeElement;
use Behat\Mink\Tests\Driver\TestCase;

/**
* Mouse interaction with elements that are outside the viewport.
*/
final class OutsideViewportTest extends TestCase
{
/**
* @group mouse-events
*/
public function testClick(): void
{
$element = $this->findOutsideViewport('click-target');

$this->scrollToTop();
$element->click();

$this->assertElementText('clicked', $element);
}

/**
* @group mouse-events
*/
public function testDoubleClick(): void
{
$element = $this->findOutsideViewport('double-click-target');

$this->scrollToTop();
$element->doubleClick();

$this->assertElementText('double clicked', $element);
}

/**
* @group mouse-events
*/
public function testRightClick(): void
{
$element = $this->findOutsideViewport('right-click-target');

$this->scrollToTop();
$element->rightClick();

$this->assertElementText('right clicked', $element);
}

/**
* @group mouse-events
*/
public function testMouseOver(): void
{
$element = $this->findOutsideViewport('mouse-over-target');

$this->scrollToTop();
$element->mouseOver();

$this->assertElementText('mouse overed', $element);
}

private function findOutsideViewport(string $id): NodeElement
{
$this->getSession()->visit($this->pathTo('/outside_viewport.html'));

$this->scrollToTop();
$this->assertTrue(
$this->getSession()->evaluateScript(
'document.getElementById("' . $id . '").getBoundingClientRect().top >= window.innerHeight'
),
'The element must be outside the viewport before the test'
);

return $this->findById($id);
}

private function scrollToTop(): void
{
$this->getSession()->executeScript('window.scrollTo(0, 0);');
}

private function assertElementText(string $expected, NodeElement $element): void
{
$this->scrollToTop();
$this->assertSame($expected, $element->getText());
}
}
37 changes: 37 additions & 0 deletions web-fixtures/outside_viewport.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<title>Outside viewport</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
<body>
<div style="height: 3000px;"></div>

<!-- in a row, so the mouse is never already over the next target -->
<div style="display: flex; gap: 20px;">
<div id="click-target">not clicked</div>
<div id="double-click-target">not double clicked</div>
<div id="right-click-target">not right clicked</div>
<div id="mouse-over-target">not mouse overed</div>
</div>

<script type="text/javascript">
document.getElementById('click-target').addEventListener('click', function () {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just thinking out loud, could it make sense to use pointer events (which also capture touch etc) instead? Or do we maybe specifically want these mouse events?

I guess it's more of a question on the mink side - that touch events should be a different API?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@uuf6429, we'd better create an issue in Mink repo itself for discussing such API.

Some ideas:

  • understand underlying driver support for pointer events?
  • how are js events triggered (does mouse* event trigger matching pointer* event and vice versa)?
  • how do mouse/pointer events behave on desktop/mobile devices?
  • what about community-built Mink drivers (Chrome, Playwright, etc.)?

What I try to avoid here is by making the current mouse-api acting as point event API causing a break for desktop users. If we do introduce parallel pointer-api, then could drivers actually handle it without Syn-based magic.

this.textContent = 'clicked';
});

document.getElementById('double-click-target').addEventListener('dblclick', function () {
this.textContent = 'double clicked';
});

document.getElementById('right-click-target').addEventListener('contextmenu', function (event) {
event.preventDefault();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm wondering if we should add preventDefault()/stopPropagation() everywhere.
Some browsers, like MS Edge show a menu context menu when text is selected via double click, for instance.

@aik099 aik099 Oct 19, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@uuf6429, this deserves a separate task/pr with research attached. Test suite doesn't really fail without them, but to avoid any future failing tests due to unexpected event propagation, this could be handy.

this.textContent = 'right clicked';
});

document.getElementById('mouse-over-target').addEventListener('mouseover', function () {
this.textContent = 'mouse overed';
});
</script>
</body>
</html>
Loading