mirror of
https://github.com/koel/koel
synced 2025-02-17 13:58:28 +00:00
Merge test
This commit is contained in:
commit
4ca08c903d
36 changed files with 1504 additions and 48 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -66,3 +66,5 @@ Temporary Items
|
|||
### Sass ###
|
||||
.sass-cache/
|
||||
*.css.map
|
||||
|
||||
/database/e2e.sqlite
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
29
app/Http/Middleware/UseDifferentConfigIfE2E.php
Normal file
29
app/Http/Middleware/UseDifferentConfigIfE2E.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
|
|
51
composer.lock
generated
51
composer.lock
generated
|
@ -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",
|
||||
|
|
|
@ -52,6 +52,12 @@ return [
|
|||
'prefix' => '',
|
||||
],
|
||||
|
||||
'sqlite-e2e' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => __DIR__.'/../database/e2e.sqlite',
|
||||
'prefix' => '',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
|
|
|
@ -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(),
|
||||
|
|
25
database/seeds/E2EDataSeeder.php
Normal file
25
database/seeds/E2EDataSeeder.php
Normal 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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
17
index.html
Normal file
17
index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Koel</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" href="public/css/vendors.css">
|
||||
<link rel="stylesheet" href="public/css/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script src="public/js/vendors.js"></script>
|
||||
<script src="public/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
22
package.json
22
package.json
|
@ -13,7 +13,7 @@
|
|||
"type": "git",
|
||||
"url": "https://github.com/phanan/koel"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dependencies": {
|
||||
"babel-plugin-lodash": "^2.2.1",
|
||||
"babel-plugin-transform-runtime": "^6.3.13",
|
||||
"babel-polyfill": "^6.9.1",
|
||||
|
@ -22,8 +22,6 @@
|
|||
"babel-register": "^6.3.13",
|
||||
"babel-runtime": "^6.0.0",
|
||||
"blueimp-md5": "^2.3.0",
|
||||
"browserify-hmr": "^0.3.1",
|
||||
"chai": "^3.4.1",
|
||||
"cross-env": "^1.0.7",
|
||||
"font-awesome": "^4.5.0",
|
||||
"gulp": "^3.9.0",
|
||||
|
@ -34,7 +32,6 @@
|
|||
"laravel-elixir": "^5.0.0",
|
||||
"local-storage": "^1.4.2",
|
||||
"lodash": "^4.6.1",
|
||||
"mocha": "^2.3.4",
|
||||
"node-sass": "^3.4.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"plyr": "1.5.x",
|
||||
|
@ -49,8 +46,23 @@
|
|||
"vueify-insert-css": "^1.0.0",
|
||||
"youtube-player": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"body-parser": "^1.15.2",
|
||||
"browserify-hmr": "^0.3.1",
|
||||
"chai": "^3.4.1",
|
||||
"chalk": "^1.1.3",
|
||||
"express": "^4.14.0",
|
||||
"faker": "^3.1.0",
|
||||
"http-server": "^0.9.0",
|
||||
"jsdom": "^9.2.1",
|
||||
"json-server": "^0.8.14",
|
||||
"mocha": "^2.3.4",
|
||||
"nightmare": "^2.5.2",
|
||||
"sinon": "^1.17.2"
|
||||
},
|
||||
"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"
|
||||
"test": "mocha --compilers js:babel-register --require resources/assets/js/tests/helper.js resources/assets/js/tests/**/*Test.js",
|
||||
"e2e": "echo 'Remember to run `php artisan serve --port=8081`' && phpunit tests/e2e -c phpunit.e2e.xml"
|
||||
}
|
||||
}
|
||||
|
|
37
phpunit.e2e.xml
Normal file
37
phpunit.e2e.xml
Normal 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>
|
|
@ -11,6 +11,7 @@
|
|||
<testsuites>
|
||||
<testsuite name="Application Test Suite">
|
||||
<directory>./tests/</directory>
|
||||
<exclude>./tests/e2e</exclude>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
|
|
12
resources/assets/js/.idea/js.iml
generated
Normal file
12
resources/assets/js/.idea/js.iml
generated
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
resources/assets/js/.idea/jsLibraryMappings.xml
generated
Normal file
6
resources/assets/js/.idea/jsLibraryMappings.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="ECMAScript 6" />
|
||||
</component>
|
||||
</project>
|
16
resources/assets/js/.idea/misc.xml
generated
Normal file
16
resources/assets/js/.idea/misc.xml
generated
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
|
||||
<OptionsSetting value="true" id="Add" />
|
||||
<OptionsSetting value="true" id="Remove" />
|
||||
<OptionsSetting value="true" id="Checkout" />
|
||||
<OptionsSetting value="true" id="Update" />
|
||||
<OptionsSetting value="true" id="Status" />
|
||||
<OptionsSetting value="true" id="Edit" />
|
||||
<ConfirmationsSetting value="0" id="Add" />
|
||||
<ConfirmationsSetting value="0" id="Remove" />
|
||||
</component>
|
||||
</project>
|
8
resources/assets/js/.idea/modules.xml
generated
Normal file
8
resources/assets/js/.idea/modules.xml
generated
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/js.iml" filepath="$PROJECT_DIR$/.idea/js.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
4
resources/assets/js/.idea/watcherTasks.xml
generated
Normal file
4
resources/assets/js/.idea/watcherTasks.xml
generated
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions" suppressed-tasks="Babel" />
|
||||
</project>
|
338
resources/assets/js/.idea/workspace.xml
generated
Normal file
338
resources/assets/js/.idea/workspace.xml
generated
Normal file
|
@ -0,0 +1,338 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="ab3db53b-a246-4a74-8766-88c1db2358b0" name="Default" comment="" />
|
||||
<ignored path="js.iws" />
|
||||
<ignored path=".idea/workspace.xml" />
|
||||
<ignored path="$PROJECT_DIR$/.tmp/" />
|
||||
<ignored path="$PROJECT_DIR$/temp/" />
|
||||
<ignored path="$PROJECT_DIR$/tmp/" />
|
||||
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
|
||||
<option name="TRACKING_ENABLED" value="true" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="CreatePatchCommitExecutor">
|
||||
<option name="PATCH_PATH" value="" />
|
||||
</component>
|
||||
<component name="ExecutionTargetManager" SELECTED_TARGET="default_target" />
|
||||
<component name="FavoritesManager">
|
||||
<favorites_list name="js" />
|
||||
</component>
|
||||
<component name="FileEditorManager">
|
||||
<leaf SIDE_TABS_SIZE_LIMIT_KEY="300">
|
||||
<file leaf-file-name="login-form.vue" pinned="false" current-in-tab="false">
|
||||
<entry file="file://$PROJECT_DIR$/components/auth/login-form.vue">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="465">
|
||||
<caret line="32" column="25" selection-start-line="32" selection-start-column="25" selection-end-line="32" selection-end-column="25" />
|
||||
<folding />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
</file>
|
||||
<file leaf-file-name="index.vue" pinned="false" current-in-tab="false">
|
||||
<entry file="file://$PROJECT_DIR$/components/main-wrapper/index.vue">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="75">
|
||||
<caret line="5" column="8" selection-start-line="5" selection-start-column="8" selection-end-line="5" selection-end-column="8" />
|
||||
<folding />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
</file>
|
||||
<file leaf-file-name="edit-songs-form.vue" pinned="false" current-in-tab="false">
|
||||
<entry file="file://$PROJECT_DIR$/components/modals/edit-songs-form.vue">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="-3720">
|
||||
<caret line="13" column="15" selection-start-line="13" selection-start-column="15" selection-end-line="13" selection-end-column="15" />
|
||||
<folding />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
</file>
|
||||
<file leaf-file-name="album-item.vue" pinned="false" current-in-tab="true">
|
||||
<entry file="file://$PROJECT_DIR$/components/shared/album-item.vue">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="451">
|
||||
<caret line="106" column="43" selection-start-line="106" selection-start-column="43" selection-end-line="106" selection-end-column="43" />
|
||||
<folding />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
</file>
|
||||
<file leaf-file-name="main.js" pinned="false" current-in-tab="false">
|
||||
<entry file="file://$PROJECT_DIR$/main.js">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="180">
|
||||
<caret line="15" column="16" selection-start-line="15" selection-start-column="16" selection-end-line="15" selection-end-column="16" />
|
||||
<folding>
|
||||
<element signature="e#0#22#0" expanded="true" />
|
||||
</folding>
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
</file>
|
||||
</leaf>
|
||||
</component>
|
||||
<component name="JsBuildToolGruntFileManager" detection-done="true" sorting="DEFINITION_ORDER" />
|
||||
<component name="JsBuildToolPackageJson" detection-done="true" sorting="DEFINITION_ORDER" />
|
||||
<component name="JsGulpfileManager">
|
||||
<detection-done>true</detection-done>
|
||||
<sorting>DEFINITION_ORDER</sorting>
|
||||
</component>
|
||||
<component name="ProjectFrameBounds">
|
||||
<option name="x" value="20" />
|
||||
<option name="y" value="43" />
|
||||
<option name="width" value="1400" />
|
||||
<option name="height" value="833" />
|
||||
</component>
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
|
||||
<OptionsSetting value="true" id="Add" />
|
||||
<OptionsSetting value="true" id="Remove" />
|
||||
<OptionsSetting value="true" id="Checkout" />
|
||||
<OptionsSetting value="true" id="Update" />
|
||||
<OptionsSetting value="true" id="Status" />
|
||||
<OptionsSetting value="true" id="Edit" />
|
||||
<ConfirmationsSetting value="0" id="Add" />
|
||||
<ConfirmationsSetting value="0" id="Remove" />
|
||||
</component>
|
||||
<component name="ProjectView">
|
||||
<navigator currentView="ProjectPane" proportions="" version="1">
|
||||
<flattenPackages />
|
||||
<showMembers />
|
||||
<showModules />
|
||||
<showLibraryContents />
|
||||
<hideEmptyPackages />
|
||||
<abbreviatePackageNames />
|
||||
<autoscrollToSource />
|
||||
<autoscrollFromSource />
|
||||
<sortByType />
|
||||
<manualOrder />
|
||||
<foldersAlwaysOnTop value="true" />
|
||||
</navigator>
|
||||
<panes>
|
||||
<pane id="Scratches" />
|
||||
<pane id="Scope" />
|
||||
<pane id="ProjectPane">
|
||||
<subPane>
|
||||
<PATH>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
|
||||
</PATH_ELEMENT>
|
||||
</PATH>
|
||||
<PATH>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
</PATH>
|
||||
<PATH>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="components" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
</PATH>
|
||||
<PATH>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="components" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="shared" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
</PATH>
|
||||
<PATH>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="components" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="modals" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
</PATH>
|
||||
<PATH>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="components" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="main-wrapper" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
</PATH>
|
||||
<PATH>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="js" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="components" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
<PATH_ELEMENT>
|
||||
<option name="myItemId" value="auth" />
|
||||
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
|
||||
</PATH_ELEMENT>
|
||||
</PATH>
|
||||
</subPane>
|
||||
</pane>
|
||||
</panes>
|
||||
</component>
|
||||
<component name="PropertiesComponent">
|
||||
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
||||
<property name="WebServerToolWindowFactoryState" value="false" />
|
||||
<property name="HbShouldOpenHtmlAsHb" value="" />
|
||||
<property name="nodejs_interpreter_path" value="/usr/local/bin/node" />
|
||||
</component>
|
||||
<component name="RunManager">
|
||||
<configuration default="true" type="NodeJSConfigurationType" factoryName="Node.js" path-to-node="project" working-dir="">
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="ShelveChangesManager" show_recycled="false">
|
||||
<option name="remove_strategy" value="false" />
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="ab3db53b-a246-4a74-8766-88c1db2358b0" name="Default" comment="" />
|
||||
<created>1479096421308</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1479096421308</updated>
|
||||
<workItem from="1479096423357" duration="76000" />
|
||||
<workItem from="1479096510774" duration="15000" />
|
||||
<workItem from="1479096542485" duration="78000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TimeTrackingManager">
|
||||
<option name="totallyTimeSpent" value="169000" />
|
||||
</component>
|
||||
<component name="ToolWindowManager">
|
||||
<frame x="20" y="43" width="1400" height="833" extended-state="0" />
|
||||
<editor active="false" />
|
||||
<layout>
|
||||
<window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.17571428" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
|
||||
<window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Event Log" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="7" side_tool="true" content_ui="tabs" />
|
||||
<window_info id="Version Control" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Terminal" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Favorites" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="true" content_ui="tabs" />
|
||||
<window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Inspection" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Hierarchy" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="combo" />
|
||||
<window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Ant Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
|
||||
<window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
|
||||
</layout>
|
||||
</component>
|
||||
<component name="Vcs.Log.UiProperties">
|
||||
<option name="RECENTLY_FILTERED_USER_GROUPS">
|
||||
<collection />
|
||||
</option>
|
||||
<option name="RECENTLY_FILTERED_BRANCH_GROUPS">
|
||||
<collection />
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsContentAnnotationSettings">
|
||||
<option name="myLimit" value="2678400000" />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager />
|
||||
<watches-manager />
|
||||
</component>
|
||||
<component name="editorHistoryManager">
|
||||
<entry file="file://$PROJECT_DIR$/components/auth/login-form.vue">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="465">
|
||||
<caret line="32" column="25" selection-start-line="32" selection-start-column="25" selection-end-line="32" selection-end-column="25" />
|
||||
<folding />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/main.js">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="180">
|
||||
<caret line="15" column="16" selection-start-line="15" selection-start-column="16" selection-end-line="15" selection-end-column="16" />
|
||||
<folding>
|
||||
<element signature="e#0#22#0" expanded="true" />
|
||||
</folding>
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/components/main-wrapper/index.vue">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="75">
|
||||
<caret line="5" column="8" selection-start-line="5" selection-start-column="8" selection-end-line="5" selection-end-column="8" />
|
||||
<folding />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/components/modals/edit-songs-form.vue">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="-3720">
|
||||
<caret line="13" column="15" selection-start-line="13" selection-start-column="15" selection-end-line="13" selection-end-column="15" />
|
||||
<folding />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/components/shared/album-item.vue">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="451">
|
||||
<caret line="106" column="43" selection-start-line="106" selection-start-column="43" selection-end-line="106" selection-end-column="43" />
|
||||
<folding />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
</component>
|
||||
</project>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div id="youtube-wrapper">
|
||||
<div id="youtube-extra-wrapper">
|
||||
<template v-if="videos && videos.length">
|
||||
<a class="video" v-for="video in videos" href @click.prevent="playYouTube(video.id.videoId)">
|
||||
<div class="thumb">
|
||||
|
@ -58,7 +58,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
#youtube-wrapper {
|
||||
#youtube-extra-wrapper {
|
||||
overflow-x: hidden;
|
||||
|
||||
.video {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<section id="youTubePlayer">
|
||||
<section id="youtubeWrapper">
|
||||
<h1 class="heading"><span>YouTube Video</span></h1>
|
||||
<div id="player">
|
||||
<p class="none">Your YouTube video will be played here.<br/>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<div v-show="currentView === 'details'">
|
||||
<div class="form-row" v-if="editSingle">
|
||||
<label>Title</label>
|
||||
<input type="text" v-model="formData.title">
|
||||
<input name="title" type="text" v-model="formData.title">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>Artist</label>
|
||||
|
@ -55,7 +55,7 @@
|
|||
</div>
|
||||
<div class="form-row" v-show="editSingle">
|
||||
<label>Track</label>
|
||||
<input type="number" min="0" v-model="formData.track">
|
||||
<input name="track" type="number" min="0" v-model="formData.track">
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="currentView === 'lyrics' && editSingle">
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
class="song-item"
|
||||
draggable="true"
|
||||
:data-song-id="song.id"
|
||||
@click="$parent.rowClick(song.id, $event)"
|
||||
@click="clicked($event)"
|
||||
@dblclick.prevent="playRightAwayyyyyyy"
|
||||
@dragstart="$parent.dragStart(song.id, $event)"
|
||||
@dragleave="$parent.removeDroppableState($event)"
|
||||
@dragover.prevent="$parent.allowDrop(song.id, $event)"
|
||||
@drop.stop.prevent="$parent.handleDrop(song.id, $event)"
|
||||
@contextmenu.prevent="$parent.openContextMenu(song.id, $event)"
|
||||
:class="{ selected: selected, playing: song.playbackState === 'playing' || song.playbackState === 'paused' }"
|
||||
:class="{ selected: selected, playing: playing }"
|
||||
>
|
||||
<td class="track-number">{{ song.track || '' }}</td>
|
||||
<td class="title">{{ song.title }}</td>
|
||||
|
@ -37,6 +37,12 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
playing() {
|
||||
return this.song.playbackState === 'playing' || this.song.playbackState === 'paused';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Play the song right away.
|
||||
|
@ -66,26 +72,24 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
clicked($e) {
|
||||
this.$emit('itemClicked', this.song.id, $e);
|
||||
},
|
||||
|
||||
select() {
|
||||
this.selected = true;
|
||||
},
|
||||
|
||||
deselect() {
|
||||
this.selected = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the "selected" state of the current component.
|
||||
*/
|
||||
toggleSelectedState() {
|
||||
this.selected = !this.selected;
|
||||
},
|
||||
|
||||
/**
|
||||
* Select the current component (apply a CSS class on its DOM).
|
||||
*/
|
||||
select() {
|
||||
this.selected = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deselect the current component.
|
||||
*/
|
||||
deselect() {
|
||||
this.selected = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -35,7 +35,12 @@
|
|||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr is="song-item" v-for="item in displayedItems" :song="item" ref="rows" :key="item.id"/>
|
||||
<tr is="song-item"
|
||||
v-for="item in displayedItems"
|
||||
@itemClicked="itemClicked"
|
||||
:song="item"
|
||||
:key="item.id"
|
||||
ref="rows"/>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@ -69,7 +74,6 @@ export default {
|
|||
q: '', // The filter query
|
||||
sortKey: '',
|
||||
order: 1,
|
||||
componentCache: {},
|
||||
sortingByAlbum: false,
|
||||
sortingByArtist: false,
|
||||
selectedSongs: [],
|
||||
|
@ -221,12 +225,7 @@ export default {
|
|||
* @return {Object} The Vue compoenent
|
||||
*/
|
||||
getComponentBySongId(id) {
|
||||
// A Vue component can be removed (as a result of filter for example), so we check for its $el as well.
|
||||
if (!this.componentCache[id] || !this.componentCache[id].$el) {
|
||||
this.componentCache[id] = find(this.$refs.rows, { song: { id } });
|
||||
}
|
||||
|
||||
return this.componentCache[id];
|
||||
return find(this.$refs.rows, { song: { id } });
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -269,7 +268,7 @@ export default {
|
|||
* @param {String} songId
|
||||
* @param {Object} e
|
||||
*/
|
||||
rowClick(songId, e) {
|
||||
itemClicked(songId, e) {
|
||||
const row = this.getComponentBySongId(songId);
|
||||
|
||||
// If we're on a touch device, or if Ctrl/Cmd key is pressed, just toggle selection.
|
||||
|
@ -342,8 +341,11 @@ export default {
|
|||
this.gatherSelected();
|
||||
}
|
||||
|
||||
console.log('selected songs before drop:', this.selectedSongs);
|
||||
|
||||
this.$nextTick(() => {
|
||||
const songIds = map(this.selectedSongs, 'id');
|
||||
console.log('dragging', songIds);
|
||||
e.dataTransfer.setData('application/x-koel.text+plain', songIds);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
|
||||
|
@ -377,6 +379,7 @@ export default {
|
|||
* @param {Object} e
|
||||
*/
|
||||
handleDrop(songId, e) {
|
||||
console.log('dropping into', songId);
|
||||
if (this.type !== 'queue') {
|
||||
return this.removeDroppableState(e) && false;
|
||||
}
|
||||
|
@ -386,6 +389,7 @@ export default {
|
|||
}
|
||||
|
||||
const songs = this.selectedSongs;
|
||||
console.log('selected songs after drop:', songs);
|
||||
|
||||
if (!songs.length) {
|
||||
return this.removeDroppableState(e) && false;
|
||||
|
@ -458,12 +462,6 @@ export default {
|
|||
*/
|
||||
'main-content-view:load': () => this.clearSelection(),
|
||||
|
||||
/**
|
||||
* Listens to the 'song:selection-changed' dispatched from a child song-item
|
||||
* to collect the selected songs.
|
||||
*/
|
||||
'song:selection-changed': () => this.gatherSelected(),
|
||||
|
||||
/**
|
||||
* Listen to 'song:selection-clear' (often broadcasted from the direct parent)
|
||||
* to clear the selected songs.
|
||||
|
|
148
resources/assets/js/tests/e2e/apiServer.js
Normal file
148
resources/assets/js/tests/e2e/apiServer.js
Normal file
|
@ -0,0 +1,148 @@
|
|||
'use strict'
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const faker = require('faker')
|
||||
const md5 = require('blueimp-md5')
|
||||
const _ = require('lodash')
|
||||
const jsonServer = require('json-server')
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
const app = express()
|
||||
const rootDir = path.resolve(__dirname, '../../../../../')
|
||||
let data = null
|
||||
|
||||
const getData = () => {
|
||||
if (!data) {
|
||||
data = {
|
||||
allowDownload: faker.random.boolean(),
|
||||
artists: [],
|
||||
albums: [],
|
||||
cdnUrl: 'http://localhost:8080/',
|
||||
currentUser: {},
|
||||
interactions: [],
|
||||
latestVersion: 'v2.2.1',
|
||||
playlists: [],
|
||||
settings: {
|
||||
media_path: '/tmp/fake/path'
|
||||
},
|
||||
useLastfm: faker.random.boolean(),
|
||||
users: []
|
||||
}
|
||||
|
||||
const allSongs = []
|
||||
|
||||
// create 100 songs in 10 albums by 5 artists
|
||||
for (let i = 1; i < 6; ++i) {
|
||||
const artist = {
|
||||
id: i,
|
||||
name: faker.name.findName(),
|
||||
image: faker.image.imageUrl(),
|
||||
albums: []
|
||||
}
|
||||
|
||||
for (let j = 1; j < 3; ++j) {
|
||||
const album = {
|
||||
id: i * 10 + j,
|
||||
artist_id: artist.id,
|
||||
cover: faker.image.imageUrl(),
|
||||
is_compilation: false, // let's keep it simple for now
|
||||
name: faker.lorem.sentence(3),
|
||||
songs: []
|
||||
}
|
||||
|
||||
for (let k = 1; k < 11; ++k) {
|
||||
const song = {
|
||||
id: md5(faker.random.uuid()),
|
||||
album_id: album.id,
|
||||
contributing_artist_id: null,
|
||||
title: faker.lorem.sentence(5),
|
||||
length: (Math.random()*1000).toFixed(2),
|
||||
track: faker.random.number(15),
|
||||
lyrics: faker.lorem.paragraphs(3),
|
||||
path: '/tmp/fake/file.mp3'
|
||||
}
|
||||
album.songs.push(song)
|
||||
allSongs.push(song)
|
||||
}
|
||||
artist.albums.push(album)
|
||||
}
|
||||
data.artists.push(artist)
|
||||
}
|
||||
|
||||
// create 50 interactions
|
||||
_.sampleSize(allSongs, 50).forEach(song => {
|
||||
data.interactions.push({
|
||||
song_id: song.id,
|
||||
like: faker.random.boolean(),
|
||||
play_count: faker.random.number(100)
|
||||
})
|
||||
})
|
||||
|
||||
// create 3 playlists, each contains a random of 0-20 songs
|
||||
for (let i = 1; i < 4; ++i) {
|
||||
data.playlists.push({
|
||||
id: i,
|
||||
name: faker.lorem.sentence(3),
|
||||
songs: _.map(_.sampleSize(allSongs, faker.random.number(20)), 'id')
|
||||
})
|
||||
}
|
||||
|
||||
// create 3 users and make the first one the current and admin
|
||||
for (let i = 1; i < 4; ++i) {
|
||||
data.users.push({
|
||||
id: i,
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email(),
|
||||
is_admin: i === 1
|
||||
})
|
||||
}
|
||||
data.currentUser = _.clone(data.users[0])
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
fs.createReadStream(__dirname + '/app.html')
|
||||
.pipe(fs.createWriteStream(rootDir + '/index.html'))
|
||||
|
||||
app.use(express.static(rootDir))
|
||||
app.use(bodyParser.json())
|
||||
|
||||
// Mock all basic routes
|
||||
app.get('/api/data', (req, res) => {
|
||||
res.json(getData())
|
||||
}).post('/api/me', (req, res, next) => {
|
||||
if (req.body.email === 'admin@koel.dev' && req.body.password === 'koel') {
|
||||
res.json({ token: 'koelToken' })
|
||||
} else {
|
||||
res.status(401).json({ error: 'invalid_credentials' })
|
||||
}
|
||||
}).delete('/api/me', (req, res, next) => {
|
||||
next()
|
||||
}).put('/api/me', (req, res, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
server: null,
|
||||
|
||||
start() {
|
||||
this.server = app.listen(3000, function () {
|
||||
console.log('API server started at port 3000')
|
||||
})
|
||||
},
|
||||
|
||||
stop() {
|
||||
this.server.close()
|
||||
},
|
||||
|
||||
restart() {
|
||||
this.stop()
|
||||
this.start()
|
||||
},
|
||||
|
||||
data() {
|
||||
return getData()
|
||||
}
|
||||
}
|
17
resources/assets/js/tests/e2e/app.html
Normal file
17
resources/assets/js/tests/e2e/app.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Koel</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" href="public/css/vendors.css">
|
||||
<link rel="stylesheet" href="public/css/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script src="public/js/vendors.js"></script>
|
||||
<script src="public/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
49
resources/assets/js/tests/e2e/runner.js
Normal file
49
resources/assets/js/tests/e2e/runner.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
'use strict'
|
||||
const fs = require('fs')
|
||||
const chalk = require('chalk')
|
||||
const path = require('path')
|
||||
require('chai').should()
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
|
||||
console.log(chalk.white.bgMagenta('Remember to run gulp before e2e!'))
|
||||
|
||||
app.use('/public', express.static(path.resolve(__dirname, '../../../../public')))
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.resolve(__dirname, 'app.html'))
|
||||
})
|
||||
const appServer = app.listen(8080, function () {
|
||||
console.log('App server started at 8080')
|
||||
})
|
||||
|
||||
let apiServer = require('./apiServer')
|
||||
|
||||
const Nightmare = require('nightmare')
|
||||
const nightmare = Nightmare({ show: true })
|
||||
|
||||
apiServer.start()
|
||||
|
||||
describe('test elements rendered', function () {
|
||||
afterEach(() => {
|
||||
nightmare.end()
|
||||
})
|
||||
|
||||
after(function() {
|
||||
apiServer.stop()
|
||||
apiServer = null
|
||||
})
|
||||
|
||||
it ('should display the login form', done => {
|
||||
nightmare
|
||||
.goto('http://localhost:8080')
|
||||
.wait('#main')
|
||||
.evaluate(() => {
|
||||
return document.querySelector('.login-wrapper')
|
||||
})
|
||||
.then(result => {
|
||||
result.should.not.be.null
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
25
tests/e2e/AlbumsScreenTest.php
Normal file
25
tests/e2e/AlbumsScreenTest.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace E2E;
|
||||
|
||||
use Facebook\WebDriver\WebDriverBy;
|
||||
|
||||
class AlbumsScreenTest extends TestCase
|
||||
{
|
||||
public function testAlbumsScreen()
|
||||
{
|
||||
$this->loginAndWait()->goto('albums');
|
||||
|
||||
static::assertNotEmpty($this->els('#albumsWrapper .albums article.item'));
|
||||
$firstAlbum = $this->el('#albumsWrapper .albums article.item:nth-child(1)');
|
||||
static::assertNotEmpty($firstAlbum->findElement(WebDriverBy::cssSelector('.info .name'))->getText());
|
||||
static::assertNotEmpty($firstAlbum->findElement(WebDriverBy::cssSelector('.info .artist'))->getText());
|
||||
static::assertContains('10 songs', $firstAlbum->findElement(WebDriverBy::cssSelector('.meta'))->getText());
|
||||
|
||||
// test the view modes
|
||||
$this->click('#albumsWrapper > h1.heading > span.view-modes > a:nth-child(2)');
|
||||
static::assertCount(1, $this->els('#albumsWrapper > div.albums.as-list'));
|
||||
$this->click('#albumsWrapper > h1.heading > span.view-modes > a:nth-child(1)');
|
||||
static::assertCount(1, $this->els('#albumsWrapper > div.albums.as-thumbnails'));
|
||||
}
|
||||
}
|
8
tests/e2e/AllSongsScreenTest.php
Normal file
8
tests/e2e/AllSongsScreenTest.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace E2E;
|
||||
|
||||
abstract class AllSongsScreenTest extends TestCase
|
||||
{
|
||||
// All we need to test should have been cover in SongListTest class.
|
||||
}
|
24
tests/e2e/ArtistsScreenTest.php
Normal file
24
tests/e2e/ArtistsScreenTest.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace E2E;
|
||||
|
||||
use Facebook\WebDriver\WebDriverBy;
|
||||
|
||||
class ArtistsScreenTest extends TestCase
|
||||
{
|
||||
public function testArtistsScreen()
|
||||
{
|
||||
$this->loginAndWait()->goto('artists');
|
||||
|
||||
static::assertNotEmpty($this->els('#artistsWrapper .artists article.item'));
|
||||
$firstArtist = $this->el('#artistsWrapper .artists article.item:nth-child(1)');
|
||||
static::assertNotEmpty($firstArtist->findElement(WebDriverBy::cssSelector('.info .name'))->getText());
|
||||
static::assertContains('5 albums • 50 songs', $firstArtist->findElement(WebDriverBy::cssSelector('.meta'))->getText());
|
||||
|
||||
// test the view modes
|
||||
$this->click('#artistsWrapper > h1.heading > span.view-modes > a:nth-child(2)');
|
||||
static::assertCount(1, $this->els('#artistsWrapper > div.artists.as-list'));
|
||||
$this->click('#artistsWrapper > h1.heading > span.view-modes > a:nth-child(1)');
|
||||
static::assertCount(1, $this->els('#artistsWrapper > div.artists.as-thumbnails'));
|
||||
}
|
||||
}
|
40
tests/e2e/DefaultsTest.php
Normal file
40
tests/e2e/DefaultsTest.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace E2E;
|
||||
|
||||
use Facebook\WebDriver\WebDriverBy;
|
||||
use Facebook\WebDriver\WebDriverExpectedCondition;
|
||||
|
||||
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());
|
||||
|
||||
// While we're at this, test logging out as well.
|
||||
$this->click('#userBadge > a.logout');
|
||||
$this->waitUntil(WebDriverExpectedCondition::visibilityOfElementLocated(
|
||||
WebDriverBy::cssSelector($formSelector)
|
||||
));
|
||||
}
|
||||
}
|
45
tests/e2e/HomeScreenTest.php
Normal file
45
tests/e2e/HomeScreenTest.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: an
|
||||
* Date: 15/11/16
|
||||
* Time: 4:14 PM
|
||||
*/
|
||||
|
||||
namespace E2E;
|
||||
|
||||
use Facebook\WebDriver\Remote\RemoteWebElement;
|
||||
|
||||
class HomeScreenTest extends TestCase
|
||||
{
|
||||
public function testHomeScreen()
|
||||
{
|
||||
$this->loginAndWait();
|
||||
|
||||
$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');
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
24
tests/e2e/QueueScreenTest.php
Normal file
24
tests/e2e/QueueScreenTest.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace E2E;
|
||||
|
||||
class QueueScreenTest extends TestCase
|
||||
{
|
||||
public function test()
|
||||
{
|
||||
$this->loginAndWait();
|
||||
$this->goto('queue');
|
||||
static::assertContains('Current Queue', $this->el('#queueWrapper > h1 > span')->getText());
|
||||
|
||||
// As the queue is currently empty, the "Shuffling all song" link should be there
|
||||
$this->click('#queueWrapper a.start');
|
||||
$this->waitUntil(function () {
|
||||
return count($this->els('#queueWrapper .song-item'));
|
||||
});
|
||||
|
||||
// Clear the queue
|
||||
$this->click('#queueWrapper .buttons button.btn.btn-red');
|
||||
static::assertEmpty($this->els('#queueWrapper tr.song-item'));
|
||||
}
|
||||
|
||||
}
|
68
tests/e2e/SideBarTest.php
Normal file
68
tests/e2e/SideBarTest.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace E2E;
|
||||
|
||||
use Facebook\WebDriver\WebDriverBy;
|
||||
use Facebook\WebDriver\WebDriverElement;
|
||||
use Facebook\WebDriver\WebDriverExpectedCondition;
|
||||
|
||||
class SideBarTest extends TestCase
|
||||
{
|
||||
public function testSideBar()
|
||||
{
|
||||
$this->loginAndWait();
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
// Add a playlist
|
||||
$this->click('#playlists > h1 > i.create');
|
||||
$this->waitUntil(WebDriverExpectedCondition::visibilityOfElementLocated(
|
||||
WebDriverBy::cssSelector('#playlists > form.create')
|
||||
));
|
||||
$this->typeIn('#playlists > form > input[type="text"]', 'Bar');
|
||||
$this->enter();
|
||||
/** @var WebDriverElement $mostRecentPlaylist */
|
||||
$mostRecentPlaylist = null;
|
||||
$this->waitUntil(function () use (&$mostRecentPlaylist) {
|
||||
/** @var WebDriverElement $mostRecentPlaylist */
|
||||
$list = $this->els('#playlists .playlist');
|
||||
$mostRecentPlaylist = end($list);
|
||||
|
||||
return $mostRecentPlaylist->getText() === 'Bar';
|
||||
});
|
||||
|
||||
// Double click to edit/rename a playlist
|
||||
$this->doubleClick($mostRecentPlaylist);
|
||||
$this->waitUntil(function () use (&$mostRecentPlaylist) {
|
||||
return count($mostRecentPlaylist->findElements(WebDriverBy::cssSelector('input[type="text"]')));
|
||||
});
|
||||
$this->typeIn(
|
||||
$mostRecentPlaylist->findElement(WebDriverBy::cssSelector('input[type="text"]')),
|
||||
'Baz'
|
||||
);
|
||||
$this->enter();
|
||||
$this->waitUntil(function () {
|
||||
$list = $this->els('#playlists .playlist');
|
||||
$mostRecentPlaylist = end($list);
|
||||
|
||||
return $mostRecentPlaylist->getText() === 'Baz';
|
||||
});
|
||||
|
||||
// Edit with an empty name shouldn't do anything.
|
||||
$this->doubleClick($mostRecentPlaylist);
|
||||
$mostRecentPlaylist->findElement(WebDriverBy::cssSelector('input[type="text"]'))->clear();
|
||||
$this->enter();
|
||||
$this->waitUntil(function () {
|
||||
$list = $this->els('#playlists .playlist');
|
||||
$mostRecentPlaylist = end($list);
|
||||
|
||||
return $mostRecentPlaylist->getText() === 'Baz';
|
||||
});
|
||||
}
|
||||
}
|
195
tests/e2e/SongListTest.php
Normal file
195
tests/e2e/SongListTest.php
Normal file
|
@ -0,0 +1,195 @@
|
|||
<?php
|
||||
|
||||
namespace E2E;
|
||||
|
||||
use Facebook\WebDriver\Interactions\WebDriverActions;
|
||||
use Facebook\WebDriver\WebDriverBy;
|
||||
use Facebook\WebDriver\WebDriverElement;
|
||||
use Facebook\WebDriver\WebDriverExpectedCondition;
|
||||
use Facebook\WebDriver\WebDriverKeys;
|
||||
|
||||
class SongListTest extends TestCase
|
||||
{
|
||||
public function testSelection()
|
||||
{
|
||||
$this->loginAndWait()->repopulateList();
|
||||
|
||||
// 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 songs
|
||||
$this->selectAll();
|
||||
static::assertCount(7, $this->els('#queueWrapper tr.song-item.selected'));
|
||||
}
|
||||
|
||||
public function testActionButtons()
|
||||
{
|
||||
$this->loginAndWait()->repopulateList();
|
||||
|
||||
// Since no songs are selected, the shuffle button must read "(Shuffle) All"
|
||||
static::assertContains('ALL', $this->el('#queueWrapper button.play-shuffle')->getText());
|
||||
// Now we selected all songs it to read "(Shuffle) Selected"
|
||||
$this->selectAll();
|
||||
static::assertContains('SELECTED', $this->el('#queueWrapper button.play-shuffle')->getText());
|
||||
|
||||
// Add to favorites
|
||||
$this->el('#queueWrapper tr.song-item:nth-child(1)')->click();
|
||||
$this->click('#queueWrapper .buttons button.btn.btn-green');
|
||||
$this->click('#queueWrapper .buttons li:nth-child(1)');
|
||||
$this->goto('favorites');
|
||||
static::assertCount(1, $this->els('#favoritesWrapper tr.song-item'));
|
||||
|
||||
$this->goto('queue');
|
||||
$this->click('#queueWrapper tr.song-item:nth-child(1)');
|
||||
// Try adding a song 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'
|
||||
));
|
||||
}
|
||||
|
||||
public function testSorting()
|
||||
{
|
||||
$this->loginAndWait()->repopulateList();
|
||||
|
||||
// Confirm that we can't sort in Queue screen
|
||||
/** @var WebDriverElement $th */
|
||||
foreach ($this->els('#queueWrapper div.song-list-wrap th') as $th) {
|
||||
if (!$th->isDisplayed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($th->findElements(WebDriverBy::tagName('i')) as $sortDirectionIcon) {
|
||||
static::assertFalse($sortDirectionIcon->isDisplayed());
|
||||
}
|
||||
}
|
||||
|
||||
// Now go to All Songs screen and sort there
|
||||
$this->goto('songs');
|
||||
$this->click('#songsWrapper div.song-list-wrap th:nth-child(2)');
|
||||
$last = null;
|
||||
$results = [];
|
||||
/** @var WebDriverElement $td */
|
||||
foreach ($this->els('#songsWrapper div.song-list-wrap td.title') as $td) {
|
||||
$current = $td->getText();
|
||||
$results[] = $last === null ? true : $current <= $last;
|
||||
$last = $current;
|
||||
|
||||
}
|
||||
static::assertNotContains(false, $results);
|
||||
|
||||
// Second click will reverse the sort
|
||||
$this->click('#songsWrapper div.song-list-wrap th:nth-child(2)');
|
||||
$last = null;
|
||||
$results = [];
|
||||
/** @var WebDriverElement $td */
|
||||
foreach ($this->els('#songsWrapper div.song-list-wrap td.title') as $td) {
|
||||
$current = $td->getText();
|
||||
$results[] = $last === null ? true : $current >= $last;
|
||||
$last = $current;
|
||||
}
|
||||
static::assertNotContains(false, $results);
|
||||
}
|
||||
|
||||
public function testContextMenu()
|
||||
{
|
||||
$this->loginAndWait()->goto('songs');
|
||||
$this->rightClick('#songsWrapper tr.song-item:nth-child(1)');
|
||||
|
||||
$by = WebDriverBy::cssSelector('#songsWrapper .song-menu');
|
||||
$this->waitUntil(WebDriverExpectedCondition::visibilityOfElementLocated($by));
|
||||
|
||||
// 7 sub menu items
|
||||
static::assertCount(7, $this->els('#songsWrapper .song-menu > li'));
|
||||
|
||||
// Clicking the "Go to Album" menu item
|
||||
$this->click('#songsWrapper .song-menu > li:nth-child(2)');
|
||||
$this->waitUntil(WebDriverExpectedCondition::visibilityOfElementLocated(
|
||||
WebDriverBy::cssSelector('#albumWrapper')
|
||||
));
|
||||
|
||||
// Clicking the "Go to Artist" menu item
|
||||
$this->back();
|
||||
$this->rightClick('#songsWrapper tr.song-item:nth-child(1)');
|
||||
$this->click('#songsWrapper .song-menu > li:nth-child(3)');
|
||||
$this->waitUntil(WebDriverExpectedCondition::visibilityOfElementLocated(
|
||||
WebDriverBy::cssSelector('#artistWrapper')
|
||||
));
|
||||
|
||||
// Clicking "Edit"
|
||||
$this->back();
|
||||
$this->rightClick('#songsWrapper tr.song-item:nth-child(1)');
|
||||
$this->click('#songsWrapper .song-menu > li:nth-child(5)');
|
||||
$this->waitUntil(WebDriverExpectedCondition::visibilityOfElementLocated(
|
||||
WebDriverBy::cssSelector('#editSongsOverlay form')
|
||||
));
|
||||
// Updating song
|
||||
$this->typeIn('#editSongsOverlay form input[name="title"]', 'Foo');
|
||||
$this->typeIn('#editSongsOverlay form input[name="track"]', 99);
|
||||
$this->enter();
|
||||
$this->waitUntil(WebDriverExpectedCondition::invisibilityOfElementLocated(
|
||||
WebDriverBy::cssSelector('#editSongsOverlay form')
|
||||
));
|
||||
static::assertEquals('99', $this->el('#songsWrapper tr.song-item:nth-child(1) .track-number')->getText());
|
||||
static::assertEquals('Foo', $this->el('#songsWrapper tr.song-item:nth-child(1) .title')->getText());
|
||||
}
|
||||
|
||||
private function repopulateList()
|
||||
{
|
||||
// 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');
|
||||
}
|
||||
|
||||
private function selectAll()
|
||||
{
|
||||
$this->focusIntoApp(); // make sure focus is there before executing shortcut keys
|
||||
(new WebDriverActions($this->driver))
|
||||
->keyDown(null, WebDriverKeys::COMMAND)
|
||||
->keyDown(null, 'A')
|
||||
->keyUp(null, 'A')
|
||||
->keyUp(null, WebDriverKeys::COMMAND)
|
||||
->perform();
|
||||
}
|
||||
}
|
133
tests/e2e/TestCase.php
Normal file
133
tests/e2e/TestCase.php
Normal file
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
namespace E2E;
|
||||
|
||||
use App\Application;
|
||||
use Facebook\WebDriver\Remote\DesiredCapabilities;
|
||||
use Facebook\WebDriver\Remote\RemoteWebDriver;
|
||||
use Facebook\WebDriver\WebDriverBy;
|
||||
use Facebook\WebDriver\WebDriverExpectedCondition;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
abstract class TestCase extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
use WebDriverShortcuts;
|
||||
|
||||
/**
|
||||
* @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());
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
protected function loginAndWait()
|
||||
{
|
||||
$this->login();
|
||||
$this->waitUntil(WebDriverExpectedCondition::textToBePresentInElement(
|
||||
WebDriverBy::cssSelector('#userBadge > a.view-profile.control > span'), 'Koel Admin'
|
||||
));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to allow going to a specific screen.
|
||||
*
|
||||
* @param $screen
|
||||
*
|
||||
* @return \Facebook\WebDriver\Remote\RemoteWebElement
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function goto($screen)
|
||||
{
|
||||
if ($screen === 'favorites') {
|
||||
$this->click('#sidebar .favorites a');
|
||||
} else {
|
||||
$this->click("#sidebar a.$screen");
|
||||
}
|
||||
|
||||
$this->waitUntil(WebDriverExpectedCondition::visibilityOfElementLocated(
|
||||
WebDriverBy::cssSelector("#{$screen}Wrapper")
|
||||
));
|
||||
}
|
||||
|
||||
protected function waitForUserInput()
|
||||
{
|
||||
if (trim(fgets(fopen('php://stdin', 'rb'))) !== chr(13)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected function focusIntoApp()
|
||||
{
|
||||
$this->click('#app');
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->driver->get($this->url);
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
{
|
||||
$this->driver->quit();
|
||||
}
|
||||
}
|
114
tests/e2e/WebDriverShortcuts.php
Normal file
114
tests/e2e/WebDriverShortcuts.php
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace E2E;
|
||||
|
||||
use Facebook\WebDriver\Interactions\Internal\WebDriverDoubleClickAction;
|
||||
use Facebook\WebDriver\Remote\RemoteWebDriver;
|
||||
use Facebook\WebDriver\WebDriverBy;
|
||||
use Facebook\WebDriver\WebDriverElement;
|
||||
use Facebook\WebDriver\WebDriverKeys;
|
||||
|
||||
trait WebDriverShortcuts
|
||||
{
|
||||
/**
|
||||
* @var RemoteWebDriver
|
||||
*/
|
||||
protected $driver;
|
||||
|
||||
/**
|
||||
* @param $selector WebDriverElement|string
|
||||
*
|
||||
* @return WebDriverElement
|
||||
*/
|
||||
protected function el($selector)
|
||||
{
|
||||
if (is_string($selector)) {
|
||||
return $this->driver->findElement(WebDriverBy::cssSelector($selector));
|
||||
}
|
||||
|
||||
return $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)
|
||||
{
|
||||
$this->click($element)->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 rightClick($element)
|
||||
{
|
||||
return $this->driver->getMouse()->contextClick($this->el($element)->getCoordinates());
|
||||
}
|
||||
|
||||
protected function doubleClick($element)
|
||||
{
|
||||
$action = new WebDriverDoubleClickAction($this->driver->getMouse(), $this->el($element));
|
||||
|
||||
$action->perform();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep (implicit wait) for some seconds.
|
||||
*
|
||||
* @param $seconds
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
protected function back()
|
||||
{
|
||||
return $this->driver->navigate()->back();
|
||||
}
|
||||
|
||||
protected function forward()
|
||||
{
|
||||
return $this->driver->navigate()->forward();
|
||||
}
|
||||
|
||||
protected function refresh()
|
||||
{
|
||||
return $this->driver->navigate()->refresh();
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue