Skip to content

Commit

Permalink
Added mouse API
Browse files Browse the repository at this point in the history
  • Loading branch information
gsouf committed Jun 22, 2018
1 parent f08f6a4 commit 684e6fc
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 8 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
* Bug fixes:
* none


## 0.2.2

> *20xx-xx-xx* (not released)
> Added input controls
* Features:
* Added mouse api (move, click)
* Bug fixes:
* none

## 0.2.1

> *2018-06-20*
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,20 @@ You can use the option "clip" in order to choose an area for the screenshot.
$screenshot->saveToFile('/some/place/file.jpg');
```

### Mouse API

The mouse API is dependent on the page instance and allows you to control the mouse's moves and clicks.

```php
$page->mouse()
->move(10, 20) // Moves mouse to position x=10;y=20
->click() // left click on position set above
->move(100, 200, ['steps' => 5]) // move mouse to x=100;y=200 in 5 equal steps
->click(['button' => Mouse::BUTTON_RIGHT]; // right click on position set above

// given the last click was on a link, the next step will wait for the page to load after the link was clicked
$page->waitForReload();
```


------------------------------------------------------------------------------------------------------------------------
Expand Down
125 changes: 125 additions & 0 deletions src/Input/Mouse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
/**
* @license see LICENSE
*/

namespace HeadlessChromium\Input;

use HeadlessChromium\Communication\Message;
use HeadlessChromium\Page;

class Mouse
{

const BUTTON_LEFT = 'left';
const BUTTON_NONE = 'none';
const BUTTON_RIGHT = 'right';
const BUTTON_MIDDLE = 'middle';

/**
* @var Page
*/
protected $page;

protected $x = 0;
protected $y = 0;

protected $button = self::BUTTON_NONE;

/**
* @param Page $page
*/
public function __construct(Page $page)
{
$this->page = $page;
}

/**
* @param int $x
* @param int $y
* @param array|null $options
* @return $this
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*/
public function move(int $x, int $y, array $options = null)
{
$this->page->assertNotClosed();

// get origin of the move
$originX = $this->x;
$originY = $this->y;

// set new position after move
$this->x = $x;
$this->y = $y;

// number of steps to achieve the move
$steps = $options['steps'] ?? 1;
if ($steps <= 0) {
throw new \InvalidArgumentException('options "steps" for mouse move must be a positive integer');
}

// move
for ($i = 1; $i <= $steps; $i++) {
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchMouseEvent', [
'x' => $originX + ($this->x - $originX) * ($i / $steps),
'y' => $originY + ($this->y - $originY) * ($i / $steps),
'type' => 'mouseMoved'
]));
}

return $this;
}

/**
* @param $options
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*/
public function press(array $options = null)
{
$this->page->assertNotClosed();
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchMouseEvent', [
'x' => $this->x,
'y' => $this->y,
'type' => 'mousePressed',
'button' => $options['button'] ?? self::BUTTON_LEFT,
'clickCount' => 1
]));

return $this;
}

/**
* @param $options
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*/
public function release(array $options = null)
{
$this->page->assertNotClosed();
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchMouseEvent', [
'x' => $this->x,
'y' => $this->y,
'type' => 'mouseReleased',
'button' => $options['button'] ?? self::BUTTON_LEFT,
'clickCount' => 1
]));

return $this;
}

/**
* @param array|null $options
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*/
public function click(array $options = null)
{
$this->press($options);
$this->release($options);

return $this;
}
}
33 changes: 25 additions & 8 deletions src/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@
namespace HeadlessChromium;

use HeadlessChromium\Communication\Message;
use HeadlessChromium\Communication\ResponseReader;
use HeadlessChromium\Communication\Session;
use HeadlessChromium\Communication\Target;
use HeadlessChromium\Exception\CommunicationException;
use HeadlessChromium\Exception\NoResponseAvailable;
use HeadlessChromium\Exception\CommunicationException\ResponseHasError;
use HeadlessChromium\Exception\TargetDestroyed;
use HeadlessChromium\Input\Mouse;
use HeadlessChromium\PageUtils\PageEvaluation;
use HeadlessChromium\PageUtils\PageNavigation;
use HeadlessChromium\PageUtils\PageScreenshot;
Expand All @@ -35,6 +34,11 @@ class Page
*/
protected $frameManager;

/**
* @var Mouse|Null
*/
protected $mouse;

public function __construct(Target $target, array $frameTree)
{
$this->target = $target;
Expand Down Expand Up @@ -65,9 +69,6 @@ public function getSession(): Session
/**
* @param $url
* @return PageNavigation
*
* @throws NoResponseAvailable
* @throws CommunicationException
*/
public function navigate($url)
{
Expand Down Expand Up @@ -147,11 +148,14 @@ public function hasLifecycleEvent(string $event): bool

/**
* Wait for the page to unload
*
* @param string $eventName
* @param int $timeout
* @param null $loaderId
* @return $this
* @throws CommunicationException\CannotReadResponse
* @throws CommunicationException\InvalidResponse
* @throws Exception\OperationTimedOut
*
* @return $this
*/
public function waitForReload($eventName = Page::LOAD, $timeout = 30000, $loaderId = null)
{
Expand Down Expand Up @@ -333,6 +337,19 @@ public function setViewport(int $width, int $height)
]);
}

/**
* Get mouse object to play with
* @return Mouse
*/
public function mouse()
{
if (!$this->mouse) {
$this->mouse = new Mouse($this);
}

return $this->mouse;
}

/**
* Request to close the page
* @throws CommunicationException
Expand All @@ -357,7 +374,7 @@ public function close()
/**
* Throws if the page was closed
*/
private function assertNotClosed()
public function assertNotClosed()
{
if ($this->target->isDestroyed()) {
throw new TargetDestroyed('The page was closed and is not available anymore.');
Expand Down
12 changes: 12 additions & 0 deletions test/resources/static-web/b.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<html>
<head>
<title>b - test</title>
</head>
<body>
<h1>page b</h1>
<div>b</div>

<a id="a" href="./a.html">go to a</a>

</body>
</html>
63 changes: 63 additions & 0 deletions test/suites/MouseApiTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
/**
* @license see LICENSE
*/

namespace HeadlessChromium\Test;

use HeadlessChromium\Browser;
use HeadlessChromium\BrowserFactory;

/**
* @covers \HeadlessChromium\Browser
* @covers \HeadlessChromium\Page
*/
class MouseApiTest extends BaseTestCase
{

/**
* @var Browser\ProcessAwareBrowser
*/
public static $browser;

public static function setUpBeforeClass()
{
parent::setUpBeforeClass();
$factory = new BrowserFactory();
self::$browser = $factory->createBrowser();
}

public static function tearDownAfterClass()
{
parent::tearDownAfterClass();
self::$browser->close();
}

private function openSitePage($file)
{
$page = self::$browser->createPage();
$page->navigate($this->sitePath($file))->waitForNavigation();

return $page;
}

/**
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*/
public function testClickLink()
{
// initial navigation
$page = $this->openSitePage('b.html');
$rect = $page
->evaluate('JSON.parse(JSON.stringify(document.querySelector("#a").getBoundingClientRect()));')
->getReturnValue();

$page->mouse()->move($rect['x'], $rect['y'])->click();
$page->waitForReload();

$title = $page->evaluate('document.title')->getReturnValue();

$this->assertEquals('a - test', $title);
}
}

0 comments on commit 684e6fc

Please sign in to comment.