More tests

This commit is contained in:
An Phan 2016-11-15 15:54:41 +08:00
parent ef618a611b
commit 8b7c226343
No known key found for this signature in database
GPG key ID: 05536BB4BCDC02A2
18 changed files with 806 additions and 235 deletions

View file

@ -33,7 +33,7 @@
"autoload-dev": {
"classmap": [
"tests/TestCase.php",
"tests/E2E/TestCase.php"
"tests/E2E"
]
},
"scripts": {

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": "echo 'remember to run `php artisan serve --port=8081`' && phpunit tests/e2e -c phpunit.e2e.xml"
"e2e": "echo 'Remember to run `php artisan serve --port=8081`' && phpunit tests/e2e -c phpunit.e2e.xml"
}
}

12
resources/assets/js/.idea/js.iml generated Normal file
View 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>

View 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
View 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
View 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>

View 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
View 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>

View file

@ -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 {

View file

@ -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/>

View file

@ -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">

View file

@ -3,15 +3,14 @@
class="song-item"
draggable="true"
:data-song-id="song.id"
key="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>
@ -38,6 +37,12 @@ export default {
};
},
computed: {
playing() {
return this.song.playbackState === 'playing' || this.song.playbackState === 'paused';
},
},
methods: {
/**
* Play the song right away.
@ -67,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>

View file

@ -26,16 +26,21 @@
<i class="fa fa-angle-down" v-show="sortingByAlbum && order > 0"/>
<i class="fa fa-angle-up" v-show="sortingByAlbum && order < 0"/>
</th>
<th @click="sort('fmtLength')" class="time">Time
<i class="fa fa-angle-down" v-show="sortKey === 'fmtLength' && order > 0"/>
<i class="fa fa-angle-up" v-show="sortKey === 'fmtLength' && order < 0"/>
<th @click="sort('length')" class="time">Time
<i class="fa fa-angle-down" v-show="sortKey === 'length' && order > 0"/>
<i class="fa fa-angle-up" v-show="sortKey === 'length' && order < 0"/>
</th>
<th class="play"></th>
</tr>
</thead>
<tbody>
<tr is="song-item" v-for="item in displayedItems" :song="item" ref="rows"/>
<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: [],
@ -117,7 +121,7 @@ export default {
/**
* Handle sorting the song list.
*
* @param {String} key The sort key. Can be 'title', 'album', 'artist', or 'fmtLength'
* @param {String} key The sort key. Can be 'title', 'album', 'artist', or 'length'
*/
sort(key) {
if (this.sortable === false) {
@ -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.

View file

@ -2,12 +2,9 @@
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
{
@ -35,9 +32,8 @@ class KoelTest extends TestCase
// Default URL must be Home
static::assertEquals($this->url.'/#!/home', $this->driver->getCurrentURL());
// $this->_testSideBar();
// $this->_testHomeScreen();
return $this->_testQueueScreen();
$this->_testSideBar();
$this->_testHomeScreen();
// While we're at this, test logging out as well.
$this->click('#userBadge > a.logout');
@ -49,8 +45,8 @@ class KoelTest extends TestCase
private function _testSideBar()
{
// All basic navigation
foreach(['home', 'queue', 'songs', 'albums', 'artists', 'youtube', 'settings', 'users'] as $screen) {
$this->goTo($screen);
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;
});
@ -74,11 +70,8 @@ class KoelTest extends TestCase
$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->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');
@ -88,87 +81,4 @@ class KoelTest extends TestCase
$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());
}
}

View 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'));
}
}

195
tests/e2e/SongListTest.php Normal file
View 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();
}
}

View file

@ -3,22 +3,16 @@
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 Facebook\WebDriver\WebDriverExpectedCondition;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Artisan;
class TestCase extends \PHPUnit_Framework_TestCase
{
/**
* @var RemoteWebDriver
*/
protected $driver;
use WebDriverShortcuts;
/**
* @var Application
@ -42,8 +36,6 @@ class TestCase extends \PHPUnit_Framework_TestCase
$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));
}
/**
@ -72,85 +64,6 @@ class TestCase extends \PHPUnit_Framework_TestCase
}
}
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.
*
@ -164,27 +77,57 @@ class TestCase extends \PHPUnit_Framework_TestCase
$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)
protected function goto($screen)
{
if ($screen === 'favorites') {
return $this->click('#sidebar .favorites a');
$this->click('#sidebar .favorites a');
} else {
return $this->click("#sidebar a.$screen");
$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 clearQueue()
protected function focusIntoApp()
{
$this->goTo('queue');
if ($this->els('#queueWrapper .song-item')) {
$this->click('#queueWrapper > h1 > div > button.btn.btn-red');
}
$this->click('#app');
}
public function setUp()
{
$this->driver->get($this->url);
}
public function tearDown()
{
$this->driver->quit();
}
}

View 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_a($selector, WebDriverElement::class)) {
return $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)
{
$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();
}
}