diff --git a/src/tests/Browser.php b/src/tests/Browser.php new file mode 100644 --- /dev/null +++ b/src/tests/Browser.php @@ -0,0 +1,118 @@ +elements($selector); + $count = count($elements); + + if ($visible) { + foreach ($elements as $element) { + if (!$element->isDisplayed()) { + $count--; + } + } + } + + Assert::assertEquals($expected_count, $count); + + return $this; + } + + /** + * Assert that the given element has specified class assigned. + */ + public function assertHasClass($selector, $class_name) + { + $element = $this->resolver->findOrFail($selector); + $classes = explode(' ', (string) $element->getAttribute('class')); + + Assert::assertContains($class_name, $classes); + + return $this; + } + + /** + * Check if in Phone mode + */ + public static function isPhone() + { + return getenv('TESTS_MODE') == 'phone'; + } + + /** + * Check if in Tablet mode + */ + public static function isTablet() + { + return getenv('TESTS_MODE') == 'tablet'; + } + + /** + * Check if in Desktop mode + */ + public static function isDesktop() + { + return !self::isPhone() && !self::isTablet(); + } + + /** + * Returns content of a downloaded file + */ + public function readDownloadedFile($filename) + { + $filename = __DIR__ . "/Browser/downloads/$filename"; + + // Give the browser a chance to finish download + if (!file_exists($filename)) { + sleep(2); + } + + Assert::assertFileExists($filename); + + return file_get_contents($filename); + } + + /** + * Removes downloaded file + */ + public function removeDownloadedFile($filename) + { + @unlink(__DIR__ . "/Browser/downloads/$filename"); + + return $this; + } + + /** + * Execute code within body context. + * Useful to execute code that selects elements outside of a component context + */ + public function withinBody($callback) + { + if ($this->resolver->prefix != 'body') { + $orig_prefix = $this->resolver->prefix; + $this->resolver->prefix = 'body'; + } + + call_user_func($callback, $this); + + if (isset($orig_prefix)) { + $this->resolver->prefix = $orig_prefix; + } + + return $this; + } +} diff --git a/src/tests/Browser/Components/Error.php b/src/tests/Browser/Components/Error.php --- a/src/tests/Browser/Components/Error.php +++ b/src/tests/Browser/Components/Error.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Components; -use Laravel\Dusk\Browser; use Laravel\Dusk\Component as BaseComponent; use PHPUnit\Framework\Assert as PHPUnit; @@ -33,11 +32,11 @@ /** * Assert that the browser page contains the component. * - * @param Browser $browser + * @param \Laravel\Dusk\Browser $browser * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->waitFor($this->selector()) ->assertSeeIn('@code', $this->code) diff --git a/src/tests/Browser/Components/ListInput.php b/src/tests/Browser/Components/ListInput.php --- a/src/tests/Browser/Components/ListInput.php +++ b/src/tests/Browser/Components/ListInput.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Components; -use Laravel\Dusk\Browser; use Laravel\Dusk\Component as BaseComponent; use PHPUnit\Framework\Assert as PHPUnit; @@ -29,11 +28,11 @@ /** * Assert that the browser page contains the component. * - * @param Browser $browser + * @param \Laravel\Dusk\Browser $browser * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { // $list = explode("\n", $browser->value($this->selector)); @@ -60,7 +59,7 @@ /** * Assert list input content */ - public function assertListInputValue(Browser $browser, array $list) + public function assertListInputValue($browser, array $list) { if (empty($list)) { $browser->assertMissing('.input-group:not(:first-child)'); @@ -76,7 +75,7 @@ /** * Add list entry */ - public function addListEntry(Browser $browser, string $value) + public function addListEntry($browser, string $value) { $browser->type('@input', $value) ->click('@add-btn') @@ -86,7 +85,7 @@ /** * Remove list entry */ - public function removeListEntry(Browser $browser, int $num) + public function removeListEntry($browser, int $num) { $selector = '.input-group:nth-child(' . ($num + 1) . ') a.btn'; $browser->click($selector)->assertMissing($selector); @@ -95,7 +94,7 @@ /** * Assert an error message on the widget */ - public function assertFormError(Browser $browser, int $num, string $msg, bool $focused = false) + public function assertFormError($browser, int $num, string $msg, bool $focused = false) { $selector = '.input-group:nth-child(' . ($num + 1) . ') input.is-invalid'; diff --git a/src/tests/Browser/Components/Menu.php b/src/tests/Browser/Components/Menu.php --- a/src/tests/Browser/Components/Menu.php +++ b/src/tests/Browser/Components/Menu.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Components; -use Laravel\Dusk\Browser; use Laravel\Dusk\Component as BaseComponent; use PHPUnit\Framework\Assert as PHPUnit; @@ -21,11 +20,11 @@ /** * Assert that the browser page contains the component. * - * @param Browser $browser + * @param \Laravel\Dusk\Browser $browser * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->assertVisible($this->selector()); $browser->assertVisible('@brand'); @@ -34,12 +33,12 @@ /** * Assert that menu contains only specified menu items. * - * @param Browser $browser - * @param array $items List of menu items + * @param \Laravel\Dusk\Browser $browser + * @param array $items List of menu items * * @return void */ - public function assertMenuItems(Browser $browser, array $items) + public function assertMenuItems($browser, array $items) { // TODO: On mobile the links will not be visible @@ -54,12 +53,12 @@ /** * Assert that specified menu item is active * - * @param Browser $browser - * @param string $item Menu item name + * @param \Laravel\Dusk\Browser $browser + * @param string $item Menu item name * * @return void */ - public function assertActiveItem(Browser $browser, string $item) + public function assertActiveItem($browser, string $item) { // TODO: On mobile the links will not be visible diff --git a/src/tests/Browser/Components/Toast.php b/src/tests/Browser/Components/Toast.php --- a/src/tests/Browser/Components/Toast.php +++ b/src/tests/Browser/Components/Toast.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Components; -use Laravel\Dusk\Browser; use Laravel\Dusk\Component as BaseComponent; use PHPUnit\Framework\Assert as PHPUnit; @@ -35,11 +34,11 @@ /** * Assert that the browser page contains the component. * - * @param Browser $browser + * @param \Laravel\Dusk\Browser $browser The browser object * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->waitFor($this->selector()); $this->element = $browser->element($this->selector()); @@ -61,7 +60,7 @@ /** * Assert title of the toast element */ - public function assertToastTitle(Browser $browser, string $title) + public function assertToastTitle($browser, string $title) { if (empty($title)) { $browser->assertMissing('@title'); @@ -73,7 +72,7 @@ /** * Assert message of the toast element */ - public function assertToastMessage(Browser $browser, string $message) + public function assertToastMessage($browser, string $message) { $browser->assertSeeIn('@message', $message); } @@ -81,7 +80,7 @@ /** * Close the toast with a click */ - public function closeToast(Browser $browser) + public function closeToast($browser) { $this->element->click(); } diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php --- a/src/tests/Browser/DomainTest.php +++ b/src/tests/Browser/DomainTest.php @@ -4,6 +4,7 @@ use App\Domain; use App\User; +use Tests\Browser; use Tests\Browser\Components\Error; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Dashboard; @@ -11,7 +12,6 @@ use Tests\Browser\Pages\DomainList; use Tests\Browser\Pages\Home; use Tests\DuskTestCase; -use Laravel\Dusk\Browser; use Illuminate\Foundation\Testing\DatabaseMigrations; class DomainTest extends DuskTestCase @@ -23,7 +23,7 @@ public function testDomainInfoUnauth(): void { // Test that the page requires authentication - $this->browse(function (Browser $browser) { + $this->browse(function ($browser) { $browser->visit('/domain/123')->on(new Home()); }); } @@ -33,7 +33,7 @@ */ public function testDomainInfo404(): void { - $this->browse(function (Browser $browser) { + $this->browse(function ($browser) { // FIXME: I couldn't make loginAs() method working // Note: Here we're also testing that unauthenticated request @@ -44,7 +44,7 @@ // TODO: the check below could look simpler, but we can't // just remove the callback argument. We'll create // Browser wrapper in future, then we could create expectError() method - ->with(new Error('404'), function (Browser $browser) { + ->with(new Error('404'), function ($browser) { }); }); } @@ -56,7 +56,7 @@ */ public function testDomainInfo(): void { - $this->browse(function (Browser $browser) { + $this->browse(function ($browser) { // Unconfirmed domain $domain = Domain::where('namespace', 'kolab.org')->first(); $domain->status ^= Domain::STATUS_CONFIRMED; @@ -64,7 +64,7 @@ $browser->visit('/domain/' . $domain->id) ->on(new DomainInfo()) - ->whenAvailable('@verify', function (Browser $browser) use ($domain) { + ->whenAvailable('@verify', function ($browser) use ($domain) { // Make sure the domain is confirmed now // TODO: Test verification process failure $domain->status |= Domain::STATUS_CONFIRMED; @@ -74,11 +74,11 @@ ->assertSeeIn('pre', $domain->hash()) ->click('button'); }) - ->whenAvailable('@config', function (Browser $browser) use ($domain) { + ->whenAvailable('@config', function ($browser) use ($domain) { $browser->assertSeeIn('pre', $domain->namespace); }) ->assertMissing('@verify') - ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) { + ->with(new Toast(Toast::TYPE_SUCCESS), function ($browser) { $browser->assertToastTitle('') ->assertToastMessage('Domain verified successfully') ->closeToast(); @@ -98,7 +98,7 @@ public function testDomainListUnauth(): void { // Test that the page requires authentication - $this->browse(function (Browser $browser) { + $this->browse(function ($browser) { $browser->visit('/logout') ->visit('/domains') ->on(new Home()); @@ -112,7 +112,7 @@ */ public function testDomainList(): void { - $this->browse(function (Browser $browser) { + $this->browse(function ($browser) { // Login the user $browser->visit('/login') ->on(new Home()) @@ -127,7 +127,7 @@ ->click('@table tbody tr:first-child td:first-child a') // On Domain Info page verify that's the clicked domain ->on(new DomainInfo()) - ->whenAvailable('@config', function (Browser $browser) { + ->whenAvailable('@config', function ($browser) { $browser->assertSeeIn('pre', 'kolab.org'); }); }); diff --git a/src/tests/Browser/ErrorTest.php b/src/tests/Browser/ErrorTest.php --- a/src/tests/Browser/ErrorTest.php +++ b/src/tests/Browser/ErrorTest.php @@ -2,8 +2,8 @@ namespace Tests\Browser; +use Tests\Browser; use Tests\DuskTestCase; -use Laravel\Dusk\Browser; class ErrorTest extends DuskTestCase { diff --git a/src/tests/Browser/LogonTest.php b/src/tests/Browser/LogonTest.php --- a/src/tests/Browser/LogonTest.php +++ b/src/tests/Browser/LogonTest.php @@ -2,12 +2,12 @@ namespace Tests\Browser; +use Tests\Browser; use Tests\Browser\Components\Menu; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Dashboard; use Tests\Browser\Pages\Home; use Tests\DuskTestCase; -use Laravel\Dusk\Browser; use Illuminate\Foundation\Testing\DatabaseMigrations; class LogonTest extends DuskTestCase diff --git a/src/tests/Browser/Pages/Dashboard.php b/src/tests/Browser/Pages/Dashboard.php --- a/src/tests/Browser/Pages/Dashboard.php +++ b/src/tests/Browser/Pages/Dashboard.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Pages; -use Laravel\Dusk\Browser; use Laravel\Dusk\Page; class Dashboard extends Page @@ -20,10 +19,11 @@ /** * Assert that the browser is on the page. * - * @param \Laravel\Dusk\Browser $browser + * @param \Laravel\Dusk\Browser $browser The browser object + * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->assertPathIs('/dashboard') ->waitUntilMissing('@app .app-loader') diff --git a/src/tests/Browser/Pages/DomainInfo.php b/src/tests/Browser/Pages/DomainInfo.php --- a/src/tests/Browser/Pages/DomainInfo.php +++ b/src/tests/Browser/Pages/DomainInfo.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Pages; -use Laravel\Dusk\Browser; use Laravel\Dusk\Page; class DomainInfo extends Page @@ -20,11 +19,11 @@ /** * Assert that the browser is on the page. * - * @param \Laravel\Dusk\Browser $browser + * @param \Laravel\Dusk\Browser $browser The browser object * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->waitUntilMissing('@app .app-loader') ->assertPresent('@config,@verify'); diff --git a/src/tests/Browser/Pages/DomainList.php b/src/tests/Browser/Pages/DomainList.php --- a/src/tests/Browser/Pages/DomainList.php +++ b/src/tests/Browser/Pages/DomainList.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Pages; -use Laravel\Dusk\Browser; use Laravel\Dusk\Page; class DomainList extends Page @@ -20,11 +19,11 @@ /** * Assert that the browser is on the page. * - * @param \Laravel\Dusk\Browser $browser + * @param \Laravel\Dusk\Browser $browser The browser object * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->assertPathIs($this->url()) ->waitUntilMissing('@app .app-loader') diff --git a/src/tests/Browser/Pages/Home.php b/src/tests/Browser/Pages/Home.php --- a/src/tests/Browser/Pages/Home.php +++ b/src/tests/Browser/Pages/Home.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Pages; -use Laravel\Dusk\Browser; use Laravel\Dusk\Page; class Home extends Page @@ -20,10 +19,11 @@ /** * Assert that the browser is on the page. * - * @param \Laravel\Dusk\Browser $browser + * @param \Laravel\Dusk\Browser $browser The browser object + * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->assertPathIs($this->url()) ->assertVisible('form.form-signin'); @@ -44,14 +44,14 @@ /** * Submit logon form. * - * @param Browser $browser - * @param string $username - * @param string $password - * @param bool $wait_for_dashboard + * @param \Laravel\Dusk\Browser $browser The browser object + * @param string $username User name + * @param string $password User password + * @param bool $wait_for_dashboard * * @return void */ - public function submitLogon(Browser $browser, $username, $password, $wait_for_dashboard = false) + public function submitLogon($browser, $username, $password, $wait_for_dashboard = false) { $browser ->type('#inputEmail', $username) diff --git a/src/tests/Browser/Pages/PasswordReset.php b/src/tests/Browser/Pages/PasswordReset.php --- a/src/tests/Browser/Pages/PasswordReset.php +++ b/src/tests/Browser/Pages/PasswordReset.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Pages; -use Laravel\Dusk\Browser; use Laravel\Dusk\Page; class PasswordReset extends Page @@ -20,11 +19,11 @@ /** * Assert that the browser is on the page. * - * @param \Laravel\Dusk\Browser $browser + * @param \Laravel\Dusk\Browser $browser The browser object * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->assertPathIs('/password-reset'); $browser->assertPresent('@step1'); diff --git a/src/tests/Browser/Pages/Signup.php b/src/tests/Browser/Pages/Signup.php --- a/src/tests/Browser/Pages/Signup.php +++ b/src/tests/Browser/Pages/Signup.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Pages; -use Laravel\Dusk\Browser; use Laravel\Dusk\Page; class Signup extends Page @@ -20,11 +19,11 @@ /** * Assert that the browser is on the page. * - * @param \Laravel\Dusk\Browser $browser + * @param \Laravel\Dusk\Browser $browser The browser object * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->assertPathIs('/signup') ->assertPresent('@step0') diff --git a/src/tests/Browser/Pages/UserInfo.php b/src/tests/Browser/Pages/UserInfo.php --- a/src/tests/Browser/Pages/UserInfo.php +++ b/src/tests/Browser/Pages/UserInfo.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Pages; -use Laravel\Dusk\Browser; use Laravel\Dusk\Page; class UserInfo extends Page @@ -20,11 +19,11 @@ /** * Assert that the browser is on the page. * - * @param \Laravel\Dusk\Browser $browser + * @param \Laravel\Dusk\Browser $browser The browser object * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->waitFor('@form'); } diff --git a/src/tests/Browser/Pages/UserList.php b/src/tests/Browser/Pages/UserList.php --- a/src/tests/Browser/Pages/UserList.php +++ b/src/tests/Browser/Pages/UserList.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Pages; -use Laravel\Dusk\Browser; use Laravel\Dusk\Page; class UserList extends Page @@ -20,11 +19,11 @@ /** * Assert that the browser is on the page. * - * @param \Laravel\Dusk\Browser $browser + * @param \Laravel\Dusk\Browser $browser The browser object * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->assertPathIs($this->url()) ->waitUntilMissing('@app .app-loader') diff --git a/src/tests/Browser/Pages/UserProfile.php b/src/tests/Browser/Pages/UserProfile.php --- a/src/tests/Browser/Pages/UserProfile.php +++ b/src/tests/Browser/Pages/UserProfile.php @@ -2,7 +2,6 @@ namespace Tests\Browser\Pages; -use Laravel\Dusk\Browser; use Laravel\Dusk\Page; class UserProfile extends Page @@ -20,11 +19,11 @@ /** * Assert that the browser is on the page. * - * @param \Laravel\Dusk\Browser $browser + * @param \Laravel\Dusk\Browser $browser The browser object * * @return void */ - public function assert(Browser $browser) + public function assert($browser) { $browser->assertPathIs($this->url()) ->waitUntilMissing('@app .app-loader') diff --git a/src/tests/Browser/PasswordResetTest.php b/src/tests/Browser/PasswordResetTest.php --- a/src/tests/Browser/PasswordResetTest.php +++ b/src/tests/Browser/PasswordResetTest.php @@ -4,11 +4,11 @@ use App\User; use App\VerificationCode; +use Tests\Browser; use Tests\Browser\Pages\Dashboard; use Tests\Browser\Pages\Home; use Tests\Browser\Pages\PasswordReset; use Tests\DuskTestCase; -use Laravel\Dusk\Browser; use Illuminate\Foundation\Testing\DatabaseMigrations; class PasswordResetTest extends DuskTestCase diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php --- a/src/tests/Browser/SignupTest.php +++ b/src/tests/Browser/SignupTest.php @@ -5,12 +5,12 @@ use App\Domain; use App\SignupCode; use App\User; +use Tests\Browser; use Tests\Browser\Components\Menu; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Dashboard; use Tests\Browser\Pages\Signup; use Tests\DuskTestCase; -use Laravel\Dusk\Browser; use Illuminate\Foundation\Testing\DatabaseMigrations; class SignupTest extends DuskTestCase diff --git a/src/tests/Browser/UserProfileTest.php b/src/tests/Browser/UserProfileTest.php --- a/src/tests/Browser/UserProfileTest.php +++ b/src/tests/Browser/UserProfileTest.php @@ -3,12 +3,12 @@ namespace Tests\Browser; use App\User; +use Tests\Browser; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Dashboard; use Tests\Browser\Pages\Home; use Tests\Browser\Pages\UserProfile; use Tests\DuskTestCase; -use Laravel\Dusk\Browser; use Illuminate\Foundation\Testing\DatabaseMigrations; class UserProfileTest extends DuskTestCase diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php --- a/src/tests/Browser/UsersTest.php +++ b/src/tests/Browser/UsersTest.php @@ -4,6 +4,7 @@ use App\User; use App\UserAlias; +use Tests\Browser; use Tests\Browser\Components\ListInput; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Dashboard; @@ -11,7 +12,6 @@ use Tests\Browser\Pages\UserInfo; use Tests\Browser\Pages\UserList; use Tests\DuskTestCase; -use Laravel\Dusk\Browser; use Illuminate\Foundation\Testing\DatabaseMigrations; class UsersTest extends DuskTestCase diff --git a/src/tests/DuskTestCase.php b/src/tests/DuskTestCase.php --- a/src/tests/DuskTestCase.php +++ b/src/tests/DuskTestCase.php @@ -84,11 +84,45 @@ protected function driver() { $options = (new ChromeOptions())->addArguments([ + '--lang=en_US', '--disable-gpu', '--headless', '--window-size=1280,720', ]); + // For file download handling + $prefs = [ + 'profile.default_content_settings.popups' => 0, + 'download.default_directory' => __DIR__ . '/Browser/downloads', + ]; + + $options->setExperimentalOption('prefs', $prefs); + + if (getenv('TESTS_MODE') == 'phone') { + // Fake User-Agent string for mobile mode + $ua = 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36' + . ' (KHTML, like Gecko) Chrome/60.0.3112.90 Mobile Safari/537.36'; + $options->setExperimentalOption('mobileEmulation', ['userAgent' => $ua]); + $options->addArguments(['--window-size=375,667']); + } elseif (getenv('TESTS_MODE') == 'tablet') { + // Fake User-Agent string for mobile mode + $ua = 'Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 ' + . ' (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36'; + $options->setExperimentalOption('mobileEmulation', ['userAgent' => $ua]); + $options->addArguments(['--window-size=800,640']); + } else { + $options->addArguments(['--window-size=1280,720']); + } + + // Make sure downloads dir exists and is empty + if (!file_exists(__DIR__ . '/Browser/downloads')) { + mkdir(__DIR__ . '/Browser/downloads', 0777, true); + } else { + foreach (glob(__DIR__ . '/Browser/downloads/*') as $file) { + @unlink($file); + } + } + return RemoteWebDriver::create( 'http://localhost:9515', DesiredCapabilities::chrome()->setCapability( @@ -97,4 +131,12 @@ ) ); } + + /** + * Replace Dusk's Browser with our (extended) Browser + */ + protected function newBrowser($driver) + { + return new Browser($driver); + } }