First steps in e2e

This commit is contained in:
An Phan 2016-11-13 23:05:24 +08:00
parent 09ca3a4b9a
commit ef618a611b
No known key found for this signature in database
GPG key ID: 05536BB4BCDC02A2
13 changed files with 523 additions and 8 deletions

2
.gitignore vendored
View file

@ -66,3 +66,5 @@ Temporary Items
### Sass ###
.sass-cache/
*.css.map
/database/e2e.sqlite

View file

@ -5,6 +5,7 @@ namespace App\Http;
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\GetUserFromToken;
use App\Http\Middleware\ObjectStorageAuthenticate;
use App\Http\Middleware\UseDifferentConfigIfE2E;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
@ -20,6 +21,7 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
CheckForMaintenanceMode::class,
UseDifferentConfigIfE2E::class,
];
/**

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
/**
* Check if the app is running in an E2E session and use the proper data settings.
*/
class UseDifferentConfigIfE2E
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if ($_SERVER['SERVER_PORT'] === '8081') {
config(['database.default' => 'sqlite-e2e']);
}
return $next($request);
}
}

View file

@ -11,7 +11,8 @@
"barryvdh/laravel-ide-helper": "^2.1",
"guzzlehttp/guzzle": "^6.1",
"tymon/jwt-auth": "^0.5.6",
"aws/aws-sdk-php-laravel": "^3.1"
"aws/aws-sdk-php-laravel": "^3.1",
"facebook/webdriver": "^1.2"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
@ -31,7 +32,8 @@
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
"tests/TestCase.php",
"tests/E2E/TestCase.php"
]
},
"scripts": {

51
composer.lock generated
View file

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "34d0eb369179b7e8718962fb530f83f0",
"content-hash": "9c68d683b99bec01c5a9f55577399ae8",
"hash": "9216fb1e8ef42422a945b1ebc83e902a",
"content-hash": "9d176a92a6e8bf22d69a770843e8e013",
"packages": [
{
"name": "aws/aws-sdk-php",
@ -412,6 +412,53 @@
],
"time": "2015-11-06 14:35:42"
},
{
"name": "facebook/webdriver",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/facebook/php-webdriver.git",
"reference": "af21de3ae5306a8ca0bcc02a19735dadc43e83f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/facebook/php-webdriver/zipball/af21de3ae5306a8ca0bcc02a19735dadc43e83f3",
"reference": "af21de3ae5306a8ca0bcc02a19735dadc43e83f3",
"shasum": ""
},
"require": {
"ext-curl": "*",
"php": "^5.5 || ~7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^1.11",
"php-mock/php-mock-phpunit": "^1.1",
"phpunit/phpunit": "4.6.* || ~5.0",
"squizlabs/php_codesniffer": "^2.6"
},
"suggest": {
"phpdocumentor/phpdocumentor": "2.*"
},
"type": "library",
"autoload": {
"psr-4": {
"Facebook\\WebDriver\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "A PHP client for WebDriver",
"homepage": "https://github.com/facebook/php-webdriver",
"keywords": [
"facebook",
"php",
"selenium",
"webdriver"
],
"time": "2016-10-14 15:16:51"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.2.1",

View file

@ -52,6 +52,12 @@ return [
'prefix' => '',
],
'sqlite-e2e' => [
'driver' => 'sqlite',
'database' => __DIR__.'/../database/e2e.sqlite',
'prefix' => '',
],
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),

View file

@ -26,16 +26,16 @@ $factory->define(App\Models\Artist::class, function ($faker) {
$factory->define(App\Models\Album::class, function ($faker) {
return [
'name' => $faker->sentence,
'name' => ucwords($faker->words(random_int(2, 5), true)),
'cover' => md5(uniqid()).'.jpg',
];
});
$factory->define(App\Models\Song::class, function ($faker) {
return [
'title' => $faker->sentence,
'title' => ucwords($faker->words(random_int(2, 5), true)),
'length' => $faker->randomFloat(2, 10, 500),
'track' => $faker->randomNumber(),
'track' => random_int(1, 20),
'lyrics' => $faker->paragraph(),
'path' => '/tmp/'.uniqid().'.mp3',
'mtime' => time(),

View file

@ -0,0 +1,25 @@
<?php
use App\Models\Album;
use App\Models\Artist;
use App\Models\Song;
use Illuminate\Database\Seeder;
class E2EDataSeeder extends Seeder
{
private $sampleSongFullPath;
public function run()
{
$this->sampleSongFullPath = realpath(__DIR__.'/../../tests/songs/full.mp3');
factory(Artist::class, 20)->create()->each(function(Artist $artist) {
factory(Album::class, 5)->create([
'artist_id' => $artist->id,
])->each(function (Album $album) {
factory(Song::class, 10)->create([
'album_id' => $album->id,
]);
});
});
}
}

View file

@ -63,6 +63,6 @@
"scripts": {
"postinstall": "cross-env NODE_ENV=production && gulp --production",
"test": "mocha --compilers js:babel-register --require resources/assets/js/tests/helper.js resources/assets/js/tests/**/*Test.js",
"e2e": "mocha resources/assets/js/tests/e2e/runner.js"
"e2e": "echo 'remember to run `php artisan serve --port=8081`' && phpunit tests/e2e -c phpunit.e2e.xml"
}
}

37
phpunit.e2e.xml Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="bootstrap/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="E2E Test Suite">
<directory>./tests/e2e</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">app/</directory>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="e2e"/>
<env name="APP_KEY" value="16efa6c23c2e8c705826b0e66778fbe7"/>
<env name="JWT_SECRET" value="ki8jSvMf5wFrlSRBAWcGbmAzBUJfc8p8"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="DB_CONNECTION" value="sqlite-e2e"/>
<env name="LASTFM_API_KEY" value="foo"/>
<env name="LASTFM_API_SECRET" value="bar"/>
<env name="YOUTUBE_API_KEY" value="foo"/>
<env name="ADMIN_EMAIL" value="koel@example.com"/>
<env name="ADMIN_NAME" value="Koel Admin"/>
<env name="ADMIN_PASSWORD" value="SoSecureK0el"/>
<env name="BROADCAST_DRIVER" value="log"/>
</php>
</phpunit>

View file

@ -11,6 +11,7 @@
<testsuites>
<testsuite name="Application Test Suite">
<directory>./tests/</directory>
<exclude>./tests/e2e</exclude>
</testsuite>
</testsuites>
<filter>

174
tests/e2e/KoelTest.php Normal file
View file

@ -0,0 +1,174 @@
<?php
namespace E2E;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverElement;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\WebDriverKeys;
class KoelTest extends TestCase
{
public function testDefaults()
{
static::assertContains('Koel', $this->driver->getTitle());
$formSelector = '#app > div.login-wrapper > form';
// Our login form should be there.
static::assertCount(1, $this->els($formSelector));
// We submit rubbish and expect an error class on the form.
$this->login('foo@bar.com', 'ThisIsWongOnSoManyLevels');
$this->waitUntil(WebDriverExpectedCondition::presenceOfElementLocated(
WebDriverBy::cssSelector("$formSelector.error")
));
// Now we submit good stuff and make sure we're in.
$this->login();
$this->waitUntil(WebDriverExpectedCondition::textToBePresentInElement(
WebDriverBy::cssSelector('#userBadge > a.view-profile.control > span'), 'Koel Admin'
));
// Default URL must be Home
static::assertEquals($this->url.'/#!/home', $this->driver->getCurrentURL());
// $this->_testSideBar();
// $this->_testHomeScreen();
return $this->_testQueueScreen();
// While we're at this, test logging out as well.
$this->click('#userBadge > a.logout');
$this->waitUntil(WebDriverExpectedCondition::visibilityOfElementLocated(
WebDriverBy::cssSelector($formSelector)
));
}
private function _testSideBar()
{
// All basic navigation
foreach(['home', 'queue', 'songs', 'albums', 'artists', 'youtube', 'settings', 'users'] as $screen) {
$this->goTo($screen);
$this->waitUntil(function () use ($screen) {
return $this->driver->getCurrentURL() === $this->url.'/#!/'.$screen;
});
}
}
private function _testHomeScreen()
{
$this->click('#sidebar a.home');
// We must see some greetings
static::assertTrue($this->el('#homeWrapper > h1')->isDisplayed());
// 6 recently added albums
static::assertCount(6, $this->els('#homeWrapper section.recently-added article'));
// 10 recently added songs
static::assertCount(10, $this->els('#homeWrapper .recently-added-song-list .song-item-home'));
// Shuffle must work for latest albums
$this->click('#homeWrapper section.recently-added article:nth-child(1) span.right a:nth-child(1)');
static::assertCount(10, $this->els('#queueWrapper .song-list-wrap tr.song-item'));
$this->goTo('home');
$this->waitUntil(WebDriverExpectedCondition::visibilityOfElementLocated(
WebDriverBy::cssSelector('#homeWrapper section.recently-added')
));
// Simulate a "double click to play" action
/** @var $clickedSong RemoteWebElement */
$clickedSong = $this->el('#homeWrapper section.recently-added > div > div:nth-child(2) li:nth-child(1) .details');
$this->doubleClick($clickedSong);
// The song must appear at the top of "Recently played" section
/** @var $mostRecentSong RemoteWebElement */
$mostRecentSong = $this->el('#homeWrapper .recently-added-song-list .song-item-home:nth-child(1) .details');
static::assertEquals($mostRecentSong->getText(), $clickedSong->getText());
}
private function _testQueueScreen()
{
$this->goTo('queue');
static::assertContains('Current Queue', $this->el('#queueWrapper > h1 > span')->getText());
// Clear the queue
$this->clearQueue();
static::assertEmpty($this->els('#queueWrapper tr.song-item'));
// Go back to Albums and queue an album of 10 songs
$this->goTo('albums');
$this->click('#albumsWrapper > div > article:nth-child(1) > footer > p > span.right > a:nth-child(1)');
$this->goTo('queue');
// Single song selection
static::assertContains(
'selected',
$this->click('#queueWrapper tr.song-item:nth-child(1)')->getAttribute('class')
);
// shift+click
(new WebDriverActions($this->driver))
->keyDown(null, WebDriverKeys::SHIFT)
->click($this->el('#queueWrapper tr.song-item:nth-child(5)'))
->keyUp(null, WebDriverKeys::SHIFT)
->perform();
// should have 5 selected rows
static::assertCount(5, $this->els('#queueWrapper tr.song-item.selected'));
// Cmd+Click
(new WebDriverActions($this->driver))
->keyDown(null, WebDriverKeys::COMMAND)
->click($this->el('#queueWrapper tr.song-item:nth-child(2)'))
->click($this->el('#queueWrapper tr.song-item:nth-child(3)'))
->keyUp(null, WebDriverKeys::COMMAND)
->perform();
// should have only 3 selected rows remaining
static::assertCount(3, $this->els('#queueWrapper tr.song-item.selected'));
// 2nd and 3rd rows must not be selected
static::assertNotContains(
'selected',
$this->el('#queueWrapper tr.song-item:nth-child(2)')->getAttribute('class')
);
static::assertNotContains(
'selected',
$this->el('#queueWrapper tr.song-item:nth-child(3)')->getAttribute('class')
);
// Delete key should remove selected songs
$this->press(WebDriverKeys::DELETE);
$this->waitUntil(function () {
return count($this->els('#queueWrapper tr.song-item.selected')) === 0
&& count($this->els('#queueWrapper tr.song-item')) === 7;
});
// Ctrl+A/Cmd+A should select all remaining songs
(new WebDriverActions($this->driver))
->keyDown(null, WebDriverKeys::COMMAND)
->keyDown(null, 'A')
->keyUp(null, 'A')
->keyUp(null, WebDriverKeys::COMMAND)
->perform();
static::assertCount(7, $this->els('#queueWrapper tr.song-item.selected'));
// Try adding these songs into a new playlist
$this->click('#queueWrapper .buttons button.btn.btn-green');
$this->typeIn('#queueWrapper .buttons input[type="text"]', 'Foo');
$this->enter();
$this->waitUntil(WebDriverExpectedCondition::textToBePresentInElement(
WebDriverBy::cssSelector('#playlists > ul'), 'Foo'
));
// Now go back to the queue and try adding a song into favorites
$this->goTo('queue');
/** @var WebDriverElement $firstSongInQueue */
$firstSongInQueue = $this->el('#queueWrapper tr.song-item:nth-child(1) .title')->click();
// TODO: There's a bug here.
// After deleting, selection goes wrong (clicking first row selects second).
var_dump($firstSongInQueue->getText());
$this->click('#queueWrapper .buttons button.btn.btn-green');
$this->click('#queueWrapper .buttons li:nth-child(1)');
$this->goTo('favorites');
var_dump($this->el('#favoritesWrapper tr.song-item:nth-last-child(1) .title')->getText());
static::assertEquals($firstSongInQueue->getText(),
$this->el('#favoritesWrapper tr.song-item:nth-last-child(1) .title')->getText());
}
}

190
tests/e2e/TestCase.php Normal file
View file

@ -0,0 +1,190 @@
<?php
namespace E2E;
use App\Application;
use Facebook\WebDriver\Interactions\Internal\WebDriverDoubleClickAction;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\WebDriverKeys;
use Facebook\WebDriver\WebDriverPoint;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Artisan;
class TestCase extends \PHPUnit_Framework_TestCase
{
/**
* @var RemoteWebDriver
*/
protected $driver;
/**
* @var Application
*/
protected $app;
/**
* The default Koel URL for E2E (server by `php artisan serve --port=8081`).
*
* @var string
*/
protected $url = 'http://localhost:8081';
protected $coverPath;
/**
* TestCase constructor.
*/
public function __construct()
{
parent::__construct();
$this->createApp();
$this->prepareForE2E();
$this->driver = RemoteWebDriver::create('http://localhost:4444/wd/hub', DesiredCapabilities::chrome());
$this->driver->manage()->window()->setPosition(new WebDriverPoint(0, 0))
->setSize(new WebDriverDimension(1440, 900));
}
/**
* @return Application
*/
protected function createApp()
{
$this->app = require __DIR__.'/../../bootstrap/app.php';
$this->app->make(Kernel::class)->bootstrap();
return $this->app;
}
private function prepareForE2E()
{
// Make sure we have a fresh database.
@unlink(__DIR__.'/../../database/e2e.sqlite');
touch(__DIR__.'/../../database/e2e.sqlite');
Artisan::call('migrate');
Artisan::call('db:seed');
Artisan::call('db:seed', ['--class' => 'E2EDataSeeder']);
if (!file_exists($this->coverPath)) {
@mkdir($this->coverPath, 0777, true);
}
}
public function setUp()
{
$this->driver->get($this->url);
}
public function tearDown()
{
//$this->driver->quit();
}
protected function el($selector)
{
return $this->driver->findElement(WebDriverBy::cssSelector($selector));
}
protected function els($selector)
{
return $this->driver->findElements(WebDriverBy::cssSelector($selector));
}
protected function type($string)
{
return $this->driver->getKeyboard()->sendKeys($string);
}
protected function typeIn($element, $string)
{
if (is_string($element)) {
$element = $this->el($element);
}
$element->click()->clear();
return $this->type($string);
}
protected function press($key = WebDriverKeys::ENTER)
{
return $this->driver->getKeyboard()->pressKey($key);
}
protected function enter()
{
return $this->press();
}
protected function click($element)
{
return $this->el($element)->click();
}
protected function doubleClick($element)
{
if (is_string($element)) {
$element = $this->el($element);
}
$action = new WebDriverDoubleClickAction($this->driver->getMouse(), $element);
return $action->perform();
}
protected function sleep($seconds)
{
$this->driver->manage()->timeouts()->implicitlyWait($seconds);
}
/**
* Wait until a condition is met.
*
* @param $func (closure|WebDriverExpectedCondition)
* @param int $timeout
*
* @return mixed
* @throws \Exception
*/
protected function waitUntil($func, $timeout = 10)
{
return $this->driver->wait($timeout)->until($func);
}
/**
* Log into Koel.
*
* @param string $username
* @param string $password
*/
protected function login($username = 'koel@example.com', $password = 'SoSecureK0el')
{
$this->typeIn("#app > div.login-wrapper > form > [type='email']", $username);
$this->typeIn("#app > div.login-wrapper > form > [type='password']", $password);
$this->enter();
}
/**
* A helper to allow going to a specific screen.
*
* @param $screen
*
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*/
protected function goTo($screen)
{
if ($screen === 'favorites') {
return $this->click('#sidebar .favorites a');
} else {
return $this->click("#sidebar a.$screen");
}
}
protected function clearQueue()
{
$this->goTo('queue');
if ($this->els('#queueWrapper .song-item')) {
$this->click('#queueWrapper > h1 > div > button.btn.btn-red');
}
}
}