chore: refactor event bus and mixins
|
@ -1,2 +0,0 @@
|
|||
libs
|
||||
tests
|
3
.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
|||
[submodule "resources/assets"]
|
||||
path = resources/assets
|
||||
url = https://github.com/koel/core.git
|
98
package.json
|
@ -13,42 +13,106 @@
|
|||
"type": "git",
|
||||
"url": "https://github.com/koel/koel"
|
||||
},
|
||||
"dependencies": {
|
||||
"alertify.js": "^1.0.12",
|
||||
"axios": "^0.21.1",
|
||||
"blueimp-md5": "^2.3.0",
|
||||
"compare-versions": "^3.5.1",
|
||||
"font-awesome": "^4.7.0",
|
||||
"intersection-observer": "^0.2.0",
|
||||
"ismobilejs": "^0.4.0",
|
||||
"local-storage": "^2.0.0",
|
||||
"lodash": "^4.17.19",
|
||||
"mitt": "^3.0.0",
|
||||
"nouislider": "^14.0.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"plyr": "1.5.x",
|
||||
"pusher-js": "^4.1.0",
|
||||
"select": "^1.0.6",
|
||||
"sketch-js": "^1.1.3",
|
||||
"slugify": "^1.0.2",
|
||||
"vue": "^3.2.32",
|
||||
"vue-global-events": "^1.0.2",
|
||||
"vue-virtual-scroller": "^2.0.0-alpha.1",
|
||||
"vuequery": "~2.1.1",
|
||||
"youtube-player": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/cypress": "^7.0.6",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/polyfill": "^7.8.7",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@testing-library/cypress": "^8.0.2",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/blueimp-md5": "^2.7.0",
|
||||
"@types/faker": "^4.1.11",
|
||||
"@types/jest": "^26",
|
||||
"@types/local-storage": "^1.4.0",
|
||||
"@types/lodash": "^4.14.150",
|
||||
"@types/node": "^13.13.4",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/pusher-js": "^4.2.2",
|
||||
"@types/wicg-mediasession": "^1.1.0",
|
||||
"@types/youtube-player": "^5.5.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.11.1",
|
||||
"@typescript-eslint/parser": "^4.11.1",
|
||||
"cross-env": "^3.2.3",
|
||||
"cypress": "^7.3.0",
|
||||
"@vue/compiler-sfc": "^3.2.32",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"@vue/test-utils": "^1.0.0-beta.25",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"crypto-random-string": "^1.0.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"cypress": "^9.5.4",
|
||||
"cypress-file-upload": "^4.1.1",
|
||||
"eslint": "^7.17.0",
|
||||
"eslint-config-vue": "^2.0.1",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-jest": "^22.0.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"factoria": "^3.1.3",
|
||||
"file-loader": "^1.1.6",
|
||||
"font-awesome": "^4.7.0",
|
||||
"husky": "^4.2.5",
|
||||
"kill-port": "^1.6.1",
|
||||
"laravel-mix": "^5.0.4",
|
||||
"laravel-mix": "^6.0.43",
|
||||
"lint-staged": "^10.3.0",
|
||||
"postcss": "^8.4.12",
|
||||
"resolve-url-loader": "^3.1.1",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sass": "^1.50.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"start-server-and-test": "^1.11.7",
|
||||
"ts-loader": "^7.0.1",
|
||||
"typescript": "^3.8.3",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.42.1",
|
||||
"webpack-node-externals": "^1.6.0"
|
||||
"ts-loader": "^9.2.8",
|
||||
"typescript": "^4.6.3",
|
||||
"vue-loader": "^16.2.0",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vue-test-helpers": "^2.0.0",
|
||||
"webpack": "^5.72.0",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint ./cypress/**/*.ts",
|
||||
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"watch-poll": "yarn watch -- --watch-poll",
|
||||
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"dev": "start-test 'php artisan serve --port=8000 --quiet' :8000 hot",
|
||||
"watch.bak": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"watch-poll.bak": "yarn watch -- --watch-poll",
|
||||
"hot.bak": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"dev.bak": "start-test 'php artisan serve --port=8000 --quiet' :8000 hot",
|
||||
"test:e2e": "kill-port 8080 && start-test dev :8080 'cypress open'",
|
||||
"test:e2e:ci": "kill-port 8080 && start-test 'php artisan serve --port=8080 --quiet' :8080 'cypress run'",
|
||||
"build": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"build-demo": "cross-env NODE_ENV=demo node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js -p",
|
||||
"production": "yarn build"
|
||||
"production.bak": "yarn build",
|
||||
"dev": "npm run development",
|
||||
"development": "mix",
|
||||
"watch": "mix watch",
|
||||
"watch-poll": "mix watch -- --watch-options-poll=1000",
|
||||
"hot": "mix watch --hot",
|
||||
"prod": "npm run production",
|
||||
"production": "mix --production"
|
||||
},
|
||||
"dependencies": {},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 853396f2b17cfaa420de772d5534f8eb2fce5ff2
|
5
resources/assets/.babelrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"presets": [
|
||||
"@babel/preset-env"
|
||||
]
|
||||
}
|
2
resources/assets/.eslintignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
js/libs
|
||||
js/tests/__coverage__
|
|
@ -1,14 +1,26 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parser": "vue-eslint-parser",
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"@vue/standard",
|
||||
"@vue/typescript/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"globals": {
|
||||
"KOEL_ENV": true,
|
||||
"NODE_ENV": true,
|
||||
"HTMLElement": true,
|
||||
"FileReader": true
|
||||
},
|
||||
"rules": {
|
||||
"camelcase": 0,
|
||||
"no-multi-str": 0,
|
||||
|
@ -21,6 +33,8 @@
|
|||
"@typescript-eslint/no-inferrable-types": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/ban-ts-ignore": 0
|
||||
"@typescript-eslint/ban-ts-ignore": 0,
|
||||
"vue/no-side-effects-in-computed-properties": 0,
|
||||
"vue/valid-v-on": 0
|
||||
}
|
||||
}
|
4
resources/assets/.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [phanan]
|
||||
open_collective: [koel]
|
10
resources/assets/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Hey ya! Thanks for contributing to this project. Unless the issue is _very_ specific to this repo, you may want to report it under the main [phanan/koel](https://github.com/phanan/koel/issues/new/choose) instead. Cheers!
|
23
resources/assets/.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
name: main
|
||||
on: [ push, workflow_dispatch ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [ 10, 11, 12, 13, 14, 15 ]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Run static type checking
|
||||
run: yarn type-check
|
||||
- name: Run unit tests
|
||||
run: yarn test --forceExit # Jest won't exit in Node 15 for whatever reason
|
||||
- name: Collect coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
35
resources/assets/.gitignore
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
node_modules
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*node_modules
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
### OSX ###
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
### Sass ###
|
||||
.sass-cache/
|
||||
*.css.map
|
||||
|
||||
.nyc_output
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
__coverage__
|
3
resources/assets/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# koel-core [![Build Status](https://github.com/koel/core/workflows/main/badge.svg)](https://github.com/koel/core/actions) [![codecov](https://codecov.io/gh/koel/core/branch/master/graph/badge.svg)](https://codecov.io/gh/koel/core)
|
||||
|
||||
The core components and assets shared by the [web](https://github.com/koel/koel) and [desktop](https://github.com/koel/app) versions of Koel.
|
1
resources/assets/css/meyer-reset.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}table{border-collapse:collapse;border-spacing:0}
|
0
resources/assets/img/artists/.gitkeep
Normal file
BIN
resources/assets/img/bars.gif
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
resources/assets/img/covers/unknown-album.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
resources/assets/img/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
resources/assets/img/icon.png
Normal file
After Width: | Height: | Size: 11 KiB |
20
resources/assets/img/itunes.svg
Executable file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="176px" height="177px" viewBox="0 0 176 177" enable-background="new 0 0 176 177" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#fff" d="M88,0.5c-48.602,0-88,39.399-88,88c0,48.601,39.398,88,88,88c48.601,0,88-39.399,88-88
|
||||
C176,39.899,136.601,0.5,88,0.5z M88,169.108c-44.52,0-80.608-36.09-80.608-80.608C7.392,43.981,43.48,7.892,88,7.892
|
||||
c44.519,0,80.608,36.089,80.608,80.608C168.608,133.019,132.519,169.108,88,169.108z M126.254,31.462
|
||||
c-1.221-1.033-3.796-0.399-3.796-0.399L70.117,41.451c0,0-2.253,0.057-3.795,1.998c-1.083,1.363-0.999,3.796-0.999,3.796v62.129
|
||||
c0,0,0.188,2.209-1.398,3.796c-1.843,1.842-6.479,2.14-10.588,3.196c-6.84,1.76-13.585,4.634-13.585,12.985
|
||||
c0,5.86,2.965,13.186,13.186,13.186c12.313,0,18.578-6.789,18.578-15.982c0-5.952,0-7.392,0-7.392l0.2-47.346
|
||||
c0,0-0.299-2.608,0.6-3.596c1.264-1.389,3.995-1.598,3.995-1.598l40.354-8.19c0,0,2.452-0.845,3.596,0
|
||||
c1.154,0.853,0.999,3.396,0.999,3.396v36.159c0,0,0.219,2.379-0.999,3.596c-2.288,2.289-6.141,2.242-9.988,3.196
|
||||
c-7.225,1.792-14.783,4.386-14.783,13.785c0,9.929,9.67,12.785,12.785,12.785c13.62,0,19.378-7.352,19.378-16.182V35.458
|
||||
C127.652,35.458,127.622,32.62,126.254,31.462z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
resources/assets/img/logo.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
31
resources/assets/img/logo.svg
Normal file
|
@ -0,0 +1,31 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" width="443" height="443" viewBox="0 0 443 443">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1, .cls-4 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2, .cls-3 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #fa0000;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M341.199,181.514 C341.199,181.514 329.240,198.397 364.162,215.016 C364.162,215.016 310.908,209.376 296.786,195.998 C296.786,195.998 204.949,208.024 241.644,296.184 C241.644,296.184 211.447,274.452 202.192,246.099 C202.192,246.099 133.385,318.336 179.528,436.617 C179.528,436.617 129.141,382.779 122.046,329.902 C122.046,329.902 117.782,338.194 119.576,350.880 C119.576,350.880 99.298,292.397 104.504,261.508 C104.504,261.508 92.138,278.643 91.378,288.332 C91.378,288.332 93.107,266.615 101.661,253.245 C101.661,253.245 99.170,233.867 107.680,213.575 C116.171,193.332 113.463,171.640 111.827,167.782 C111.827,167.782 109.198,176.572 105.433,180.351 C105.433,180.351 117.471,132.757 112.356,109.929 C112.356,109.929 101.837,120.099 103.373,145.597 C103.373,145.597 97.287,123.077 103.298,98.997 C103.298,98.997 97.916,99.526 97.476,105.016 C97.476,105.016 91.763,89.607 99.801,71.864 C99.801,71.864 53.861,41.051 49.370,37.405 C49.370,37.405 42.748,34.052 34.576,34.782 C60.449,24.014 94.715,42.762 115.899,56.843 C127.423,64.850 133.085,69.917 145.649,73.271 C158.380,76.669 169.714,87.898 169.714,87.898 C156.987,69.315 152.956,79.582 118.199,54.835 C68.767,19.873 41.714,29.300 32.237,35.104 C31.001,35.334 29.742,35.658 28.475,36.118 C28.475,36.118 48.041,-5.329 130.152,21.206 C130.152,21.206 142.859,8.268 150.718,7.000 C150.718,7.000 139.765,14.805 137.775,21.256 C137.775,21.256 153.368,9.960 166.082,10.086 C166.082,10.086 151.494,17.506 144.164,24.376 C144.164,24.376 153.849,19.212 167.805,19.383 C167.805,19.383 159.135,23.576 156.156,25.016 C155.536,25.315 155.162,25.496 155.162,25.496 C155.162,25.496 163.938,23.635 168.699,23.535 C168.699,23.535 192.437,12.135 213.904,14.367 C213.904,14.367 206.439,14.422 196.089,22.692 C196.089,22.692 228.441,20.998 250.117,31.701 C250.117,31.701 231.766,28.100 220.340,28.180 C220.340,28.180 276.182,30.408 322.808,91.274 C371.863,155.310 373.908,160.510 413.969,190.628 C413.969,190.628 365.515,188.519 341.199,181.514 Z" class="cls-1"/>
|
||||
<path d="M307.000,158.000 C307.000,158.000 298.389,172.789 324.000,184.000 C324.000,184.000 284.902,183.184 274.000,173.000 C274.000,173.000 202.346,191.137 234.000,261.000 C234.000,261.000 209.486,246.084 201.000,223.000 C201.000,223.000 147.602,292.965 190.000,382.000 C190.000,382.000 146.674,346.092 138.000,303.000 C138.000,303.000 134.801,310.563 137.000,321.000 C137.000,321.000 116.229,273.617 119.000,246.000 C119.000,246.000 109.084,262.424 109.000,271.000 C109.000,271.000 109.211,251.727 116.000,239.000 C116.000,239.000 112.623,221.998 119.000,203.000 C125.377,184.002 121.697,164.428 120.000,161.000 C120.000,161.000 118.160,169.258 115.000,173.000 C115.000,173.000 123.014,128.197 117.000,107.000 C117.000,107.000 107.994,117.129 111.000,141.000 C111.000,141.000 103.994,120.180 108.000,97.000 C108.000,97.000 103.051,97.730 103.000,103.000 C103.000,103.000 96.660,88.441 103.000,71.000 C103.000,71.000 56.725,41.609 52.000,38.000 C52.000,38.000 45.130,34.687 37.006,35.551 C61.771,24.269 96.266,42.627 117.000,56.000 C128.061,63.455 133.500,68.163 145.000,71.000 C156.500,73.837 167.000,84.000 167.000,84.000 C154.894,66.930 151.844,76.752 119.000,54.000 C69.846,20.172 43.925,29.888 34.829,35.879 C33.563,36.132 32.281,36.495 31.000,37.000 C31.000,37.000 47.998,-5.445 128.000,21.000 C128.000,21.000 138.914,8.246 146.000,7.000 C146.000,7.000 136.453,14.670 135.000,21.000 C135.000,21.000 148.561,9.895 160.000,10.000 C160.000,10.000 147.270,17.262 141.000,24.000 C141.000,24.000 149.486,18.910 162.000,19.000 C162.000,19.000 154.466,23.109 151.868,24.526 C151.327,24.822 151.000,25.000 151.000,25.000 C151.000,25.000 158.766,23.131 163.000,23.000 C163.000,23.000 183.369,11.930 202.000,14.000 C202.000,14.000 195.598,14.076 187.000,22.000 C187.000,22.000 214.584,20.193 233.000,30.000 C233.000,30.000 217.627,26.824 208.000,27.000 C208.000,27.000 254.348,28.752 292.000,82.000 C329.652,135.248 330.877,138.893 359.000,161.000 C359.000,161.000 324.770,162.277 307.000,158.000 Z" class="cls-2"/>
|
||||
<path d="M118.000,37.000 C118.000,37.000 125.482,30.131 128.000,41.000 C128.000,41.000 124.428,44.504 122.000,43.000 C119.572,41.496 118.000,37.000 118.000,37.000 Z" class="cls-1"/>
|
||||
<g>
|
||||
<path d="M184.000,67.000 C184.000,67.000 189.596,53.143 207.000,58.000 C221.245,62.388 222.000,74.000 222.000,74.000 C222.000,74.000 215.773,89.560 204.000,88.000 C192.227,86.440 184.492,76.630 184.000,67.000 Z" class="cls-3"/>
|
||||
<path d="M200.055,61.063 C207.079,60.796 213.823,64.926 214.031,70.403 C214.243,75.962 207.620,79.457 200.388,78.313 C194.177,77.332 189.733,73.180 189.712,68.967 C189.691,64.801 194.001,61.293 200.055,61.063 Z" class="cls-2"/>
|
||||
<ellipse cx="196.5" cy="65" rx="4.5" ry="4" class="cls-4"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.1 KiB |
BIN
resources/assets/img/themes/bg-cat.jpg
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
resources/assets/img/themes/bg-jungle.jpg
Normal file
After Width: | Height: | Size: 130 KiB |
BIN
resources/assets/img/themes/bg-mountains.jpg
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
resources/assets/img/themes/bg-nemo.jpg
Normal file
After Width: | Height: | Size: 130 KiB |
BIN
resources/assets/img/themes/bg-pines.jpg
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
resources/assets/img/themes/bg-pop-culture.jpg
Normal file
After Width: | Height: | Size: 111 KiB |
1
resources/assets/img/themes/bg-purple-waves.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 800" xmlns:v="https://vecta.io/nano"><path fill="#5b0f6e" d="M486 705.8c-109.3-21.8-223.4-32.2-335.3-19.4C99.5 692.1 49 703 0 719.8V800h843.8c-115.9-33.2-230.8-68.1-347.6-92.2l-10.2-2z"/><path fill="#540c68" d="M1600 0H0v719.8C49 703 99.5 692 150.7 686.3c111.9-12.7 226-2.4 335.3 19.4l10.2 2c116.8 24 231.7 59 347.6 92.2H1600V0z"/><path fill="#4d0861" d="M478.4 581l9.5 2.5C684.1 636 876.6 717 1081.4 760.1c174.2 36.6 349.5 29.2 518.6-10.2V0H0v574.9c52.3-17.6 106.5-27.7 161.1-30.9 107.3-6.6 214.6 10.2 317.3 37z"/><path fill="#45055a" d="M0 0v429.4c55.6-18.4 113.5-27.3 171.4-27.7 102.8-.8 203.2 22.7 299.3 54.5l8.9 3c183.6 62 365.7 146.1 562.4 192.1 186.7 43.7 376.3 34.4 557.9-12.6V0H0z"/><path fill="#3e0354" d="M181.8 259.4c98.2 6 191.9 35.2 281.3 72.1 2.8 1.1 5.5 2.3 8.3 3.4 171 71.6 342.7 158.5 531.3 207.7 198.8 51.8 403.4 40.8 597.3-14.8V0H0v283.2a483.5 483.5 0 0 1 181.8-23.8z"/><path fill="#340745" d="M1600 0H0v136.3c62.3-20.9 127.7-27.5 192.2-19.2 93.6 12.1 180.5 47.7 263.3 89.6l7.7 3.9c158.4 81.1 319.7 170.9 500.3 223.2 210.5 61 430.8 49 636.6-16.6V0z"/><path fill="#2a0a36" d="M454.9 86.3C600.7 177 751.6 269.3 924.1 325c208.6 67.4 431.3 60.8 637.9-5.3 12.8-4.1 25.4-8.4 38.1-12.9V0h-1312c56 21.3 108.7 50.6 159.7 82 2.4 1.4 4.7 2.9 7.1 4.3z"/><path fill="#210a28" d="M1600 0H498c118.1 85.8 243.5 164.5 386.8 216.2c191.8 69.2 400 74.7 595 21.1c40.8-11.2 81.1-25.2 120.3-41.7V0z"/><path fill="#18061a" d="M1397.5 154.8c47.2-10.6 93.6-25.3 138.6-43.8 21.7-8.9 43-18.8 63.9-29.5V0H643.4c62.9 41.7 129.7 78.2 202.1 107.4 174.9 70.7 368.7 88.7 552 47.4z"/><path fill="#0a0109" d="M1315.3 72.4c75.3-12.6 148.9-37.1 216.8-72.4h-723c157.7 71 335.6 101 506.2 72.4z"/></svg>
|
After Width: | Height: | Size: 1.7 KiB |
1
resources/assets/img/themes/bg-rose-petals.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 400" xmlns:v="https://vecta.io/nano"><defs><radialGradient id="A" cx="396" cy="281" r="514" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d18"/><stop offset="1" stop-color="#300"/></radialGradient><linearGradient id="B" gradientUnits="userSpaceOnUse" x1="400" y1="148" x2="400" y2="333"><stop offset="0" stop-color="#fa3" stop-opacity="0"/><stop offset="1" stop-color="#fa3" stop-opacity=".5"/></linearGradient></defs><path fill="url(#A)" d="M0 0h800v400H0z"/><g fill-opacity=".4" fill="url(#B)"><circle cx="267.5" cy="61" r="300"/><circle cx="532.5" cy="61" r="300"/><circle cx="400" cy="30" r="300"/></g></svg>
|
After Width: | Height: | Size: 685 B |
BIN
resources/assets/img/tile-wide.png
Executable file
After Width: | Height: | Size: 3.2 KiB |
BIN
resources/assets/img/tile.png
Executable file
After Width: | Height: | Size: 6.3 KiB |
40
resources/assets/jest.config.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* By default jest doesn't transform files in node_modules.
|
||||
* List names of the libraries we want to whitelist here, e.g., those export ES6 modules.
|
||||
*/
|
||||
const forceTransformModules = [
|
||||
'@phanan/vuebus'
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
moduleFileExtensions: [
|
||||
'ts',
|
||||
'js',
|
||||
'vue'
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/js/$1'
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': 'ts-jest',
|
||||
'.*\\.(vue)$': 'vue-jest',
|
||||
'^.+\\.(svg|gif|jpg|png)$': '<rootDir>/js/__tests__/__transformers__/image.js'
|
||||
},
|
||||
snapshotSerializers: [
|
||||
'<rootDir>/node_modules/jest-serializer-vue'
|
||||
],
|
||||
testMatch: ['**/__tests__/**/*.spec.ts'],
|
||||
transformIgnorePatterns: [
|
||||
`node_modules/(?!(${forceTransformModules.join('|')})/)`
|
||||
],
|
||||
globals: {
|
||||
KOEL_ENV: 'web',
|
||||
NODE_ENV: 'test'
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/js/__tests__/setup.ts'],
|
||||
verbose: true,
|
||||
collectCoverage: true,
|
||||
coverageReporters: ['lcov', 'json', 'html'],
|
||||
coverageDirectory: '<rootDir>/js/__tests__/__coverage__',
|
||||
coveragePathIgnorePatterns: ['/node_modules/', '/__tests__/', '/stubs/', '/libs/']
|
||||
}
|
13
resources/assets/js/__tests__/.eslintrc
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"plugins": ["jest"],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"globals": {
|
||||
"noop": true,
|
||||
"shallow": true,
|
||||
"mount": true,
|
||||
"Vue": true,
|
||||
"Event": true
|
||||
}
|
||||
}
|
2
resources/assets/js/__tests__/__helpers__/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './noop'
|
||||
export * from './mock'
|
18
resources/assets/js/__tests__/__helpers__/mock.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import FunctionNames = jest.FunctionPropertyNames
|
||||
import { noop } from '@/utils'
|
||||
|
||||
export const mock = <T extends {}, M extends FunctionNames<Required<T>>>(
|
||||
object: T,
|
||||
method: M,
|
||||
implementation: any = noop
|
||||
) => {
|
||||
const m = jest.spyOn(object, method)
|
||||
|
||||
if (implementation instanceof Function) {
|
||||
m.mockImplementation(implementation)
|
||||
} else {
|
||||
m.mockImplementation((): any => implementation)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
2
resources/assets/js/__tests__/__helpers__/noop.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
/* eslint @typescript-eslint/no-empty-function: 0 */
|
||||
export const noop = () => {}
|
8
resources/assets/js/__tests__/__mocks__/axios.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
get: jest.fn((): Promise<void> => Promise.resolve()),
|
||||
post: jest.fn((): Promise<void> => Promise.resolve()),
|
||||
patch: jest.fn((): Promise<void> => Promise.resolve()),
|
||||
put: jest.fn((): Promise<void> => Promise.resolve()),
|
||||
delete: jest.fn((): Promise<void> => Promise.resolve()),
|
||||
request: jest.fn(() => Promise.resolve({ data: [] }))
|
||||
}
|
14
resources/assets/js/__tests__/__mocks__/lodash.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/* eslint @typescript-eslint/no-unused-vars: 0 */
|
||||
import _, { Cancelable } from 'lodash'
|
||||
|
||||
_.orderBy = jest.fn(<T>(collection: T[]): T[] => collection)
|
||||
|
||||
_.shuffle = jest.fn(<T>(collection: T[]): T[] => collection)
|
||||
|
||||
_.throttle = jest.fn((fn: Function, wait: number): any => fn)
|
||||
|
||||
_.sample = jest.fn(<T>(collection: T[]): T | undefined => {
|
||||
return collection.length ? collection[0] : undefined
|
||||
})
|
||||
|
||||
module.exports = _
|
9
resources/assets/js/__tests__/__transformers__/image.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
process () {
|
||||
return 'module.exports = {};'
|
||||
},
|
||||
|
||||
getCacheKey () {
|
||||
return 'imageTransform'
|
||||
}
|
||||
}
|
35
resources/assets/js/__tests__/adapter.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import Vue from 'vue'
|
||||
import { Wrapper as BaseWrapper, WrapperArray as BaseWrapperArray, VueClass } from '@vue/test-utils/types/index'
|
||||
import { mount as baseMount, shallowMount, MountOptions } from '@vue/test-utils'
|
||||
|
||||
export interface Wrapper extends BaseWrapper<Vue> {
|
||||
readonly vm: Vue
|
||||
value: string
|
||||
has(what: any): boolean
|
||||
html(): string
|
||||
text(): string
|
||||
click(selector?: string, options?: any): Wrapper
|
||||
change(selector?: string): Wrapper
|
||||
dblclick(selector?: string): Wrapper
|
||||
submit (selector?: string): Wrapper
|
||||
find(any: any): Wrapper
|
||||
setValue(value: string): Wrapper
|
||||
input(selector?: string, options?: any): Wrapper
|
||||
blur(selector?: string): Wrapper
|
||||
hasAll(...args: any): Wrapper
|
||||
hasNone(...args: any): Wrapper
|
||||
findAll(...args: any): WrapperArray
|
||||
hasEmitted(event: string, data?: any): Wrapper
|
||||
}
|
||||
|
||||
export interface WrapperArray extends BaseWrapperArray<Vue> {
|
||||
at(index: number): Wrapper
|
||||
}
|
||||
|
||||
export const mount = (component: VueClass<Vue>, options: MountOptions<Vue> = {}): Wrapper => {
|
||||
return baseMount(component, options) as Wrapper
|
||||
}
|
||||
|
||||
export const shallow = (component: VueClass<Vue>, options: MountOptions<Vue> = {}): Wrapper => {
|
||||
return shallowMount(component, options) as Wrapper
|
||||
}
|
222
resources/assets/js/__tests__/blobs/data.ts
Normal file
|
@ -0,0 +1,222 @@
|
|||
import factory from '@/__tests__/factory'
|
||||
|
||||
const currentUser = factory<User>('user', {
|
||||
id: 1,
|
||||
name: 'Phan An',
|
||||
email: 'me@phanan.net',
|
||||
is_admin: true
|
||||
})
|
||||
|
||||
export default {
|
||||
artists: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Unknown Artist'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Various Artists'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'All-4-One'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Boy Dylan'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'James Blunt'
|
||||
}
|
||||
],
|
||||
albums: [
|
||||
{
|
||||
id: 1193,
|
||||
artist_id: 3,
|
||||
name: 'All-4-One',
|
||||
cover: '/img/covers/565c0f7067425.jpeg'
|
||||
},
|
||||
{
|
||||
id: 1194,
|
||||
artist_id: 3,
|
||||
name: 'And The Music Speaks',
|
||||
cover: '/img/covers/unknown-album.png'
|
||||
},
|
||||
{
|
||||
id: 1195,
|
||||
artist_id: 3,
|
||||
name: 'Space Jam',
|
||||
cover: '/img/covers/565c0f7115e0f.png'
|
||||
},
|
||||
{
|
||||
id: 1217,
|
||||
artist_id: 4,
|
||||
name: 'Highway 61 Revisited',
|
||||
cover: '/img/covers/565c0f76dc6e8.jpeg'
|
||||
},
|
||||
{
|
||||
id: 1218,
|
||||
artist_id: 4,
|
||||
name: 'Pat Garrett & Billy the Kid',
|
||||
cover: '/img/covers/unknown-album.png'
|
||||
},
|
||||
{
|
||||
id: 1219,
|
||||
artist_id: 4,
|
||||
name: "The Times They Are A-Changin",
|
||||
cover: '/img/covers/unknown-album.png'
|
||||
},
|
||||
{
|
||||
id: 1268,
|
||||
artist_id: 5,
|
||||
name: 'Back To Bedlam',
|
||||
cover: '/img/covers/unknown-album.png'
|
||||
}
|
||||
],
|
||||
|
||||
songs: [
|
||||
{
|
||||
id: '39189f4545f9d5671fb3dc964f0080a0',
|
||||
album_id: 1193,
|
||||
artist_id: 3,
|
||||
title: 'I Swear',
|
||||
length: 259.92,
|
||||
playCount: 4
|
||||
},
|
||||
{
|
||||
id: 'a6a550f7d950d2a2520f9bf1a60f025a',
|
||||
album_id: 1194,
|
||||
artist_id: 3,
|
||||
title: 'I can love you like that',
|
||||
length: 262.61,
|
||||
playCount: 2
|
||||
},
|
||||
{
|
||||
id: 'd86c30fd34f13c1aff8db59b7fc9c610',
|
||||
album_id: 1195,
|
||||
artist_id: 3,
|
||||
title: 'I turn to you',
|
||||
length: 293.04
|
||||
},
|
||||
{
|
||||
id: 'e6d3977f3ffa147801ca5d1fdf6fa55e',
|
||||
album_id: 1217,
|
||||
artist_id: 4,
|
||||
title: 'Like a rolling stone',
|
||||
length: 373.63
|
||||
},
|
||||
{
|
||||
id: 'aa16bbef6a9710eb9a0f41ecc534fad5',
|
||||
album_id: 1218,
|
||||
artist_id: 4,
|
||||
title: "Knockin' on heaven's door",
|
||||
length: 151.9
|
||||
},
|
||||
{
|
||||
id: 'cb7edeac1f097143e65b1b2cde102482',
|
||||
album_id: 1219,
|
||||
artist_id: 4,
|
||||
title: "The times they are a-changin'",
|
||||
length: 196
|
||||
},
|
||||
{
|
||||
id: '0ba9fb128427b32683b9eb9140912a70',
|
||||
album_id: 1268,
|
||||
artist_id: 5,
|
||||
title: 'No bravery',
|
||||
length: 243.12
|
||||
},
|
||||
{
|
||||
id: '123fd1ad32240ecab28a4e86ed5173',
|
||||
album_id: 1268,
|
||||
artist_id: 5,
|
||||
title: 'So long, Jimmy',
|
||||
length: 265.04
|
||||
},
|
||||
{
|
||||
id: '6a54c674d8b16732f26df73f59c63e21',
|
||||
album_id: 1268,
|
||||
artist_id: 5,
|
||||
title: 'Wisemen',
|
||||
length: 223.14
|
||||
},
|
||||
{
|
||||
id: '6df7d82a9a8701e40d1c291cf14a16bc',
|
||||
album_id: 1268,
|
||||
artist_id: 5,
|
||||
title: 'Goodbye my lover',
|
||||
length: 258.61
|
||||
},
|
||||
{
|
||||
id: '74a2000d343e4587273d3ad14e2fd741',
|
||||
album_id: 1268,
|
||||
artist_id: 5,
|
||||
title: 'High',
|
||||
length: 245.86
|
||||
},
|
||||
{
|
||||
id: '7900ab518f51775fe6cf06092c074ee5',
|
||||
album_id: 1268,
|
||||
artist_id: 5,
|
||||
title: "You're beautiful",
|
||||
length: 213.29
|
||||
},
|
||||
{
|
||||
id: '803910a51f9893347e087af851e38777',
|
||||
album_id: 1268,
|
||||
artist_id: 5,
|
||||
title: 'Cry',
|
||||
length: 246.91
|
||||
},
|
||||
{
|
||||
id: 'd82b0d4d4803ebbcb61000a5b6a868f5',
|
||||
album_id: 1268,
|
||||
artist_id: 5,
|
||||
title: 'Tears and rain',
|
||||
length: 244.45
|
||||
}
|
||||
],
|
||||
interactions: [
|
||||
{
|
||||
id: 1,
|
||||
song_id: '7900ab518f51775fe6cf06092c074ee5',
|
||||
liked: false,
|
||||
play_count: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
song_id: '95c0ffc33c08c8c14ea5de0a44d5df3c',
|
||||
liked: false,
|
||||
play_count: 2
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
song_id: 'c83b201502eb36f1084f207761fa195c',
|
||||
liked: false,
|
||||
play_count: 1
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
song_id: 'cb7edeac1f097143e65b1b2cde102482',
|
||||
liked: true,
|
||||
play_count: 3
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
song_id: 'ccc38cc14bb95aefdf6da4b34adcf548',
|
||||
liked: false,
|
||||
play_count: 4
|
||||
}
|
||||
],
|
||||
currentUser,
|
||||
users: [
|
||||
currentUser,
|
||||
factory<User>('user', {
|
||||
id: 2,
|
||||
name: 'John Doe',
|
||||
email: 'john@doe.tld',
|
||||
is_admin: false
|
||||
})
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`componnents/song/track-list-item renders 1`] = `
|
||||
<li title="" role="button" tabindex="0" class=""><span class="no">2</span> <span class="title">Foo and bar</span> <a href="http://koel.local/itunes/song/42?q=Foo%20and%20bar&api_token=abcdef" target="_blank" title="View on iTunes" class="view-on-itunes">
|
||||
iTunes
|
||||
</a> <span class="length">00:42</span></li>
|
||||
`;
|
50
resources/assets/js/__tests__/components/album/card.spec.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import Component from '@/components/album/card.vue'
|
||||
import Thumbnail from '@/components/ui/album-artist-thumbnail.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { playback, download } from '@/services'
|
||||
import { sharedStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount, shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/album/card', () => {
|
||||
let album: Album
|
||||
|
||||
beforeEach(() => {
|
||||
album = factory<Album>('album', {
|
||||
songs: factory<Song>('song', 10)
|
||||
})
|
||||
// @ts-ignore
|
||||
sharedStore.state = { allowDownload: true }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders properly', async () => {
|
||||
const wrapper = mount(Component, { propsData: { album } })
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has(Thumbnail)).toBe(true)
|
||||
const html = wrapper.html()
|
||||
expect(html).toMatch(album.name)
|
||||
expect(html).toMatch('10 songs')
|
||||
})
|
||||
|
||||
it('shuffles', () => {
|
||||
const wrapper = shallow(Component, { propsData: { album } })
|
||||
const m = mock(playback, 'playAllInAlbum')
|
||||
|
||||
wrapper.click('.shuffle-album')
|
||||
expect(m).toHaveBeenCalledWith(album, true)
|
||||
})
|
||||
|
||||
it('downloads', () => {
|
||||
const wrapper = shallow(Component, { propsData: { album } })
|
||||
const m = mock(download, 'fromAlbum')
|
||||
|
||||
wrapper.click('.download-album')
|
||||
expect(m).toHaveBeenCalledWith(album)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,54 @@
|
|||
import Component from '@/components/album/context-menu.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { playback, download } from '@/services'
|
||||
import { sharedStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { shallow, mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/album/context-menu', () => {
|
||||
let album: Album
|
||||
|
||||
beforeEach(() => {
|
||||
album = factory<Album>('album')
|
||||
// @ts-ignore
|
||||
sharedStore.state = { allowDownload: true }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('plays all', () => {
|
||||
const wrapper = shallow(Component, { propsData: { album } })
|
||||
const m = mock(playback, 'playAllInAlbum')
|
||||
|
||||
wrapper.click('[data-test=play]')
|
||||
expect(m).toHaveBeenCalledWith(album)
|
||||
})
|
||||
|
||||
it('shuffles', () => {
|
||||
const wrapper = shallow(Component, { propsData: { album } })
|
||||
const m = mock(playback, 'playAllInAlbum')
|
||||
|
||||
wrapper.click('[data-test=shuffle]')
|
||||
expect(m).toHaveBeenCalledWith(album, true)
|
||||
})
|
||||
|
||||
it('downloads', async () => {
|
||||
const wrapper = mount(Component, { propsData: { album } })
|
||||
await wrapper.vm.$nextTick()
|
||||
await (wrapper.vm as any).open(0, 0)
|
||||
const m = mock(download, 'fromAlbum')
|
||||
|
||||
wrapper.click('[data-test=download]')
|
||||
expect(m).toHaveBeenCalledWith(album)
|
||||
})
|
||||
|
||||
it('does not have a download item if not downloadable', () => {
|
||||
// @ts-ignore
|
||||
sharedStore.state = { allowDownload: false }
|
||||
const wrapper = shallow(Component, { propsData: { album } })
|
||||
expect(wrapper.has('[data-test=download]')).toBe(false)
|
||||
})
|
||||
})
|
45
resources/assets/js/__tests__/components/album/info.spec.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import Component from '@/components/album/info.vue'
|
||||
import AlbumThumbnail from '@/components/ui/album-artist-thumbnail.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { shallow, mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/album/info', () => {
|
||||
it('displays the info as a sidebar by default', () => {
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: {
|
||||
album: factory('album')
|
||||
}
|
||||
})
|
||||
expect(wrapper.findAll('.album-info.sidebar')).toHaveLength(1)
|
||||
expect(wrapper.findAll('.album-info.full')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('can display the info in full mode', () => {
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: {
|
||||
album: factory('album'),
|
||||
mode: 'full'
|
||||
}
|
||||
})
|
||||
expect(wrapper.findAll('.album-info.sidebar')).toHaveLength(0)
|
||||
expect(wrapper.findAll('.album-info.full')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('triggers showing full wiki', () => {
|
||||
const album = factory<Album>('album')
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: { album }
|
||||
})
|
||||
wrapper.click('.wiki button.more')
|
||||
expect(wrapper.html()).toMatch(album.info!.wiki!.full)
|
||||
})
|
||||
|
||||
it('shows the album thumbnail', async () => {
|
||||
const album = factory('album')
|
||||
const wrapper = mount(Component, {
|
||||
propsData: { album }
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has(AlbumThumbnail)).toBe(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,57 @@
|
|||
import Component from '@/components/album/track-list-item.vue'
|
||||
import { sharedStore, songStore, queueStore } from '@/stores'
|
||||
import { playback, ls } from '@/services'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('componnents/song/track-list-item', () => {
|
||||
let song: Song
|
||||
const track = {
|
||||
title: 'Foo and bar',
|
||||
fmtLength: '00:42'
|
||||
}
|
||||
const album = factory('album', { id: 42 })
|
||||
window.BASE_URL = 'http://koel.local/'
|
||||
|
||||
beforeEach(() => {
|
||||
sharedStore.state.useiTunes = true
|
||||
song = factory('song')
|
||||
mock(ls, 'get', 'abcdef')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: {
|
||||
track,
|
||||
album,
|
||||
index: 1
|
||||
}
|
||||
})
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('plays', () => {
|
||||
mock(songStore, 'guess', song)
|
||||
const containsStub = mock(queueStore, 'contains', false)
|
||||
const queueStub = mock(queueStore, 'queueAfterCurrent')
|
||||
const playStub = mock(playback, 'play')
|
||||
|
||||
shallow(Component, {
|
||||
propsData: {
|
||||
track,
|
||||
album,
|
||||
index: 1
|
||||
}
|
||||
}).click('li')
|
||||
|
||||
expect(containsStub).toHaveBeenCalledWith(song)
|
||||
expect(queueStub).toHaveBeenCalledWith(song)
|
||||
expect(playStub).toHaveBeenCalledWith(song)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
import Component from '@/components/album/track-list.vue'
|
||||
import TrackListItem from '@/components/album/track-list-item.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/album/track-list', () => {
|
||||
it('lists the correct number of tracks', async () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
album: factory('album')
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findAll(TrackListItem)).toHaveLength(2)
|
||||
})
|
||||
})
|
53
resources/assets/js/__tests__/components/artist/card.spec.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import Component from '@/components/artist/card.vue'
|
||||
import Thumbnail from '@/components/ui/album-artist-thumbnail.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { playback, download } from '@/services'
|
||||
import { sharedStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount, shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/artist/card', () => {
|
||||
let artist: Artist
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
sharedStore.state = { allowDownload: true }
|
||||
artist = factory<Artist>('artist', {
|
||||
id: 3, // make sure it's not "Various Artists"
|
||||
albums: factory<Album>('album', 4),
|
||||
songs: factory<Song>('song', 16)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders properly', async () => {
|
||||
const wrapper = mount(Component, { propsData: { artist } })
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has(Thumbnail)).toBe(true)
|
||||
const html = wrapper.html()
|
||||
expect(html).toMatch('4 albums')
|
||||
expect(html).toMatch('16 songs')
|
||||
expect(html).toMatch(artist.name)
|
||||
})
|
||||
|
||||
it('shuffles', () => {
|
||||
const wrapper = shallow(Component, { propsData: { artist } })
|
||||
const playStub = mock(playback, 'playAllByArtist')
|
||||
|
||||
wrapper.click('.shuffle-artist')
|
||||
expect(playStub).toHaveBeenCalledWith(artist, true)
|
||||
})
|
||||
|
||||
it('downloads', () => {
|
||||
const wrapper = shallow(Component, { propsData: { artist } })
|
||||
const downloadStub = mock(download, 'fromArtist')
|
||||
|
||||
wrapper.click('.download-artist')
|
||||
expect(downloadStub).toHaveBeenCalledWith(artist)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,54 @@
|
|||
import Component from '@/components/artist/context-menu.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { playback, download } from '@/services'
|
||||
import { sharedStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { shallow, mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/artist/context-menu', () => {
|
||||
let artist: Artist
|
||||
|
||||
beforeEach(() => {
|
||||
artist = factory<Artist>('artist')
|
||||
// @ts-ignore
|
||||
sharedStore.state = { allowDownload: true }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('plays all', () => {
|
||||
const wrapper = shallow(Component, { propsData: { artist } })
|
||||
const m = mock(playback, 'playAllByArtist')
|
||||
|
||||
wrapper.click('[data-test=play]')
|
||||
expect(m).toHaveBeenCalledWith(artist)
|
||||
})
|
||||
|
||||
it('shuffles', () => {
|
||||
const wrapper = shallow(Component, { propsData: { artist } })
|
||||
const m = mock(playback, 'playAllByArtist')
|
||||
|
||||
wrapper.click('[data-test=shuffle]')
|
||||
expect(m).toHaveBeenCalledWith(artist, true)
|
||||
})
|
||||
|
||||
it('downloads', async () => {
|
||||
const wrapper = mount(Component, { propsData: { artist } })
|
||||
await wrapper.vm.$nextTick()
|
||||
await (wrapper.vm as any).open(0, 0)
|
||||
const m = mock(download, 'fromArtist')
|
||||
|
||||
wrapper.click('[data-test=download]')
|
||||
expect(m).toHaveBeenCalledWith(artist)
|
||||
})
|
||||
|
||||
it('does not have a download item if not downloadable', () => {
|
||||
// @ts-ignore
|
||||
sharedStore.state = { allowDownload: false }
|
||||
const wrapper = shallow(Component, { propsData: { artist } })
|
||||
expect(wrapper.has('[data-test=download]')).toBe(false)
|
||||
})
|
||||
})
|
45
resources/assets/js/__tests__/components/artist/info.spec.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import Component from '@/components/artist/info.vue'
|
||||
import ArtistThumbnail from '@/components/ui/album-artist-thumbnail.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { shallow, mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/artist/info', () => {
|
||||
it('displays the info as a sidebar by default', () => {
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: {
|
||||
artist: factory('artist')
|
||||
}
|
||||
})
|
||||
expect(wrapper.findAll('.artist-info.sidebar')).toHaveLength(1)
|
||||
expect(wrapper.findAll('.artist-info.full')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('can display the info in full mode', () => {
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: {
|
||||
artist: factory('artist'),
|
||||
mode: 'full'
|
||||
}
|
||||
})
|
||||
expect(wrapper.findAll('.artist-info.sidebar')).toHaveLength(0)
|
||||
expect(wrapper.findAll('.artist-info.full')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('triggers showing full bio', () => {
|
||||
const artist = factory<Artist>('artist')
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: { artist }
|
||||
})
|
||||
wrapper.click('.bio button.more')
|
||||
expect(wrapper.html()).toMatch(artist.info!.bio!.full)
|
||||
})
|
||||
|
||||
it('shows the artist thumbnail', async () => {
|
||||
const artist = factory('artist')
|
||||
const wrapper = mount(Component, {
|
||||
propsData: { artist }
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has(ArtistThumbnail)).toBe(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/auth/login-form renders properly 1`] = `
|
||||
<form data-testid="login-form" class="">
|
||||
<div class="logo"><img src="@/../img/logo.svg" width="156" height="auto" alt="Koel's logo"></div>
|
||||
<!----> <input type="email" placeholder="Email Address" autofocus="autofocus" required="required"> <input type="password" placeholder="Password" required="required">
|
||||
<btn-stub type="submit">Log In</btn-stub>
|
||||
</form>
|
||||
`;
|
|
@ -0,0 +1,26 @@
|
|||
import Component from '@/components/auth/login-form.vue'
|
||||
import { userStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/auth/login-form', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders properly', () => {
|
||||
expect(shallow(Component)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('triggers login when form is submitted', () => {
|
||||
const loginStub = mock(userStore, 'login')
|
||||
shallow(Component, {
|
||||
data: () => ({
|
||||
email: 'john@doe.com',
|
||||
password: 'secret'
|
||||
})
|
||||
}).submit('form')
|
||||
expect(loginStub).toHaveBeenCalledWith('john@doe.com', 'secret')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,38 @@
|
|||
import Component from '@/components/layout/app-header.vue'
|
||||
import compareVersions from 'compare-versions'
|
||||
import { eventBus } from '@/utils'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
import { sharedStore, userStore } from '@/stores'
|
||||
import factory from '@/__tests__/factory'
|
||||
|
||||
describe('components/layout/app-header', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('toggles sidebar', () => {
|
||||
const m = mock(eventBus, 'emit')
|
||||
shallow(Component).click('.hamburger')
|
||||
expect(m).toHaveBeenCalledWith('TOGGLE_SIDEBAR')
|
||||
})
|
||||
|
||||
it('toggles search form', () => {
|
||||
const m = mock(eventBus, 'emit')
|
||||
shallow(Component).click('.magnifier')
|
||||
expect(m).toHaveBeenCalledWith('TOGGLE_SEARCH_FORM')
|
||||
})
|
||||
|
||||
it.each([[true, true, true], [false, true, false], [true, false, false], [false, false, false]])(
|
||||
'announces a new version if applicable',
|
||||
(hasNewVersion, isAdmin, shouldAnnounce) => {
|
||||
mock(compareVersions, 'compare').mockReturnValue(hasNewVersion)
|
||||
userStore.state.current = factory<User>('user', {
|
||||
is_admin: isAdmin
|
||||
})
|
||||
const wrapper = shallow(Component)
|
||||
expect(wrapper.has('[data-test=new-version-available]')).toBe(shouldAnnounce)
|
||||
}
|
||||
)
|
||||
})
|
|
@ -0,0 +1,61 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/layout/extra-panel does not have a YouTube tab if not using YouTube 1`] = `
|
||||
<section id="extra" data-testid="extra-panel" class="text-secondary showing">
|
||||
<div class="tabs">
|
||||
<div role="tablist" class="clear"><button aria-selected="true" aria-controls="extraPanelLyrics" id="extraTabLyrics" role="tab">
|
||||
Lyrics
|
||||
</button> <button aria-controls="extraPanelArtist" id="extraTabArtist" role="tab">
|
||||
Artist
|
||||
</button> <button aria-controls="extraPanelAlbum" id="extraTabAlbum" role="tab">
|
||||
Album
|
||||
</button>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="panes">
|
||||
<div aria-labelledby="extraTabLyrics" id="extraPanelLyrics" role="tabpanel" tabindex="0">
|
||||
<lyricspane-stub></lyricspane-stub>
|
||||
</div>
|
||||
<div aria-labelledby="extraTabArtist" id="extraPanelArtist" role="tabpanel" tabindex="0" style="display: none;">
|
||||
<!---->
|
||||
</div>
|
||||
<div aria-labelledby="extraTabAlbum" id="extraPanelAlbum" role="tabpanel" tabindex="0" style="display: none;">
|
||||
<!---->
|
||||
</div>
|
||||
<div aria-labelledby="extraTabYouTube" id="extraPanelYouTube" role="tabpanel" tabindex="0" style="display: none;">
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
exports[`components/layout/extra-panel renders properly 1`] = `
|
||||
<section id="extra" data-testid="extra-panel" class="text-secondary showing">
|
||||
<div class="tabs">
|
||||
<div role="tablist" class="clear"><button aria-selected="true" aria-controls="extraPanelLyrics" id="extraTabLyrics" role="tab">
|
||||
Lyrics
|
||||
</button> <button aria-controls="extraPanelArtist" id="extraTabArtist" role="tab">
|
||||
Artist
|
||||
</button> <button aria-controls="extraPanelAlbum" id="extraTabAlbum" role="tab">
|
||||
Album
|
||||
</button>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="panes">
|
||||
<div aria-labelledby="extraTabLyrics" id="extraPanelLyrics" role="tabpanel" tabindex="0">
|
||||
<lyrics-pane-stub></lyrics-pane-stub>
|
||||
</div>
|
||||
<div aria-labelledby="extraTabArtist" id="extraPanelArtist" role="tabpanel" tabindex="0" style="display: none;">
|
||||
<!---->
|
||||
</div>
|
||||
<div aria-labelledby="extraTabAlbum" id="extraPanelAlbum" role="tabpanel" tabindex="0" style="display: none;">
|
||||
<!---->
|
||||
</div>
|
||||
<div aria-labelledby="extraTabYouTube" id="extraPanelYouTube" role="tabpanel" tabindex="0" style="display: none;">
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
|
@ -0,0 +1,153 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`component/layout/main-wrapper/index renders properly 1`] = `
|
||||
<div id="mainWrapper">
|
||||
<nav id="sidebar" class="side side-nav showing">
|
||||
<section class="music">
|
||||
<h1>Your Music</h1>
|
||||
<ul class="menu">
|
||||
<li><a href="#!/home" class="home active">Home</a></li>
|
||||
<li><a href="#!/queue" class="queue">Current Queue</a></li>
|
||||
<li><a href="#!/songs" class="songs">All Songs</a></li>
|
||||
<li><a href="#!/albums" class="albums">Albums</a></li>
|
||||
<li><a href="#!/artists" class="artists">
|
||||
Artists
|
||||
</a></li>
|
||||
<!---->
|
||||
</ul>
|
||||
</section>
|
||||
<!---->
|
||||
<!---->
|
||||
</nav>
|
||||
<section id="mainContent">
|
||||
<!---->
|
||||
<!---->
|
||||
<section id="homeWrapper">
|
||||
<!---->
|
||||
<div class="main-scroll-wrap">
|
||||
<div class="two-cols">
|
||||
<!---->
|
||||
<section class="recent">
|
||||
<h1>
|
||||
Recently Played
|
||||
<!---->
|
||||
</h1>
|
||||
<!---->
|
||||
<p class="text-secondary">
|
||||
Your recently played songs will be displayed here.<br>
|
||||
Start listening!
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
</section>
|
||||
<section id="queueWrapper" style="display: none;">
|
||||
<!---->
|
||||
<!---->
|
||||
</section>
|
||||
<section id="songsWrapper" style="display: none;">
|
||||
<!---->
|
||||
<div tabindex="0" class="song-list-wrap main-scroll-wrap all-songs">
|
||||
<table class="song-list-header sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="track-number">
|
||||
#
|
||||
<i class="fa fa-angle-down" style="display: none;"></i> <i class="fa fa-angle-up" style="display: none;"></i></th>
|
||||
<th class="title">
|
||||
Title
|
||||
<i class="fa fa-angle-down" style="display: none;"></i> <i class="fa fa-angle-up" style="display: none;"></i></th>
|
||||
<th class="artist">
|
||||
Artist
|
||||
<i class="fa fa-angle-down" style="display: none;"></i> <i class="fa fa-angle-up" style="display: none;"></i></th>
|
||||
<th class="album">
|
||||
Album
|
||||
<i class="fa fa-angle-down" style="display: none;"></i> <i class="fa fa-angle-up" style="display: none;"></i></th>
|
||||
<th class="time">
|
||||
Time
|
||||
<i class="fa fa-angle-down" style="display: none;"></i> <i class="fa fa-angle-up" style="display: none;"></i></th>
|
||||
<th class="favorite"></th>
|
||||
<th class="play"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div data-v-3eef485a="" class="virtual-scroller scroller">
|
||||
<div data-v-3eef485a="" class="item-container">
|
||||
<table data-v-3eef485a="" class="items"></table>
|
||||
</div>
|
||||
<div data-v-b329ee4c="" data-v-3eef485a="" tabindex="-1" class="resize-observer"><object style="display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;" aria-hidden="true" tabindex="-1" type="text/html" data="about:blank"></object></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="albumsWrapper" style="display: none;">
|
||||
<!---->
|
||||
<div class="albums main-scroll-wrap as-null">
|
||||
<!---->
|
||||
</div>
|
||||
</section>
|
||||
<section id="artistsWrapper" style="display: none;">
|
||||
<!---->
|
||||
<div class="artists main-scroll-wrap as-null">
|
||||
<!---->
|
||||
</div>
|
||||
</section>
|
||||
<section id="playlistWrapper" style="display: none;">
|
||||
<!---->
|
||||
<!---->
|
||||
</section>
|
||||
<section id="favoritesWrapper" style="display: none;">
|
||||
<!---->
|
||||
<!---->
|
||||
</section>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
</section>
|
||||
<section id="extra" data-testid="extra-panel" class="text-secondary showing">
|
||||
<div class="tabs">
|
||||
<div role="tablist" class="clear"><button aria-selected="true" aria-controls="extraPanelLyrics" id="extraTabLyrics" role="tab">
|
||||
Lyrics
|
||||
</button> <button aria-controls="extraPanelArtist" id="extraTabArtist" role="tab">
|
||||
Artist
|
||||
</button> <button aria-controls="extraPanelAlbum" id="extraTabAlbum" role="tab">
|
||||
Album
|
||||
</button>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="panes">
|
||||
<div aria-labelledby="extraTabLyrics" id="extraPanelLyrics" role="tabpanel" tabindex="0">
|
||||
<!---->
|
||||
</div>
|
||||
<div aria-labelledby="extraTabArtist" id="extraPanelArtist" role="tabpanel" tabindex="0" style="display: none;">
|
||||
<!---->
|
||||
</div>
|
||||
<div aria-labelledby="extraTabAlbum" id="extraPanelAlbum" role="tabpanel" tabindex="0" style="display: none;">
|
||||
<!---->
|
||||
</div>
|
||||
<div aria-labelledby="extraTabYouTube" id="extraPanelYouTube" role="tabpanel" tabindex="0" style="display: none;">
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="modal-wrapper">
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,25 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/layout/main-wrapper/main-content has a translucent image per song/album 1`] = `
|
||||
<section id="mainContent">
|
||||
<!---->
|
||||
<!---->
|
||||
<homescreen-stub></homescreen-stub>
|
||||
<queuescreen-stub style="display: none;"></queuescreen-stub>
|
||||
<allsongsscreen-stub style="display: none;"></allsongsscreen-stub>
|
||||
<albumlistscreen-stub style="display: none;"></albumlistscreen-stub>
|
||||
<artistlistscreen-stub style="display: none;"></artistlistscreen-stub>
|
||||
<playlistscreen-stub style="display: none;"></playlistscreen-stub>
|
||||
<favoritesscreen-stub style="display: none;"></favoritesscreen-stub>
|
||||
<recentlyplayedscreen-stub style="display: none;"></recentlyplayedscreen-stub>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
</section>
|
||||
`;
|
|
@ -0,0 +1,81 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/layout/main-wrapper/sidebar displays YouTube menu item if using YouTube 1`] = `
|
||||
<nav id="sidebar" class="side side-nav showing">
|
||||
<section class="music">
|
||||
<h1>Your Music</h1>
|
||||
<ul class="menu">
|
||||
<li><a href="#!/home" class="home active">Home</a></li>
|
||||
<li><a href="#!/queue" class="queue">Current Queue</a></li>
|
||||
<li><a href="#!/songs" class="songs">All Songs</a></li>
|
||||
<li><a href="#!/albums" class="albums">Albums</a></li>
|
||||
<li><a href="#!/artists" class="artists">
|
||||
Artists
|
||||
</a></li>
|
||||
<li><a href="#!/youtube" class="youtube">
|
||||
YouTube Video
|
||||
</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<playlistlist-stub current-view="Home"></playlistlist-stub>
|
||||
<!---->
|
||||
</nav>
|
||||
`;
|
||||
|
||||
exports[`components/layout/main-wrapper/sidebar displays management menu items for admin 1`] = `
|
||||
<nav id="sidebar" class="side side-nav showing">
|
||||
<section class="music">
|
||||
<h1>Your Music</h1>
|
||||
<ul class="menu">
|
||||
<li><a href="#!/home" class="home active">Home</a></li>
|
||||
<li><a href="#!/queue" class="queue">Current Queue</a></li>
|
||||
<li><a href="#!/songs" class="songs">All Songs</a></li>
|
||||
<li><a href="#!/albums" class="albums">Albums</a></li>
|
||||
<li><a href="#!/artists" class="artists">
|
||||
Artists
|
||||
</a></li>
|
||||
<li><a href="#!/youtube" class="youtube">
|
||||
YouTube Video
|
||||
</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<playlistlist-stub current-view="Home"></playlistlist-stub>
|
||||
<section class="manage">
|
||||
<h1>Manage</h1>
|
||||
<ul class="menu">
|
||||
<li><a href="#!/settings" class="settings">Settings</a></li>
|
||||
<li><a href="#!/upload" class="upload">Upload</a></li>
|
||||
<li><a href="#!/users" class="users">Users</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</nav>
|
||||
`;
|
||||
|
||||
exports[`components/layout/main-wrapper/sidebar displays new version info 1`] = `
|
||||
<nav id="sidebar" class="side side-nav showing">
|
||||
<section class="music">
|
||||
<h1>Your Music</h1>
|
||||
<ul class="menu">
|
||||
<li><a href="#!/home" class="home active">Home</a></li>
|
||||
<li><a href="#!/queue" class="queue">Current Queue</a></li>
|
||||
<li><a href="#!/songs" class="songs">All Songs</a></li>
|
||||
<li><a href="#!/albums" class="albums">Albums</a></li>
|
||||
<li><a href="#!/artists" class="artists">
|
||||
Artists
|
||||
</a></li>
|
||||
<li><a href="#!/youtube" class="youtube">
|
||||
YouTube Video
|
||||
</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<playlistlist-stub current-view="Home"></playlistlist-stub>
|
||||
<section class="manage">
|
||||
<h1>Manage</h1>
|
||||
<ul class="menu">
|
||||
<li><a href="#!/settings" class="settings">Settings</a></li>
|
||||
<li><a href="#!/upload" class="upload">Upload</a></li>
|
||||
<li><a href="#!/users" class="users">Users</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</nav>
|
||||
`;
|
|
@ -0,0 +1,55 @@
|
|||
import Component from '@/components/layout/main-wrapper/extra-panel.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { eventBus } from '@/utils'
|
||||
import { songInfo } from '@/services'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { shallow, Wrapper } from '@/__tests__/adapter'
|
||||
|
||||
const shallowComponent = (data: object = {}): Wrapper => shallow(Component, {
|
||||
stubs: ['lyrics-pane', 'artist-info', 'album-info', 'you-tube-video-list'],
|
||||
data: () => data
|
||||
})
|
||||
|
||||
describe('components/layout/extra-panel', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders properly', () => {
|
||||
expect(shallowComponent()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('does not have a YouTube tab if not using YouTube', async () => {
|
||||
const wrapper = shallowComponent({
|
||||
sharedState: {
|
||||
useYouTube: false
|
||||
}
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(shallow(Component)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('has a YouTube tab if using YouTube', async () => {
|
||||
const wrapper = shallowComponent({
|
||||
sharedState: {
|
||||
useYouTube: true
|
||||
}
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has('#extraTabYouTube')).toBe(true)
|
||||
})
|
||||
|
||||
it.each<[string]>([['#extraTabLyrics'], ['#extraTabAlbum'], ['#extraTabArtist']])
|
||||
('switches to "%s" tab', selector => {
|
||||
expect(shallowComponent().click(selector).find('[aria-selected=true]').is(selector)).toBe(true)
|
||||
})
|
||||
|
||||
it('fetch song info when a new song is played', () => {
|
||||
shallowComponent()
|
||||
const song = factory('song')
|
||||
const m = mock(songInfo, 'fetch', song)
|
||||
eventBus.emit('SONG_STARTED', song)
|
||||
expect(m).toHaveBeenCalledWith(song)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,10 @@
|
|||
import Component from '@/components/layout/main-wrapper/index.vue'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('component/layout/main-wrapper/index', () => {
|
||||
it('renders properly', async () => {
|
||||
const wrapper = mount(Component)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
import Component from '@/components/layout/main-wrapper/main-content.vue'
|
||||
import { eventBus } from '@/utils'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/layout/main-wrapper/main-content', () => {
|
||||
it('has a translucent image per song/album', () => {
|
||||
const wrapper = shallow(Component)
|
||||
const song = factory('song', {
|
||||
album: factory('album', {
|
||||
cover: 'http://foo/bar.jpg'
|
||||
})
|
||||
})
|
||||
eventBus.emit('SONG_STARTED', song)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,39 @@
|
|||
import Component from '@/components/layout/main-wrapper/sidebar.vue'
|
||||
import { sharedStore } from '@/stores'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/layout/main-wrapper/sidebar', () => {
|
||||
it('displays YouTube menu item if using YouTube', () => {
|
||||
sharedStore.state.useYouTube = true
|
||||
expect(shallow(Component)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('displays management menu items for admin', async () => {
|
||||
const wrapper = shallow(Component, {
|
||||
data: () => ({
|
||||
userState: {
|
||||
current: factory('user', { is_admin: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('displays new version info', async () => {
|
||||
sharedStore.state.currentVersion = 'v0.0.0'
|
||||
sharedStore.state.latestVersion = 'v0.0.1'
|
||||
const wrapper = shallow(Component, {
|
||||
data: () => ({
|
||||
userState: {
|
||||
current: factory('user', { is_admin: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,45 @@
|
|||
import Component from '@/components/layout/modal-wrapper.vue'
|
||||
import { eventBus } from '@/utils'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { http } from '@/services'
|
||||
|
||||
describe('components/layout/modal-wrapper', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each<[string, string, User | Song | undefined]>([
|
||||
['add-user-form', 'MODAL_SHOW_ADD_USER_FORM', undefined],
|
||||
['edit-user-form', 'MODAL_SHOW_EDIT_USER_FORM', factory('user')],
|
||||
['edit-song-form', 'MODAL_SHOW_EDIT_SONG_FORM', factory('song')],
|
||||
['create-smart-playlist-form', 'MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM', undefined]
|
||||
])('shows %s modal', async (modalName, eventName, eventParams?) => {
|
||||
if (modalName === 'edit-song-form') {
|
||||
// mocking the songInfo.fetch() request made during edit-form modal opening
|
||||
mock(http, 'request').mockReturnValue(Promise.resolve({ data: {}}))
|
||||
}
|
||||
|
||||
const wrapper = shallow(Component, {
|
||||
stubs: [modalName]
|
||||
})
|
||||
|
||||
eventBus.emit(eventName, eventParams)
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has(`${modalName}-stub`)).toBe(true)
|
||||
})
|
||||
|
||||
it('closes', async () => {
|
||||
const wrapper = shallow(Component, {
|
||||
stubs: ['add-user-form']
|
||||
})
|
||||
eventBus.emit('MODAL_SHOW_ADD_USER_FORM')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has('add-user-form-stub')).toBe(true)
|
||||
;(wrapper.vm as any).close()
|
||||
expect(wrapper.has('add-user-form-stub')).toBe(false)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,51 @@
|
|||
import Component from '@/components/meta/about-dialog.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/meta/about-dialog', () => {
|
||||
const versionPermutations = [
|
||||
['v4.0.0'/* latest ver */, 'v4.0.0-beta'/* this ver */, true/* admin */, true/* show new ver notification */],
|
||||
['v4.0.0', 'v4.0.0', true, false],
|
||||
['v4.0.0', 'v3.9.0', false, false]
|
||||
]
|
||||
|
||||
it.each(versionPermutations)('new version notification', (latestVersion, currentVersion, isAdmin, visible) => {
|
||||
const wrapper = shallow(Component, {
|
||||
data: () => ({
|
||||
userState: {
|
||||
current: factory('user', {
|
||||
is_admin: isAdmin
|
||||
})
|
||||
},
|
||||
sharedState: {
|
||||
latestVersion,
|
||||
currentVersion
|
||||
}
|
||||
})
|
||||
})
|
||||
expect(wrapper.has('.new-version')).toBe(visible)
|
||||
})
|
||||
|
||||
const demoPermutations = [
|
||||
[true, true],
|
||||
[false, false]
|
||||
]
|
||||
|
||||
it.each(demoPermutations)('builds demo version with(out) credits', (inDemoEnv, creditVisible) => {
|
||||
const wrapper = shallow(Component, {
|
||||
data: () => ({
|
||||
userState: {
|
||||
current: factory('user', {
|
||||
is_admin: true
|
||||
})
|
||||
},
|
||||
sharedState: {
|
||||
latestVersion: 'v1.0.0',
|
||||
currentVersion: 'v1.0.0'
|
||||
},
|
||||
demo: inDemoEnv
|
||||
})
|
||||
})
|
||||
expect(wrapper.has('.demo-credits')).toBe(creditVisible)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,29 @@
|
|||
import Component from '@/components/playlist/name-editor.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { playlistStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/playlist/name-editor', () => {
|
||||
let playlist: Playlist
|
||||
beforeEach(() => {
|
||||
playlist = factory<Playlist>('playlist', {
|
||||
id: 99,
|
||||
name: 'Foo'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('updates a playlist', () => {
|
||||
const updateStub = mock(playlistStore, 'update')
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: { playlist }
|
||||
})
|
||||
wrapper.find('[type=text]').setValue('Bar').input().blur()
|
||||
expect(updateStub).toHaveBeenCalledWith(expect.objectContaining({ id: 99, name: 'Bar' }))
|
||||
})
|
||||
})
|
|
@ -0,0 +1,41 @@
|
|||
import Component from '@/components/playlist/sidebar-item.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { shallow, mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/playlist/sidebar-item', () => {
|
||||
let playlist: Playlist
|
||||
beforeEach(() => {
|
||||
playlist = factory<Playlist>('playlist', {
|
||||
id: 99,
|
||||
name: 'Foo'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('edits a playlist', async () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: { playlist }
|
||||
})
|
||||
|
||||
wrapper.dblclick('li.playlist')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has('[name=name]')).toBe(true)
|
||||
})
|
||||
|
||||
it("doesn't allow editing Favorites item", async () => {
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: {
|
||||
playlist: { name: 'Favorites' },
|
||||
type: 'favorites'
|
||||
}
|
||||
})
|
||||
|
||||
wrapper.dblclick('li.favorites')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has('[name=name]')).toBe(false)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`profile-preferences/profile-form renders 1`] = `
|
||||
<div data-testid="theme-card-sample" class="theme" style="background-color: rgb(255, 0, 0);">
|
||||
<div class="name">Sample</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,20 @@
|
|||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { preferenceStore as preferences } from '@/stores'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
import Component from '@/components/profile-preferences/preferences.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
|
||||
describe('profile-preferences/preferences', () => {
|
||||
beforeEach(() => preferences.init(factory('user')))
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each([['notify'], ['confirm_closing'], ['show_album_art_overlay']])('updates preference "%s"', key => {
|
||||
const m = mock(preferences, 'save')
|
||||
shallow(Component).change(`input[name=${key}]`)
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,33 @@
|
|||
import { shallow } from '@/__tests__/adapter'
|
||||
import Component from '@/components/profile-preferences/theme-card.vue'
|
||||
|
||||
describe('profile-preferences/profile-form', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const theme: Theme = {
|
||||
id: 'sample',
|
||||
thumbnailColor: '#f00'
|
||||
}
|
||||
|
||||
it('renders', () => {
|
||||
expect(shallow(Component, {
|
||||
propsData: {
|
||||
theme
|
||||
}
|
||||
})).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('emits an event when theme is selected', () => {
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: {
|
||||
theme
|
||||
}
|
||||
})
|
||||
|
||||
wrapper.click('[data-testid=theme-card-sample]')
|
||||
expect(wrapper.hasEmitted('selected', theme)).toBe(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,18 @@
|
|||
import List from '@/components/screens/album-list.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/album-list', () => {
|
||||
it('displays a list of albums', async () => {
|
||||
const wrapper = mount(List, {
|
||||
sync: false, // https://github.com/vuejs/vue-test-utils/issues/673,
|
||||
stubs: ['album-card'],
|
||||
data: () => ({
|
||||
albums: factory('album', 5)
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findAll('album-card-stub')).toHaveLength(5)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,54 @@
|
|||
import Component from '@/components/screens/album.vue'
|
||||
import SongList from '@/components/song/list.vue'
|
||||
import { download, albumInfo as albumInfoService, playback } from '@/services'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount, shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/album', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders properly', async () => {
|
||||
const album = factory<Album>('album')
|
||||
const wrapper = mount(Component, {
|
||||
propsData: { album }
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.hasAll(SongList)).toBe(true)
|
||||
})
|
||||
|
||||
it('loads info from Last.fm', async () => {
|
||||
const album = factory<Album>('album', {
|
||||
info: null,
|
||||
songs: factory<Song>('song', 2)
|
||||
})
|
||||
const wrapper = await shallow(Component, {
|
||||
propsData: { album },
|
||||
data: () => ({
|
||||
sharedState: { useLastfm: true }
|
||||
})
|
||||
})
|
||||
const m = mock(albumInfoService, 'fetch')
|
||||
wrapper.click('a.info')
|
||||
expect(m).toHaveBeenCalledWith(album)
|
||||
})
|
||||
|
||||
it('allows downloading', () => {
|
||||
const album = factory<Album>('album', {
|
||||
songs: factory<Song>('song', 2)
|
||||
})
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: { album },
|
||||
data: () => ({
|
||||
sharedState: { allowDownload: true }
|
||||
})
|
||||
})
|
||||
const m = mock(download, 'fromAlbum')
|
||||
wrapper.click('a.download')
|
||||
expect(m).toHaveBeenCalledWith(album)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,15 @@
|
|||
import Component from '@/components/screens/all-songs.vue'
|
||||
import SongList from '@/components/song/list.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { songStore } from '@/stores'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/all-songs', () => {
|
||||
it('renders properly', async () => {
|
||||
songStore.all = factory<Song>('song', 10)
|
||||
const wrapper = mount(Component)
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has(SongList)).toBe(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,18 @@
|
|||
import List from '@/components/screens/artist-list.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/artist-list', () => {
|
||||
it('displays a list of artists', async () => {
|
||||
const wrapper = mount(List, {
|
||||
sync: false, // https://github.com/vuejs/vue-test-utils/issues/673
|
||||
stubs: ['artist-card'],
|
||||
data: () => ({
|
||||
artists: factory<Artist>('artist', 5)
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findAll('artist-card-stub')).toHaveLength(5)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,63 @@
|
|||
import Component from '@/components/screens/artist.vue'
|
||||
import SongList from '@/components/song/list.vue'
|
||||
import { download, artistInfo as artistInfoService, playback } from '@/services'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount, shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/artist', () => {
|
||||
let artist: Artist
|
||||
beforeEach(() => {
|
||||
artist = factory('artist')
|
||||
const album = factory<Album>('album', {
|
||||
artist,
|
||||
artist_id: artist.id
|
||||
})
|
||||
artist.albums = [album]
|
||||
artist.songs = factory<Song>('song', 5, {
|
||||
artist,
|
||||
album,
|
||||
artist_id: artist.id,
|
||||
album_id: album.id
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders upon receiving event', async () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: { artist }
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has(SongList)).toBe(true)
|
||||
})
|
||||
|
||||
it('loads info from Last.fm', () => {
|
||||
artist.info = null
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: { artist },
|
||||
data: () => ({
|
||||
sharedState: { useLastfm: true }
|
||||
})
|
||||
})
|
||||
const m = mock(artistInfoService, 'fetch')
|
||||
wrapper.click('a.info')
|
||||
expect(m).toHaveBeenCalledWith(artist)
|
||||
})
|
||||
|
||||
it('allows downloading', () => {
|
||||
const wrapper = shallow(Component, {
|
||||
propsData: { artist },
|
||||
data: () => ({
|
||||
sharedState: { allowDownload: true }
|
||||
})
|
||||
})
|
||||
const m = mock(download, 'fromArtist')
|
||||
wrapper.click('a.download')
|
||||
expect(m).toHaveBeenCalledWith(artist)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,60 @@
|
|||
import Component from '@/components/screens/favorites.vue'
|
||||
import SongList from '@/components/song/list.vue'
|
||||
import SongListControls from '@/components/song/list-controls.vue'
|
||||
import { download } from '@/services'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount, shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/favorites', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('displays the song list if there are favorites', async () => {
|
||||
const wrapper = mount(Component, {
|
||||
data: () => ({
|
||||
state: {
|
||||
songs: factory('song', 5)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.hasAll(SongList, SongListControls)).toBe(true)
|
||||
expect(wrapper.findAll('div.none')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('displays a fallback message if there are no favorites', async () => {
|
||||
const wrapper = mount(Component, {
|
||||
data: () => ({
|
||||
state: {
|
||||
songs: []
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has('[data-test=screen-placeholder]')).toBe(true)
|
||||
})
|
||||
|
||||
it('allows downloading', () => {
|
||||
const m = mock(download, 'fromFavorites')
|
||||
|
||||
shallow(Component, {
|
||||
data: () => ({
|
||||
state: {
|
||||
songs: factory('song', 5),
|
||||
},
|
||||
sharedState: { allowDownload: true },
|
||||
meta: {
|
||||
songCount: 5,
|
||||
totalLength: '12:34'
|
||||
}
|
||||
})
|
||||
}).click('a.download')
|
||||
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,22 @@
|
|||
import Home from '@/components/screens/home.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { eventBus } from '@/utils'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/home', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('refreshes when a new song is played', async () => {
|
||||
const wrapper = mount(Home)
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
// @ts-ignore
|
||||
const m = mock(wrapper.vm, 'refreshDashboard')
|
||||
eventBus.emit('SONG_STARTED', factory('song'))
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,52 @@
|
|||
import Component from '@/components/screens/playlist.vue'
|
||||
import SongList from '@/components/song/list.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { eventBus } from '@/utils'
|
||||
import { playlistStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount, shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/playlist', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders properly', async () => {
|
||||
const playlist = factory<Playlist>('playlist', { populated: true })
|
||||
const wrapper = mount(Component, { data: () => ({ playlist }) })
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has(SongList)).toBe(true)
|
||||
})
|
||||
|
||||
it('fetch and populate playlist content on demand', () => {
|
||||
const playlist = factory('playlist', { songs: [] })
|
||||
shallow(Component)
|
||||
|
||||
const m = mock(playlistStore, 'fetchSongs')
|
||||
eventBus.emit('LOAD_MAIN_CONTENT', 'Playlist', playlist)
|
||||
expect(m).toHaveBeenCalledWith(playlist)
|
||||
})
|
||||
|
||||
it('displays a fallback message if the playlist is empty', async () => {
|
||||
const wrapper = mount(Component, {
|
||||
data: () => ({
|
||||
playlist: factory('playlist', {
|
||||
populated: true,
|
||||
songs: []
|
||||
})
|
||||
})
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has('[data-test=screen-placeholder]')).toBe(true)
|
||||
})
|
||||
|
||||
it('emits an event to delete the playlist', () => {
|
||||
const playlist = factory('playlist', { populated: true })
|
||||
const wrapper = shallow(Component, { data: () => ({ playlist }) })
|
||||
const emitMock = mock(eventBus, 'emit')
|
||||
wrapper.click('.btn-delete-playlist')
|
||||
expect(emitMock).toHaveBeenCalledWith('PLAYLIST_DELETE', playlist)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,90 @@
|
|||
import Component from '@/components/screens/queue.vue'
|
||||
import SongList from '@/components/song/list.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { queueStore, songStore } from '@/stores'
|
||||
import { playback } from '@/services'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount, shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/queue', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders properly', async () => {
|
||||
const wrapper = mount(Component, {
|
||||
data: () => ({
|
||||
state: {
|
||||
songs: factory<Song>('song', 10)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has(SongList)).toBe(true)
|
||||
})
|
||||
|
||||
it('prompts to shuffle all songs if there are songs and current queue is empty', async () => {
|
||||
songStore.state.songs = factory<Song>('song', 10)
|
||||
const wrapper = mount(Component, {
|
||||
data: () => ({
|
||||
state: { songs: [] }
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('a.start').text()).toMatch('shuffling all songs')
|
||||
})
|
||||
|
||||
it('doesn\'t prompt to shuffle all songs if there is no song', async () => {
|
||||
songStore.state.songs = []
|
||||
const wrapper = mount(Component, {
|
||||
data: () => ({
|
||||
state: { songs: [] }
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has('a.start')).toBe(false)
|
||||
})
|
||||
|
||||
it('shuffles all songs in the queue if any', () => {
|
||||
const m = mock(playback, 'queueAndPlay')
|
||||
const songs = factory<Song>('song', 10)
|
||||
const wrapper = mount(Component, {
|
||||
data: () => ({
|
||||
state: { songs }
|
||||
})
|
||||
})
|
||||
|
||||
wrapper.click('button.btn-shuffle-all')
|
||||
expect(m).toHaveBeenCalledWith(songs, true)
|
||||
})
|
||||
|
||||
it('shuffles all available songs if there are no songs queued', () => {
|
||||
const songs = factory<Song>('song', 10)
|
||||
songStore.state.songs = songs
|
||||
const m = mock(playback, 'queueAndPlay')
|
||||
const c = shallow(Component, {
|
||||
data: () => ({
|
||||
state: {
|
||||
songs: []
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
c.click('a.start')
|
||||
expect(m).toHaveBeenCalledWith(songs, true)
|
||||
})
|
||||
|
||||
it('clears the queue', () => {
|
||||
const m = mock(queueStore, 'clear')
|
||||
mount(Component, {
|
||||
data: () => ({
|
||||
state: { songs: factory('song', 10) }
|
||||
})
|
||||
}).click('button.btn-clear-queue')
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,34 @@
|
|||
import Component from '@/components/screens/recently-played.vue'
|
||||
import SongList from '@/components/song/list.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { recentlyPlayedStore } from '@/stores'
|
||||
import { eventBus } from '@/utils'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount, shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/recently-played', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders properly', async () => {
|
||||
const wrapper = mount(Component, {
|
||||
data: () => ({
|
||||
state: {
|
||||
songs: factory('song', 5)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.has(SongList)).toBe(true)
|
||||
})
|
||||
|
||||
it('fetch and populate content on demand', () => {
|
||||
shallow(Component)
|
||||
const m = mock(recentlyPlayedStore, 'fetchAll')
|
||||
eventBus.emit('LOAD_MAIN_CONTENT', 'RecentlyPlayed')
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,36 @@
|
|||
import Component from '@/components/screens/settings.vue'
|
||||
import { sharedStore, settingStore } from '@/stores'
|
||||
import { alerts } from '@/utils'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/settings', () => {
|
||||
beforeEach(() => {
|
||||
settingStore.state = {
|
||||
settings: {
|
||||
media_path: '/foo/'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('warns if changing a non-empty media path', () => {
|
||||
sharedStore.state.originalMediaPath = '/bar'
|
||||
const m = mock(alerts, 'confirm')
|
||||
shallow(Component).submit('form')
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("doesn't warn if changing an empty media path", () => {
|
||||
sharedStore.state.originalMediaPath = ''
|
||||
const confirmMock = mock(alerts, 'confirm')
|
||||
const updateMock = mock(settingStore, 'update')
|
||||
shallow(Component).submit('form')
|
||||
expect(confirmMock).not.toHaveBeenCalled()
|
||||
expect(updateMock).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,39 @@
|
|||
import Component from '@/components/screens/user-list.vue'
|
||||
import UserCard from '@/components/user/card.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { userStore } from '@/stores'
|
||||
import { eventBus } from '@/utils'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/screens/user-list', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('displays the users', async () => {
|
||||
userStore.all = factory<User>('user', 10)
|
||||
const wrapper = mount(Component)
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findAll(UserCard)).toHaveLength(10)
|
||||
})
|
||||
|
||||
it('adds new user', async () => {
|
||||
const emitMock = mock(eventBus, 'emit')
|
||||
const wrapper = mount(Component)
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.click('.btn-add')
|
||||
expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_ADD_USER_FORM')
|
||||
})
|
||||
|
||||
it('edits a user', () => {
|
||||
userStore.all = factory<User>('user', 10)
|
||||
const emitMock = mock(eventBus, 'emit')
|
||||
mount(Component).click('.btn-edit')
|
||||
|
||||
expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_EDIT_USER_FORM', userStore.all[0])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/song/list-controls renders properly 1`] = `
|
||||
<div data-test="song-list-controls" class="song-list-controls">
|
||||
<!---->
|
||||
<addtomenu-stub config="[object Object]" songs=""></addtomenu-stub>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,114 @@
|
|||
import _ from 'lodash'
|
||||
import Component from '@/components/song/add-to-menu.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { playlistStore, queueStore, favoriteStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
import FunctionPropertyNames = jest.FunctionPropertyNames
|
||||
|
||||
describe('components/song/add-to-menu', () => {
|
||||
const config = {
|
||||
queue: true,
|
||||
favorites: true,
|
||||
playlists: true,
|
||||
newPlaylist: true
|
||||
}
|
||||
|
||||
let songs: Song[]
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const initComponent = (customConfig = {}, func = shallow) => {
|
||||
songs = factory<Song>('song', 5)
|
||||
return func(Component, {
|
||||
propsData: {
|
||||
songs,
|
||||
config: _.assign(_.clone(config), customConfig),
|
||||
showing: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('renders', () => {
|
||||
playlistStore.all = factory<Playlist>('playlist', 10)
|
||||
const wrapper = initComponent()
|
||||
expect(wrapper.html()).toMatch('Add 5 songs to')
|
||||
expect(wrapper.hasAll(
|
||||
'li.after-current',
|
||||
'li.bottom-queue',
|
||||
'li.top-queue',
|
||||
'li.favorites',
|
||||
'form.form-new-playlist'
|
||||
)).toBe(true)
|
||||
expect(wrapper.findAll('li.playlist')).toHaveLength(10)
|
||||
})
|
||||
|
||||
it('supports different configurations', () => {
|
||||
// add to queue
|
||||
let wrapper = initComponent({ queue: false })
|
||||
expect(wrapper.hasNone('li.after-current', 'li.bottom-queue', 'li.top-queue')).toBe(true)
|
||||
|
||||
// add to favorites
|
||||
wrapper = initComponent({ favorites: false })
|
||||
expect(wrapper.has('li.favorites')).toBe(false)
|
||||
|
||||
// add to playlists
|
||||
wrapper = initComponent({ playlists: false })
|
||||
expect(wrapper.has('li.playlist')).toBe(false)
|
||||
|
||||
// add to a new playlist
|
||||
wrapper = initComponent({ newPlaylist: false })
|
||||
expect(wrapper.has('form.form-new-playlist')).toBe(false)
|
||||
})
|
||||
|
||||
it.each<[string, string, FunctionPropertyNames<typeof queueStore>]>([
|
||||
['after current', '.after-current', 'queueAfterCurrent'],
|
||||
['to bottom', '.bottom-queue', 'queue'],
|
||||
['to top', '.top-queue', 'queueToTop']
|
||||
])('queues songs %s when "%s" is clicked', (to, selector, queueFunc) => {
|
||||
const wrapper = initComponent()
|
||||
const queueMock = mock(queueStore, queueFunc)
|
||||
// @ts-ignore
|
||||
const closeMock = mock(wrapper.vm, 'close')
|
||||
wrapper.click(`li${selector}`)
|
||||
expect(queueMock).toHaveBeenCalledWith(songs)
|
||||
expect(closeMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('add songs to favorite', () => {
|
||||
const wrapper = initComponent()
|
||||
const likeStub = mock(favoriteStore, 'like')
|
||||
// @ts-ignore
|
||||
const closeStub = mock(wrapper.vm, 'close')
|
||||
wrapper.click('li.favorites')
|
||||
expect(likeStub).toHaveBeenCalledWith(songs)
|
||||
expect(closeStub).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('add songs to existing playlist', () => {
|
||||
const playlists = factory<Playlist>('playlist', 3)
|
||||
playlistStore.all = playlists
|
||||
const wrapper = initComponent()
|
||||
const addSongsStub = mock(playlistStore, 'addSongs')
|
||||
// @ts-ignore
|
||||
const closeStub = mock(wrapper.vm, 'close')
|
||||
wrapper.findAll('li.playlist').at(1).click()
|
||||
expect(addSongsStub).toHaveBeenCalledWith(playlists[1], songs)
|
||||
expect(closeStub).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('creates new playlist from songs', async () => {
|
||||
const storeStub = mock(playlistStore, 'store', new Promise(resolve => resolve(factory('playlist'))))
|
||||
const wrapper = initComponent()
|
||||
// @ts-ignore
|
||||
const closeStub = mock(wrapper.vm, 'close')
|
||||
wrapper.setData({ newPlaylistName: 'Foo' })
|
||||
wrapper.submit('form.form-new-playlist')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(storeStub).toHaveBeenCalledWith('Foo', songs)
|
||||
expect(closeStub).toHaveBeenCalled()
|
||||
})
|
||||
})
|
60
resources/assets/js/__tests__/components/song/card.spec.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import Component from '@/components/song/card.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { queueStore } from '@/stores'
|
||||
import { playback } from '@/services'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { Wrapper, shallow } from '@/__tests__/adapter'
|
||||
import FunctionPropertyNames = jest.FunctionPropertyNames
|
||||
|
||||
describe('components/song/card', () => {
|
||||
let propsData, song: Song, wrapper: Wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
song = factory<Song>('song', {
|
||||
artist: factory<Artist>('artist', {
|
||||
id: 42,
|
||||
name: 'Foo Fighter'
|
||||
}),
|
||||
playCount: 10,
|
||||
playbackState: 'Stopped',
|
||||
title: 'Foo bar'
|
||||
})
|
||||
|
||||
propsData = {
|
||||
song,
|
||||
topPlayCount: 42
|
||||
}
|
||||
|
||||
wrapper = shallow(Component, { propsData })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each([[true, false], [false, true]])('queuing and playing behavior', (shouldQueue, queued) => {
|
||||
const containsStub = mock(queueStore, 'contains', queued)
|
||||
const queueStub = mock(queueStore, 'queueAfterCurrent')
|
||||
const playStub = mock(playback, 'play')
|
||||
wrapper.dblclick('[data-test=song-card]')
|
||||
expect(containsStub).toHaveBeenCalledWith(song)
|
||||
if (queued) {
|
||||
expect(queueStub).not.toHaveBeenCalled()
|
||||
} else {
|
||||
expect(queueStub).toHaveBeenCalledWith(song)
|
||||
}
|
||||
expect(playStub).toHaveBeenCalledWith(song)
|
||||
})
|
||||
|
||||
it.each<[PlaybackState, FunctionPropertyNames<typeof playback>]>([
|
||||
['Stopped', 'play'],
|
||||
['Playing', 'pause'],
|
||||
['Paused', 'resume']
|
||||
])('if state is currently "%s", %s', (state, action) => {
|
||||
const m = mock(playback, action)
|
||||
song.playbackState = state
|
||||
wrapper.click('.cover .control')
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,97 @@
|
|||
import Component from '@/components/song/context-menu.vue'
|
||||
import { download } from '@/services'
|
||||
import { songStore, playlistStore, queueStore, favoriteStore, sharedStore, userStore } from '@/stores'
|
||||
import { eventBus } from '@/utils'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount, Wrapper } from '@/__tests__/adapter'
|
||||
import FunctionPropertyNames = jest.FunctionPropertyNames
|
||||
|
||||
describe('components/song/context-menu', () => {
|
||||
let songs: Song[], wrapper: Wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
userStore.current.is_admin = true
|
||||
sharedStore.state.allowDownload = true
|
||||
songs = factory<Song>('song', 2)
|
||||
|
||||
wrapper = mount(Component, {
|
||||
propsData: { songs },
|
||||
data: () => ({ copyable: true })
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders properly', async () => {
|
||||
const selectors = [
|
||||
'.playback',
|
||||
'.go-to-album',
|
||||
'.go-to-artist',
|
||||
'.after-current',
|
||||
'.bottom-queue',
|
||||
'.top-queue',
|
||||
'.favorite'
|
||||
]
|
||||
await (wrapper.vm as any).open(0, 0)
|
||||
expect(wrapper.hasAll(...selectors)).toBe(true)
|
||||
})
|
||||
|
||||
it.each<[string, string, FunctionPropertyNames<typeof queueStore>]>([
|
||||
['after current', '.after-current', 'queueAfterCurrent'],
|
||||
['to bottom', '.bottom-queue', 'queue'],
|
||||
['to top', '.top-queue', 'queueToTop']
|
||||
])('queues songs %s when "%s" is clicked', async (to, selector, queueFunc) => {
|
||||
const m = mock(queueStore, queueFunc)
|
||||
await (wrapper.vm as any).open(0, 0)
|
||||
wrapper.click(selector)
|
||||
expect(m).toHaveBeenCalledWith(songs)
|
||||
})
|
||||
|
||||
it('adds songs to favorite', async () => {
|
||||
const m = mock(favoriteStore, 'like')
|
||||
await (wrapper.vm as any).open(0, 0)
|
||||
wrapper.click('.favorite')
|
||||
expect(m).toHaveBeenCalledWith(songs)
|
||||
})
|
||||
|
||||
it('adds songs to existing playlist', async () => {
|
||||
playlistStore.all = factory<Playlist>('playlist', 5)
|
||||
const m = mock(playlistStore, 'addSongs')
|
||||
await (wrapper.vm as any).open(0, 0)
|
||||
const html = wrapper.html()
|
||||
playlistStore.all.forEach(playlist => expect(html).toMatch(playlist.name))
|
||||
wrapper.click('.playlist')
|
||||
expect(m).toHaveBeenCalledWith(playlistStore.all[0], songs)
|
||||
})
|
||||
|
||||
it('opens the edit form', async () => {
|
||||
const m = mock(eventBus, 'emit')
|
||||
userStore.current.is_admin = true
|
||||
await (wrapper.vm as any).open(0, 0)
|
||||
wrapper.click('.open-edit-form')
|
||||
expect(m).toHaveBeenCalledWith('MODAL_SHOW_EDIT_SONG_FORM', songs)
|
||||
})
|
||||
|
||||
it('downloads', async () => {
|
||||
const m = mock(download, 'fromSongs')
|
||||
await (wrapper.vm as any).open(0, 0)
|
||||
wrapper.click('.download')
|
||||
expect(m).toHaveBeenCalledWith(songs)
|
||||
})
|
||||
|
||||
it('copies URL', async () => {
|
||||
const getShareableUrlMock = mock(songStore, 'getShareableUrl')
|
||||
const execCommandMock = mock(document, 'execCommand')
|
||||
|
||||
const song = factory('song')
|
||||
await (wrapper.vm as any).open(0, 0)
|
||||
wrapper.setProps({ songs: [song] })
|
||||
wrapper.click('.copy-url')
|
||||
expect(getShareableUrlMock).toHaveBeenCalledWith(song)
|
||||
expect(execCommandMock).toHaveBeenCalledWith('copy')
|
||||
})
|
||||
})
|
134
resources/assets/js/__tests__/components/song/edit-form.spec.ts
Normal file
|
@ -0,0 +1,134 @@
|
|||
import Component from '@/components/song/edit-form.vue'
|
||||
import Typeahead from '@/components/ui/typeahead.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { songStore } from '@/stores'
|
||||
import { songInfo } from '@/services/info'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/song/edit-form', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('supports editing a single song', async () => {
|
||||
const song = factory<Song>('song', { infoRetrieved: true })
|
||||
const wrapper = mount(Component, {
|
||||
propsData: { songs: song }
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
const metaHtml = wrapper.find('.meta').html()
|
||||
expect(metaHtml).toMatch(song.title)
|
||||
expect(metaHtml).toMatch(song.album.name)
|
||||
expect(metaHtml).toMatch(song.artist.name)
|
||||
|
||||
await (wrapper.vm as any).open()
|
||||
expect(wrapper.has(Typeahead)).toBe(true)
|
||||
expect(wrapper.find('input[name=title]').value).toBe(song.title)
|
||||
expect(wrapper.find('input[name=album]').value).toBe(song.album.name)
|
||||
expect(wrapper.find('input[name=artist]').value).toBe(song.artist.name)
|
||||
expect(wrapper.find('input[name=track]').value).toBe(song.track.toString())
|
||||
|
||||
wrapper.click('#editSongTabLyrics')
|
||||
expect(wrapper.find('textarea[name=lyrics]').value).toBe(song.lyrics)
|
||||
})
|
||||
|
||||
it('fetches song information on demand', () => {
|
||||
const song = factory('song', { infoRetrieved: false })
|
||||
const fetchMock = mock(songInfo, 'fetch')
|
||||
mount(Component, {
|
||||
propsData: { songs: song }
|
||||
})
|
||||
expect(fetchMock).toHaveBeenCalledWith(song)
|
||||
})
|
||||
|
||||
it('supports editing multiple songs of multiple artists', () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
songs: factory('song', 3)
|
||||
}
|
||||
})
|
||||
|
||||
const metaHtml = wrapper.find('.meta').html()
|
||||
expect(metaHtml).toMatch('3 songs selected')
|
||||
expect(metaHtml).toMatch('Mixed Artists')
|
||||
expect(metaHtml).toMatch('Mixed Albums')
|
||||
|
||||
expect(wrapper.find('input[name=artist]').value).toBe('')
|
||||
expect(wrapper.find('input[name=album]').value).toBe('')
|
||||
expect(wrapper.has('.tabs .tab-lyrics')).toBe(false)
|
||||
})
|
||||
|
||||
it('supports editing multiple songs of same artist and different albums', () => {
|
||||
const artist = factory<Artist>('artist')
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
songs: factory('song', 5, {
|
||||
artist,
|
||||
artist_id: artist.id
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const metaHtml = wrapper.find('.meta').html()
|
||||
expect(metaHtml).toMatch('5 songs selected')
|
||||
expect(metaHtml).toMatch(artist.name)
|
||||
expect(metaHtml).toMatch('Mixed Albums')
|
||||
|
||||
expect(wrapper.find('input[name=artist]').value).toBe(artist.name)
|
||||
expect(wrapper.find('input[name=album]').value).toBe('')
|
||||
expect(wrapper.has('.tabs .tab-lyrics')).toBe(false)
|
||||
})
|
||||
|
||||
it('supports editing multiple songs in same album', () => {
|
||||
const album = factory<Album>('album')
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
songs: factory('song', 4, {
|
||||
album,
|
||||
album_id: album.id,
|
||||
artist: album.artist,
|
||||
artist_id: album.artist.id
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const metaHtml = wrapper.find('.meta').html()
|
||||
expect(metaHtml).toMatch('4 songs selected')
|
||||
expect(metaHtml).toMatch(album.name)
|
||||
expect(metaHtml).toMatch(album.artist.name)
|
||||
|
||||
expect(wrapper.find('input[name=artist]').value).toBe(album.artist.name)
|
||||
expect(wrapper.find('input[name=album]').value).toBe(album.name)
|
||||
expect(wrapper.has('.tabs .tab-lyrics')).toBe(false)
|
||||
})
|
||||
|
||||
it('saves', async () => {
|
||||
const updateStub = mock(songStore, 'update')
|
||||
const songs = factory('song', 3)
|
||||
const formData = { foo: 'bar' }
|
||||
const wrapper = mount(Component, {
|
||||
data: () => ({ formData }),
|
||||
propsData: {
|
||||
songs
|
||||
}
|
||||
})
|
||||
wrapper.submit('form')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(updateStub).toHaveBeenCalledWith(songs, formData)
|
||||
})
|
||||
|
||||
it('closes', async () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
songs: factory('song', 3)
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.click('.btn-cancel')
|
||||
expect(wrapper.hasEmitted('close')).toBe(true)
|
||||
})
|
||||
})
|
81
resources/assets/js/__tests__/components/song/item.spec.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import FunctionPropertyNames = jest.FunctionPropertyNames
|
||||
import Component from '@/components/song/item.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { playback } from '@/services'
|
||||
import { queueStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { Wrapper, shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/song/item', () => {
|
||||
let item: SongProxy, song: Song, artist: Artist, album: Album, wrapper: Wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
artist = factory<Artist>('artist')
|
||||
album = factory<Album>('album', {
|
||||
artist,
|
||||
artist_id: artist.id
|
||||
})
|
||||
|
||||
song = factory<Song>('song', {
|
||||
artist,
|
||||
album,
|
||||
artist_id: artist.id,
|
||||
album_id: album.id,
|
||||
fmtLength: '04:56'
|
||||
})
|
||||
|
||||
item = { song, selected: false }
|
||||
wrapper = shallow(Component, { propsData: {
|
||||
item,
|
||||
columns: ['track', 'title', 'artist', 'album', 'length']
|
||||
}})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders properly', () => {
|
||||
const html = wrapper.html()
|
||||
expect(html).toMatch(song.track.toString())
|
||||
expect(html).toMatch(song.title)
|
||||
expect(html).toMatch(artist.name)
|
||||
expect(html).toMatch(album.name)
|
||||
expect(html).toMatch('04:56')
|
||||
})
|
||||
|
||||
it('does not render some information if so configured', () => {
|
||||
wrapper = shallow(Component, { propsData: {
|
||||
item,
|
||||
columns: ['track', 'title', 'length']
|
||||
}})
|
||||
expect(wrapper.has('.album')).toBe(false)
|
||||
expect(wrapper.has('.artist')).toBe(false)
|
||||
})
|
||||
|
||||
it.each([[true, false], [false, true]])('queuing and playing behavior', (shouldQueue, queued) => {
|
||||
const containsStub = mock(queueStore, 'contains', queued)
|
||||
const queueStub = mock(queueStore, 'queueAfterCurrent')
|
||||
const playStub = mock(playback, 'play')
|
||||
wrapper.dblclick('tr')
|
||||
expect(containsStub).toHaveBeenCalledWith(song)
|
||||
if (queued) {
|
||||
expect(queueStub).not.toHaveBeenCalled()
|
||||
} else {
|
||||
expect(queueStub).toHaveBeenCalledWith(song)
|
||||
}
|
||||
expect(playStub).toHaveBeenCalledWith(song)
|
||||
})
|
||||
|
||||
it.each<[PlaybackState, FunctionPropertyNames<typeof playback>]>([
|
||||
['Stopped', 'play'],
|
||||
['Playing', 'pause'],
|
||||
['Paused', 'resume']
|
||||
])('if state is currently "%s", %s', (state, action) => {
|
||||
const m = mock(playback, action)
|
||||
song.playbackState = state
|
||||
wrapper.click('.play')
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,14 @@
|
|||
import Component from '@/components/song/like-button.vue'
|
||||
import { favoriteStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/song/like-button', () => {
|
||||
it('toggles', () => {
|
||||
const m = mock(favoriteStore, 'toggleOne')
|
||||
const song = factory<Song>('song')
|
||||
shallow(Component, { propsData: { song }}).click('button')
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,62 @@
|
|||
import Component from '@/components/song/list-controls.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { take } from 'lodash'
|
||||
import { shallow, mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/song/list-controls', () => {
|
||||
it('renders properly', () => {
|
||||
expect(shallow(Component)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it.each<[Song[], number]>([[factory<Song>('song', 5), 0], [factory<Song>('song', 5), 1]])(
|
||||
'allows shuffling all if less than 2 songs are selected',
|
||||
async (songs, selectedSongCount) => {
|
||||
const selectedSongs = take(songs, selectedSongCount)
|
||||
const wrapper = mount(Component, { propsData: { songs, selectedSongs } })
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.click('.btn-shuffle-all').hasEmitted('playAll', true)).toBe(true)
|
||||
})
|
||||
|
||||
it('allows shuffling selected if more than 1 song are selected', () => {
|
||||
const songs = factory<Song>('song', 5)
|
||||
|
||||
expect(shallow(Component, {
|
||||
propsData: {
|
||||
songs,
|
||||
selectedSongs: take(songs, 2)
|
||||
}
|
||||
}).click('.btn-shuffle-selected').hasEmitted('playSelected', true)).toBe(true)
|
||||
})
|
||||
|
||||
it('displays the "Add To" menu', () => {
|
||||
const songs = factory<Song>('song', 5)
|
||||
|
||||
expect(shallow(Component, {
|
||||
propsData: {
|
||||
songs,
|
||||
selectedSongs: take(songs, 2)
|
||||
}
|
||||
}).has('.btn-add-to')).toBe(true)
|
||||
})
|
||||
|
||||
it('allows clearing queue', () => {
|
||||
expect(shallow(Component, {
|
||||
propsData: {
|
||||
config: {
|
||||
clearQueue: true
|
||||
}
|
||||
}
|
||||
}).click('.btn-clear-queue').hasEmitted('clearQueue')).toBe(true)
|
||||
})
|
||||
|
||||
it('allows deleting current playlist', () => {
|
||||
expect(shallow(Component, {
|
||||
propsData: {
|
||||
config: {
|
||||
deletePlaylist: true
|
||||
}
|
||||
}
|
||||
}).click('.btn-delete-playlist').hasEmitted('deletePlaylist')).toBe(true)
|
||||
})
|
||||
})
|
124
resources/assets/js/__tests__/components/song/list.spec.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
import router from '@/router'
|
||||
import Component from '@/components/song/list.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { queueStore } from '@/stores'
|
||||
import { playback } from '@/services'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/song/list', () => {
|
||||
let songs: Song[]
|
||||
|
||||
beforeEach(() => {
|
||||
songs = factory<Song>('song', 20)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['.track-number', 'song.track'],
|
||||
['.title', 'song.title'],
|
||||
['.artist', ['song.album.artist.name', 'song.album.name', 'song.track']],
|
||||
['.album', ['song.album.name', 'song.track']],
|
||||
['.time', 'song.length']
|
||||
])('sorts when "%s" is clicked', (selector, criteria) => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
items: songs,
|
||||
type: 'all-songs'
|
||||
}
|
||||
})
|
||||
// @ts-ignore
|
||||
const m = mock(wrapper.vm, 'sort')
|
||||
wrapper.click(`.song-list-header ${selector}`)
|
||||
expect(m).toHaveBeenCalledWith(criteria)
|
||||
})
|
||||
|
||||
it('takes disc into account when sort an album song list', () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
items: songs,
|
||||
type: 'album'
|
||||
}
|
||||
})
|
||||
|
||||
;(wrapper.vm as any).sort()
|
||||
expect((wrapper.vm as any).sortFields).toContain('song.disc')
|
||||
})
|
||||
|
||||
it('plays when Enter is pressed with one selected song', () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
items: songs,
|
||||
type: 'all-songs'
|
||||
}
|
||||
})
|
||||
// select one row
|
||||
;(wrapper.vm as any).songProxies[0].selected = true
|
||||
|
||||
const m = mock(playback, 'play')
|
||||
wrapper.find('.song-list-wrap').trigger('keydown.enter')
|
||||
expect(m).toHaveBeenCalledWith(songs[0])
|
||||
})
|
||||
|
||||
it('plays when Enter is pressed in Queue screen', () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
items: songs,
|
||||
type: 'queue'
|
||||
}
|
||||
})
|
||||
|
||||
const m = mock(playback, 'play')
|
||||
;(wrapper.vm as any).songProxies[0].selected = true
|
||||
;(wrapper.vm as any).songProxies[1].selected = true
|
||||
wrapper.find('.song-list-wrap').trigger('keydown.enter')
|
||||
expect(m).toHaveBeenCalledWith(songs[0])
|
||||
})
|
||||
|
||||
it('queues when Enter is pressed in other screens', () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
items: songs,
|
||||
type: 'playlist'
|
||||
}
|
||||
})
|
||||
const queueMock = mock(queueStore, 'queue')
|
||||
const goMock = mock(router, 'go')
|
||||
const playMock = mock(playback, 'play')
|
||||
|
||||
// select 2 rows
|
||||
;(wrapper.vm as any).songProxies[0].selected = true
|
||||
;(wrapper.vm as any).songProxies[1].selected = true
|
||||
|
||||
// simple Enter adds selected songs to bottom
|
||||
wrapper.find('.song-list-wrap').trigger('keydown.enter')
|
||||
expect(queueMock).toHaveBeenCalledWith((wrapper.vm as any).selectedSongs)
|
||||
// the current screen should be switched to "Queue"
|
||||
expect(goMock).toHaveBeenCalledWith('queue')
|
||||
|
||||
// Shift+Enter queues to top
|
||||
const queueToTopMock = mock(queueStore, 'queueToTop')
|
||||
wrapper.find('.song-list-wrap').trigger('keydown.enter', { shiftKey: true })
|
||||
expect(queueToTopMock).toHaveBeenCalledWith((wrapper.vm as any).selectedSongs)
|
||||
expect(goMock).toHaveBeenCalledWith('queue')
|
||||
|
||||
// Ctrl[+Shift]+Enter queues and plays the first song
|
||||
wrapper.find('.song-list-wrap').trigger('keydown.enter', { ctrlKey: true })
|
||||
expect(playMock).toHaveBeenCalledWith((wrapper.vm as any).selectedSongs[0])
|
||||
})
|
||||
|
||||
it('selects all songs', () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
items: songs,
|
||||
type: 'playlist'
|
||||
}
|
||||
})
|
||||
wrapper.find('.song-list-wrap').trigger('keydown.a', { ctrlKey: true })
|
||||
;(wrapper.vm as any).songProxies.forEach((item: SongProxy) => expect(item.selected).toBe(true))
|
||||
})
|
||||
})
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ui/album-art-overlay requests album thumbnail 1`] = `<div data-testid="album-art-overlay" style="background-image: none;"></div>`;
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ui/context-menu renders 1`] = ``;
|
|
@ -0,0 +1,25 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ui/lyrics displays a fallback message if the song has no lyrics 1`] = `
|
||||
<article id="lyrics">
|
||||
<div class="content">
|
||||
<div style="display: none;">
|
||||
<div></div>
|
||||
<textzoomer-stub></textzoomer-stub>
|
||||
</div>
|
||||
<p class="none text-secondary"><span>No lyrics available. Are you listening to Bach?</span></p>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
exports[`components/ui/lyrics displays lyrics if the song has lyrics 1`] = `
|
||||
<article id="lyrics">
|
||||
<div class="content">
|
||||
<div>
|
||||
<div>Foo and bar</div>
|
||||
<textzoomer-stub></textzoomer-stub>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
</article>
|
||||
`;
|
|
@ -0,0 +1,39 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/shared/overlay allows option overriding 1`] = `
|
||||
<div id="overlay" class="overlay warning">
|
||||
<div class="display">
|
||||
<!---->
|
||||
<!----> <i class="fa fa-exclamation-triangle"></i>
|
||||
<!---->
|
||||
<!----> <span class="message">Foo</span></div> <button class="btn-dismiss">Close</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/shared/overlay hides 1`] = ``;
|
||||
|
||||
exports[`components/shared/overlay shows 1`] = `
|
||||
<div id="overlay" class="overlay loading">
|
||||
<div class="display">
|
||||
<div data-test="soundbars" class="bars"><img src="@/../img/bars.gif" alt="Sound bars" height="13" width="auto"></div>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!----> <span class="message"></span>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/shared/overlay shows with default options 1`] = `
|
||||
<div id="overlay" class="overlay loading">
|
||||
<div class="display">
|
||||
<div data-test="soundbars" class="bars"><img src="@/../img/bars.gif" alt="Sound bars" height="13" width="auto"></div>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!----> <span class="message"></span>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/song/list-controls-toggler renders properly 1`] = `<span class="controls-toggler text-orange"><i class="fa fa-angle-up toggler"></i></span>`;
|
|
@ -0,0 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ui/to-top-button renders properly 1`] = `
|
||||
<div class="to-top-btn-wrapper" style="display: none;" name="fade"><button title="Scroll to top"><i class="fa fa-arrow-circle-up"></i> Top
|
||||
</button></div>
|
||||
`;
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ui/volume renders properly 1`] = `<span id="volume" class="volume control"><i role="button" tabindex="0" title="Mute" class="fa fa-volume-up mute"></i> <input id="volumeRange" max="10" step="0.1" title="Volume" type="range" class="plyr__volume"></span>`;
|
|
@ -0,0 +1,24 @@
|
|||
import factory from '@/__tests__/factory'
|
||||
import Component from '@/components/ui/album-art-overlay.vue'
|
||||
import { albumStore } from '@/stores/album'
|
||||
import { shallow } from '@/__tests__/adapter'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { preferenceStore } from '@/stores'
|
||||
|
||||
describe('components/ui/album-art-overlay', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('requests album thumbnail', async () => {
|
||||
preferenceStore.state.showAlbumArtOverlay = true
|
||||
const getCoverThumbnailMock = mock(albumStore, 'getThumbnail').mockResolvedValue('http://localhost/foo_thumb.jpg')
|
||||
|
||||
const song = factory<Song>('song')
|
||||
const wrapper = shallow(Component)
|
||||
wrapper.setProps({ song })
|
||||
expect(getCoverThumbnailMock).toHaveBeenCalledWith(song.album)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,70 @@
|
|||
import Component from '@/components/ui/album-artist-thumbnail.vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { playback } from '@/services'
|
||||
import { queueStore, sharedStore } from '@/stores'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { Wrapper, shallow } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/ui/album-artist-thumbnail(album)', () => {
|
||||
let album: Album
|
||||
let wrapper: Wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
album = factory<Album>('album', {
|
||||
songs: factory<Song>('song', 10)
|
||||
})
|
||||
// @ts-ignore
|
||||
sharedStore.state = { allowDownload: true }
|
||||
wrapper = shallow(Component, { propsData: { entity: album } })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('plays if clicked', () => {
|
||||
const m = mock(playback, 'playAllInAlbum')
|
||||
wrapper.click('.control-play')
|
||||
expect(m).toHaveBeenCalledWith(album, false)
|
||||
})
|
||||
|
||||
it.each([['metaKey'], ['ctrlKey']])('queues if %s is clicked', key => {
|
||||
const m = mock(queueStore, 'queue')
|
||||
wrapper.click('.control-play', { [key]: true })
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('components/ui/album-artist-thumbnail(artist)', () => {
|
||||
let artist: Artist
|
||||
let wrapper: Wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
sharedStore.state = { allowDownload: true }
|
||||
artist = factory<Artist>('artist', {
|
||||
id: 3, // make sure it's not "Various Artists"
|
||||
albums: factory<Album>('album', 4),
|
||||
songs: factory<Song>('song', 16)
|
||||
})
|
||||
wrapper = shallow(Component, { propsData: { entity: artist } })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('plays if clicked', () => {
|
||||
const m = mock(playback, 'playAllByArtist')
|
||||
wrapper.click('.control-play')
|
||||
expect(m).toHaveBeenCalledWith(artist, false)
|
||||
})
|
||||
|
||||
it.each([['metaKey'], ['ctrlKey']])('queues if %s is clicked', key => {
|
||||
const m = mock(queueStore, 'queue')
|
||||
wrapper.click('.control-play', { [key]: true })
|
||||
expect(m).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,10 @@
|
|||
import Component from '@/components/ui/close-modal-btn.vue'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
describe('components/ui/close-modal-btn', () => {
|
||||
it('emits a click event', () => {
|
||||
const wrapper = mount(Component)
|
||||
wrapper.click('button')
|
||||
expect(wrapper.hasEmitted('click'))
|
||||
})
|
||||
})
|
|
@ -0,0 +1,42 @@
|
|||
import Component from '@/components/ui/context-menu.vue'
|
||||
import { eventBus } from '@/utils'
|
||||
import { mock } from '@/__tests__/__helpers__'
|
||||
import { mount } from '@/__tests__/adapter'
|
||||
|
||||
declare const global: any
|
||||
|
||||
describe('components/ui/context-menu', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(mount(Component)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders extra CSS classes', async () => {
|
||||
const wrapper = mount(Component, {
|
||||
propsData: {
|
||||
extraClass: 'foo'
|
||||
}
|
||||
})
|
||||
await (wrapper.vm as any).open(0, 0)
|
||||
expect(wrapper.find('.menu').hasClass('foo')).toBe(true)
|
||||
})
|
||||
|
||||
it('opens', () => {
|
||||
const wrapper = mount(Component)
|
||||
;(wrapper.vm as any).open(42, 128)
|
||||
expect(wrapper.find('.menu').element.style.top).toBe('42px')
|
||||
expect(wrapper.find('.menu').element.style.left).toBe('128px')
|
||||
expect(global.getComputedStyle(wrapper.find('.menu').element).display).toBe('block')
|
||||
})
|
||||
|
||||
it('closes', async () => {
|
||||
const wrapper = mount(Component)
|
||||
await (wrapper.vm as any).open(42, 128)
|
||||
;(wrapper.vm as any).close()
|
||||
expect(wrapper.html()).toBeUndefined()
|
||||
})
|
||||
})
|