Skip to content

Commit

Permalink
[7.x] Keyboard Support Improvements (#1053)
Browse files Browse the repository at this point in the history
* wip

Signed-off-by: Mior Muhammad Zaki <[email protected]>

* Apply fixes from StyleCI

* wip

Signed-off-by: Mior Muhammad Zaki <[email protected]>

* wip

Signed-off-by: Mior Muhammad Zaki <[email protected]>

* Apply fixes from StyleCI

* wip

Signed-off-by: Mior Muhammad Zaki <[email protected]>

* wip

Signed-off-by: Mior Muhammad Zaki <[email protected]>

* wip

Signed-off-by: Mior Muhammad Zaki <[email protected]>

* wip

Signed-off-by: Mior Muhammad Zaki <[email protected]>

* wip

Signed-off-by: Mior Muhammad Zaki <[email protected]>

* wip

Signed-off-by: Mior Muhammad Zaki <[email protected]>

* formatting

---------

Signed-off-by: Mior Muhammad Zaki <[email protected]>
Co-authored-by: StyleCI Bot <[email protected]>
Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
3 people authored Sep 4, 2023
1 parent e9653cd commit d9c722c
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 23 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/vendor
composer.lock
/phpunit.xml
.phpunit.cache/*
.phpunit.result.cache
25 changes: 2 additions & 23 deletions src/Concerns/InteractsWithElements.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\Remote\LocalFileDetector;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverKeys;
use Facebook\WebDriver\WebDriverSelect;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

trait InteractsWithElements
{
use InteractsWithKeyboard;

/**
* Get all of the elements matching the given selector.
*
Expand Down Expand Up @@ -113,27 +113,6 @@ public function keys($selector, ...$keys)
return $this;
}

/**
* Parse the keys before sending to the keyboard.
*
* @param array $keys
* @return array
*/
protected function parseKeys($keys)
{
return collect($keys)->map(function ($key) {
if (is_string($key) && Str::startsWith($key, '{') && Str::endsWith($key, '}')) {
$key = constant(WebDriverKeys::class.'::'.strtoupper(trim($key, '{}')));
}

if (is_array($key) && Str::startsWith($key[0], '{')) {
$key[0] = constant(WebDriverKeys::class.'::'.strtoupper(trim($key[0], '{}')));
}

return $key;
})->all();
}

/**
* Type the given value in the given field.
*
Expand Down
42 changes: 42 additions & 0 deletions src/Concerns/InteractsWithKeyboard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Laravel\Dusk\Concerns;

use Facebook\WebDriver\WebDriverKeys;
use Illuminate\Support\Str;
use Laravel\Dusk\Keyboard;

trait InteractsWithKeyboard
{
/**
* Execute the given callback while interacting with the keyboard.
*
* @param callable(\Laravel\Dusk\Keyboard):void $callback
* @return $this
*/
public function withKeyboard(callable $callback)
{
return tap($this, fn () => $callback(new Keyboard($this)));
}

/**
* Parse the keys before sending to the keyboard.
*
* @param array $keys
* @return array
*/
protected function parseKeys($keys)
{
return collect($keys)->map(function ($key) {
if (is_string($key) && Str::startsWith($key, '{') && Str::endsWith($key, '}')) {
$key = constant(WebDriverKeys::class.'::'.strtoupper(trim($key, '{}')));
}

if (is_array($key) && Str::startsWith($key[0], '{')) {
$key[0] = constant(WebDriverKeys::class.'::'.strtoupper(trim($key[0], '{}')));
}

return $key;
})->all();
}
}
20 changes: 20 additions & 0 deletions src/Concerns/InteractsWithMouse.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverKeys;
use Laravel\Dusk\Keyboard;
use Laravel\Dusk\OperatingSystem;

trait InteractsWithMouse
{
Expand Down Expand Up @@ -153,6 +156,23 @@ public function rightClick($selector = null)
return $this;
}

/**
* Control click the element at the given selector.
*
* @param string|null $selector
* @return $this
*/
public function controlClick($selector = null)
{
return $this->withKeyboard(function (Keyboard $keyboard) use ($selector) {
$key = OperatingSystem::onMac() ? WebDriverKeys::META : WebDriverKeys::CONTROL;

$keyboard->press($key);
$this->click($selector);
$keyboard->release($key);
});
}

/**
* Release the currently clicked mouse button.
*
Expand Down
101 changes: 101 additions & 0 deletions src/Keyboard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace Laravel\Dusk;

use BadMethodCallException;
use Illuminate\Support\Traits\Macroable;

/**
* @mixin \Facebook\WebDriver\Remote\RemoteKeyboard
*/
class Keyboard
{
use Macroable {
__call as macroCall;
}

/**
* The browser instance.
*
* @var \Laravel\Dusk\Browser
*/
public $browser;

/**
* Create a keyboard instance.
*
* @param \Laravel\Dusk\Browser $browser
* @return void
*/
public function __construct(Browser $browser)
{
$this->browser = $browser;
}

/**
* Press the key using keyboard.
*
* @return $this
*/
public function press($key)
{
$this->pressKey($key);

return $this;
}

/**
* Release the given pressed key.
*
* @return $this
*/
public function release($key)
{
$this->releaseKey($key);

return $this;
}

/**
* Type the given keys using keyboard.
*
* @param string|array<int, string> $keys
* @return $this
*/
public function type($keys)
{
$this->sendKeys($keys);

return $this;
}

/**
* Dynamically call a method on the keyboard.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}

$keyboard = $this->browser->driver->getKeyboard();

if (method_exists($keyboard, $method)) {
$response = $keyboard->{$method}(...$parameters);

if ($response === $keyboard) {
return $this;
} else {
return $response;
}
}

throw new BadMethodCallException("Call to undefined keyboard method [{$method}].");
}
}
19 changes: 19 additions & 0 deletions tests/BrowserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

namespace Laravel\Dusk\Tests;

use Facebook\WebDriver\Remote\RemoteKeyboard;
use Facebook\WebDriver\Remote\WebDriverBrowserType;
use Facebook\WebDriver\WebDriverKeys;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Keyboard;
use Laravel\Dusk\Page;
use Mockery as m;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -192,6 +195,22 @@ public function test_disable_console()
$browser->storeConsoleLog('file');
}

public function test_uses_keyboard()
{
$driver = m::mock(stdClass::class);
$remoteKeyboard = m::mock(RemoteKeyboard::class);
$driver->shouldReceive('getKeyboard')->once()->andReturn($remoteKeyboard);
$remoteKeyboard->shouldReceive('pressKey')->once()->with(WebDriverKeys::CONTROL)->andReturnNull();
$browser = new Browser($driver);

$browser->withKeyboard(function ($keyboard) use ($browser) {
$this->assertInstanceof(Keyboard::class, $keyboard);
$this->assertSame($browser, $keyboard->browser);

$keyboard->press(WebDriverKeys::CONTROL);
});
}

public function test_screenshot()
{
$this->driver->shouldReceive('takeScreenshot')->andReturnUsing(function ($filePath) {
Expand Down

0 comments on commit d9c722c

Please sign in to comment.