From ede72d62b0894a38fbf981f46c35feb686619633 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 8 Jan 2018 22:47:14 +0100 Subject: [PATCH 01/10] Initial setup for phtantomjs --- dev.yml | 4 +--- front/Dockerfile | 16 +++++++--------- front/docker/Dockerfile.dev | 13 ------------- front/test/unit/karma.conf.js | 2 ++ 4 files changed, 10 insertions(+), 25 deletions(-) delete mode 100644 front/docker/Dockerfile.dev diff --git a/dev.yml b/dev.yml index befc4b243..971e38b62 100644 --- a/dev.yml +++ b/dev.yml @@ -3,9 +3,7 @@ version: '2' services: front: - build: - dockerfile: docker/Dockerfile.dev - context: ./front + build: front env_file: .env.dev environment: - "HOST=0.0.0.0" diff --git a/front/Dockerfile b/front/Dockerfile index ad05f72eb..b22359b46 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,13 +1,11 @@ -FROM node:6-alpine +FROM node:6 EXPOSE 8080 - -RUN mkdir /app -WORKDIR /app +WORKDIR /app/ ADD package.json . +RUN npm install --only=production +RUN npm install --only=dev +VOLUME ["/app/node_modules"] +COPY . . -RUN npm install - -ADD . . - -RUN npm run build +CMD ["npm", "run", "dev"] diff --git a/front/docker/Dockerfile.dev b/front/docker/Dockerfile.dev deleted file mode 100644 index 1a0c90c9e..000000000 --- a/front/docker/Dockerfile.dev +++ /dev/null @@ -1,13 +0,0 @@ -FROM node:6-alpine - -EXPOSE 8080 - -RUN mkdir /app -WORKDIR /app -ADD package.json . - -RUN npm install - -VOLUME ["/app/node_modules"] - -CMD ["npm", "run", "dev"] diff --git a/front/test/unit/karma.conf.js b/front/test/unit/karma.conf.js index 8e4951c9e..3fcc5f02a 100644 --- a/front/test/unit/karma.conf.js +++ b/front/test/unit/karma.conf.js @@ -18,6 +18,8 @@ module.exports = function (config) { preprocessors: { './index.js': ['webpack', 'sourcemap'] }, + captureTimeout: 5000, + retryLimit: 1, webpack: webpackConfig, webpackMiddleware: { noInfo: true From 91d6a71a21d655f6f103de6a66a21ee4bdd352e3 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 8 Jan 2018 23:12:31 +0100 Subject: [PATCH 02/10] Can now launch tests --- front/package.json | 1 + front/src/assets/logo/{favicon.ico => favicon.png} | Bin front/src/config.js | 2 +- front/test/unit/karma.conf.js | 5 ++++- front/test/unit/specs/Hello.spec.js | 11 ----------- 5 files changed, 6 insertions(+), 13 deletions(-) rename front/src/assets/logo/{favicon.ico => favicon.png} (100%) delete mode 100644 front/test/unit/specs/Hello.spec.js diff --git a/front/package.json b/front/package.json index 58c22a408..e79ffcbeb 100644 --- a/front/package.json +++ b/front/package.json @@ -46,6 +46,7 @@ "cross-env": "^4.0.0", "cross-spawn": "^5.0.1", "css-loader": "^0.28.0", + "es6-promise": "^4.2.2", "eslint": "^3.19.0", "eslint-config-standard": "^6.2.1", "eslint-friendly-formatter": "^2.0.7", diff --git a/front/src/assets/logo/favicon.ico b/front/src/assets/logo/favicon.png similarity index 100% rename from front/src/assets/logo/favicon.ico rename to front/src/assets/logo/favicon.png diff --git a/front/src/config.js b/front/src/config.js index 3ba1247ac..76a6f5e5f 100644 --- a/front/src/config.js +++ b/front/src/config.js @@ -4,7 +4,7 @@ class Config { if (this.BACKEND_URL === '/') { this.BACKEND_URL = window.location.protocol + '//' + window.location.hostname + ':' + window.location.port } - if (!this.BACKEND_URL.endsWith('/')) { + if (!this.BACKEND_URL.slice(-1) === '/') { this.BACKEND_URL += '/' } this.API_URL = this.BACKEND_URL + 'api/v1/' diff --git a/front/test/unit/karma.conf.js b/front/test/unit/karma.conf.js index 3fcc5f02a..da5a21876 100644 --- a/front/test/unit/karma.conf.js +++ b/front/test/unit/karma.conf.js @@ -14,7 +14,10 @@ module.exports = function (config) { browsers: ['PhantomJS'], frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], reporters: ['spec', 'coverage'], - files: ['./index.js'], + files: [ + '../../node_modules/es6-promise/dist/es6-promise.auto.js', + './index.js' + ], preprocessors: { './index.js': ['webpack', 'sourcemap'] }, diff --git a/front/test/unit/specs/Hello.spec.js b/front/test/unit/specs/Hello.spec.js deleted file mode 100644 index 80140baa9..000000000 --- a/front/test/unit/specs/Hello.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import Vue from 'vue' -import Hello from '@/components/Hello' - -describe('Hello.vue', () => { - it('should render correct contents', () => { - const Constructor = Vue.extend(Hello) - const vm = new Constructor().$mount() - expect(vm.$el.querySelector('.hello h1').textContent) - .to.equal('Welcome to Your Vue.js App') - }) -}) From f7d876aec664b0884ecb1c157b373de3cdb83471 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 8 Jan 2018 23:12:45 +0100 Subject: [PATCH 03/10] First store tests \o/ :party: --- front/test/unit/specs/store/auth.spec.js | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 front/test/unit/specs/store/auth.spec.js diff --git a/front/test/unit/specs/store/auth.spec.js b/front/test/unit/specs/store/auth.spec.js new file mode 100644 index 000000000..6ef558f6e --- /dev/null +++ b/front/test/unit/specs/store/auth.spec.js @@ -0,0 +1,52 @@ +import store from '@/store/player' + +describe('mutations', () => { + it('set volume', () => { + // mock state + const state = { volume: 0 } + // apply mutation + store.mutations.volume(state, 0.9) + // assert result + expect(state.volume).to.equal(0.9) + }) + it('set volume max 1', () => { + // mock state + const state = { volume: 0 } + // apply mutation + store.mutations.volume(state, 2) + // assert result + expect(state.volume).to.equal(1) + }) + it('set volume min to 0', () => { + // mock state + const state = { volume: 0.5 } + // apply mutation + store.mutations.volume(state, -2) + // assert result + expect(state.volume).to.equal(0) + }) + it('increment volume', () => { + // mock state + const state = { volume: 0 } + // apply mutation + store.mutations.incrementVolume(state, 0.1) + // assert result + expect(state.volume).to.equal(0.1) + }) + it('increment volume max 1', () => { + // mock state + const state = { volume: 0 } + // apply mutation + store.mutations.incrementVolume(state, 2) + // assert result + expect(state.volume).to.equal(1) + }) + it('increment volume min to 0', () => { + // mock state + const state = { volume: 0.5 } + // apply mutation + store.mutations.incrementVolume(state, -2) + // assert result + expect(state.volume).to.equal(0) + }) +}) From cb2238c5761f87228aa619c6f86c4a0b74cd99cb Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 9 Jan 2018 20:47:03 +0100 Subject: [PATCH 04/10] Should now run frontend tests on gitlab --- .gitlab-ci.yml | 51 ++++++++++++++++++++++++++++++++++-------------- front/Dockerfile | 6 +++--- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91b11e8bd..cde12894a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ variables: IMAGE_NAME: funkwhale/funkwhale IMAGE: $IMAGE_NAME:$CI_COMMIT_REF_NAME IMAGE_LATEST: $IMAGE_NAME:latest - + PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache" stages: @@ -14,40 +14,61 @@ test_api: services: - postgres:9.4 stage: test - image: funkwhale/funkwhale:base + image: funkwhale/funkwhale:latest + cache: + key: "$CI_PROJECT_ID/pip_cache" + paths: + - "$PIP_CACHE_DIR" variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache" DATABASE_URL: "postgresql://postgres@postgres/postgres" before_script: - - python3 -m venv --copies virtualenv - - source virtualenv/bin/activate - cd api - pip install -r requirements/base.txt - pip install -r requirements/local.txt - pip install -r requirements/test.txt script: - pytest - cache: - key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" - paths: - - "$CI_PROJECT_DIR/pip-cache" tags: - docker -build_front: - stage: build - image: node:6-alpine + +test_front: + stage: test + image: node:9 before_script: - cd front script: - - npm install - - npm run build + - yarn install + - yarn run unit cache: - key: "$CI_COMMIT_REF_NAME" + key: "$CI_PROJECT_ID/front_dependencies" paths: - front/node_modules + - front/yarn.lock + artifacts: + name: "front_${CI_COMMIT_REF_NAME}" + paths: + - front/dist/ + tags: + - docker + + +build_front: + stage: build + image: node:9 + before_script: + - cd front + + script: + - yarn install + - yarn run build + cache: + key: "$CI_PROJECT_ID/front_dependencies" + paths: + - front/node_modules + - front/yarn.lock artifacts: name: "front_${CI_COMMIT_REF_NAME}" paths: diff --git a/front/Dockerfile b/front/Dockerfile index b22359b46..cdf92446b 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,10 +1,10 @@ -FROM node:6 +FROM node:9 EXPOSE 8080 WORKDIR /app/ ADD package.json . -RUN npm install --only=production -RUN npm install --only=dev +RUN yarn install --only=production +RUN yarn install --only=dev VOLUME ["/app/node_modules"] COPY . . From 03364dd0d08e66efd79d31622b28a36b7c31ab42 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 9 Jan 2018 21:22:05 +0100 Subject: [PATCH 05/10] Now use taggit from taggit --- api/requirements/base.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/requirements/base.txt b/api/requirements/base.txt index cff16d3f1..ce0eb9b85 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -47,8 +47,7 @@ mutagen>=1.39,<1.40 # Until this is merged -#django-taggit>=0.22,<0.23 -git+https://github.com/alex/django-taggit.git@95776ac66948ed7ba7c12e35c1170551e3be66a5 +django-taggit>=0.22,<0.23 # Until this is merged git+https://github.com/EliotBerriot/PyMemoize.git@django # Until this is merged From bc76048b4a179cfe3b871c1683ffbb3e1bea4954 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 9 Jan 2018 22:54:06 +0100 Subject: [PATCH 06/10] Added tests for auth, favorites and player in store --- front/package.json | 1 + front/src/store/auth.js | 1 + front/src/store/player.js | 4 +- front/test/unit/karma.conf.js | 2 +- front/test/unit/specs/store/auth.spec.js | 150 +++++++++++------ front/test/unit/specs/store/favorites.spec.js | 52 ++++++ front/test/unit/specs/store/player.spec.js | 153 ++++++++++++++++++ front/test/unit/utils.js | 68 ++++++++ 8 files changed, 381 insertions(+), 50 deletions(-) create mode 100644 front/test/unit/specs/store/favorites.spec.js create mode 100644 front/test/unit/specs/store/player.spec.js create mode 100644 front/test/unit/utils.js diff --git a/front/package.json b/front/package.json index e79ffcbeb..2818757a3 100644 --- a/front/package.json +++ b/front/package.json @@ -9,6 +9,7 @@ "start": "node build/dev-server.js", "build": "node build/build.js", "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", + "unit-watch": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js", "e2e": "node test/e2e/runner.js", "test": "npm run unit && npm run e2e", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" diff --git a/front/src/store/auth.js b/front/src/store/auth.js index d8bd197f3..6021f30fa 100644 --- a/front/src/store/auth.js +++ b/front/src/store/auth.js @@ -44,6 +44,7 @@ export default { state.token = value if (value) { state.tokenData = jwtDecode(value) + console.log(state.tokenData) } else { state.tokenData = {} } diff --git a/front/src/store/player.js b/front/src/store/player.js index 74b0b9f9e..9e50d6c19 100644 --- a/front/src/store/player.js +++ b/front/src/store/player.js @@ -61,8 +61,8 @@ export default { } }, actions: { - incrementVolume (context, value) { - context.commit('volume', context.state.volume + value) + incrementVolume ({commit, state}, value) { + commit('volume', state.volume + value) }, stop (context) { }, diff --git a/front/test/unit/karma.conf.js b/front/test/unit/karma.conf.js index da5a21876..47b468807 100644 --- a/front/test/unit/karma.conf.js +++ b/front/test/unit/karma.conf.js @@ -21,7 +21,7 @@ module.exports = function (config) { preprocessors: { './index.js': ['webpack', 'sourcemap'] }, - captureTimeout: 5000, + captureTimeout: 15000, retryLimit: 1, webpack: webpackConfig, webpackMiddleware: { diff --git a/front/test/unit/specs/store/auth.spec.js b/front/test/unit/specs/store/auth.spec.js index 6ef558f6e..8be2708f3 100644 --- a/front/test/unit/specs/store/auth.spec.js +++ b/front/test/unit/specs/store/auth.spec.js @@ -1,52 +1,108 @@ -import store from '@/store/player' +import store from '@/store/auth' -describe('mutations', () => { - it('set volume', () => { - // mock state - const state = { volume: 0 } - // apply mutation - store.mutations.volume(state, 0.9) - // assert result - expect(state.volume).to.equal(0.9) +import { testAction } from '../../utils' + +describe('store/auth', () => { + describe('mutations', () => { + it('profile', () => { + const state = {} + store.mutations.profile(state, {}) + expect(state.profile).to.deep.equal({}) + }) + it('username', () => { + const state = {} + store.mutations.username(state, 'world') + expect(state.username).to.equal('world') + }) + it('authenticated true', () => { + const state = {} + store.mutations.authenticated(state, true) + expect(state.authenticated).to.equal(true) + }) + it('authenticated false', () => { + const state = { + username: 'dummy', + token: 'dummy', + tokenData: 'dummy', + profile: 'dummy', + availablePermissions: 'dummy' + } + store.mutations.authenticated(state, false) + expect(state.authenticated).to.equal(false) + expect(state.username).to.equal(null) + expect(state.token).to.equal(null) + expect(state.tokenData).to.equal(null) + expect(state.profile).to.equal(null) + expect(state.availablePermissions).to.deep.equal({}) + }) + it('token null', () => { + const state = {} + store.mutations.token(state, null) + expect(state.token).to.equal(null) + expect(state.tokenData).to.deep.equal({}) + }) + it('token real', () => { + // generated on http://kjur.github.io/jsjws/tool_jwt.html + const state = {} + let token = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTUxNTUzMzQyOSwiZXhwIjoxNTE1NTM3MDI5LCJpYXQiOjE1MTU1MzM0MjksImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.' + let tokenData = { + iss: 'https://jwt-idp.example.com', + sub: 'mailto:mike@example.com', + nbf: 1515533429, + exp: 1515537029, + iat: 1515533429, + jti: 'id123456', + typ: 'https://example.com/register' + } + store.mutations.token(state, token) + expect(state.token).to.equal(token) + expect(state.tokenData).to.deep.equal(tokenData) + }) + it('permissions', () => { + const state = { availablePermissions: {} } + store.mutations.permission(state, {key: 'admin', status: true}) + expect(state.availablePermissions).to.deep.equal({admin: true}) + }) }) - it('set volume max 1', () => { - // mock state - const state = { volume: 0 } - // apply mutation - store.mutations.volume(state, 2) - // assert result - expect(state.volume).to.equal(1) + describe('getters', () => { + it('header', () => { + const state = { token: 'helloworld' } + expect(store.getters['header'](state)).to.equal('JWT helloworld') + }) }) - it('set volume min to 0', () => { - // mock state - const state = { volume: 0.5 } - // apply mutation - store.mutations.volume(state, -2) - // assert result - expect(state.volume).to.equal(0) - }) - it('increment volume', () => { - // mock state - const state = { volume: 0 } - // apply mutation - store.mutations.incrementVolume(state, 0.1) - // assert result - expect(state.volume).to.equal(0.1) - }) - it('increment volume max 1', () => { - // mock state - const state = { volume: 0 } - // apply mutation - store.mutations.incrementVolume(state, 2) - // assert result - expect(state.volume).to.equal(1) - }) - it('increment volume min to 0', () => { - // mock state - const state = { volume: 0.5 } - // apply mutation - store.mutations.incrementVolume(state, -2) - // assert result - expect(state.volume).to.equal(0) + describe('actions', () => { + it('logout', (done) => { + testAction({ + action: store.actions.logout, + params: {state: {}}, + expectedMutations: [ + { type: 'authenticated', payload: false } + ] + }, done) + }) + it('check jwt null', (done) => { + testAction({ + action: store.actions.check, + params: {state: {}}, + expectedMutations: [ + { type: 'authenticated', payload: false } + ] + }, done) + }) + it('check jwt set', (done) => { + testAction({ + action: store.actions.check, + params: {state: {token: 'test', username: 'user'}}, + expectedMutations: [ + { type: 'authenticated', payload: true }, + { type: 'username', payload: 'user' }, + { type: 'token', payload: 'test' } + ], + expectedActions: [ + { type: 'fetchProfile' }, + { type: 'refreshToken' } + ] + }, done) + }) }) }) diff --git a/front/test/unit/specs/store/favorites.spec.js b/front/test/unit/specs/store/favorites.spec.js new file mode 100644 index 000000000..6d4314ca6 --- /dev/null +++ b/front/test/unit/specs/store/favorites.spec.js @@ -0,0 +1,52 @@ +import store from '@/store/favorites' + +import { testAction } from '../../utils' + +describe('store/favorites', () => { + describe('mutations', () => { + it('track true', () => { + const state = { tracks: [] } + store.mutations.track(state, {id: 1, value: true}) + expect(state.tracks).to.deep.equal([1]) + expect(state.count).to.deep.equal(1) + }) + it('track false', () => { + const state = { tracks: [1] } + store.mutations.track(state, {id: 1, value: false}) + expect(state.tracks).to.deep.equal([]) + expect(state.count).to.deep.equal(0) + }) + }) + describe('getters', () => { + it('isFavorite true', () => { + const state = { tracks: [1] } + expect(store.getters['isFavorite'](state)(1)).to.equal(true) + }) + it('isFavorite false', () => { + const state = { tracks: [] } + expect(store.getters['isFavorite'](state)(1)).to.equal(false) + }) + }) + describe('actions', () => { + it('toggle true', (done) => { + testAction({ + action: store.actions.toggle, + payload: 1, + params: {getters: {isFavorite: () => false}}, + expectedActions: [ + { type: 'set', payload: {id: 1, value: true} } + ] + }, done) + }) + it('toggle true', (done) => { + testAction({ + action: store.actions.toggle, + payload: 1, + params: {getters: {isFavorite: () => true}}, + expectedActions: [ + { type: 'set', payload: {id: 1, value: false} } + ] + }, done) + }) + }) +}) diff --git a/front/test/unit/specs/store/player.spec.js b/front/test/unit/specs/store/player.spec.js new file mode 100644 index 000000000..af0b6b435 --- /dev/null +++ b/front/test/unit/specs/store/player.spec.js @@ -0,0 +1,153 @@ +import store from '@/store/player' + +import { testAction } from '../../utils' + +describe('store/player', () => { + describe('mutations', () => { + it('set volume', () => { + const state = { volume: 0 } + store.mutations.volume(state, 0.9) + expect(state.volume).to.equal(0.9) + }) + it('set volume max 1', () => { + const state = { volume: 0 } + store.mutations.volume(state, 2) + expect(state.volume).to.equal(1) + }) + it('set volume min to 0', () => { + const state = { volume: 0.5 } + store.mutations.volume(state, -2) + expect(state.volume).to.equal(0) + }) + it('increment volume', () => { + const state = { volume: 0 } + store.mutations.incrementVolume(state, 0.1) + expect(state.volume).to.equal(0.1) + }) + it('increment volume max 1', () => { + const state = { volume: 0 } + store.mutations.incrementVolume(state, 2) + expect(state.volume).to.equal(1) + }) + it('increment volume min to 0', () => { + const state = { volume: 0.5 } + store.mutations.incrementVolume(state, -2) + expect(state.volume).to.equal(0) + }) + it('set duration', () => { + const state = { duration: 42 } + store.mutations.duration(state, 14) + expect(state.duration).to.equal(14) + }) + it('set errored', () => { + const state = { errored: false } + store.mutations.errored(state, true) + expect(state.errored).to.equal(true) + }) + it('set looping', () => { + const state = { looping: 1 } + store.mutations.looping(state, 2) + expect(state.looping).to.equal(2) + }) + it('set playing', () => { + const state = { playing: false } + store.mutations.playing(state, true) + expect(state.playing).to.equal(true) + }) + it('set current time', () => { + const state = { currentTime: 1 } + store.mutations.currentTime(state, 2) + expect(state.currentTime).to.equal(2) + }) + it('toggle looping from 0', () => { + const state = { looping: 0 } + store.mutations.toggleLooping(state) + expect(state.looping).to.equal(1) + }) + it('toggle looping from 1', () => { + const state = { looping: 1 } + store.mutations.toggleLooping(state) + expect(state.looping).to.equal(2) + }) + it('toggle looping from 2', () => { + const state = { looping: 2 } + store.mutations.toggleLooping(state) + expect(state.looping).to.equal(0) + }) + }) + describe('getters', () => { + it('durationFormatted', () => { + const state = { duration: 12.51 } + expect(store.getters['durationFormatted'](state)).to.equal('00:13') + }) + it('currentTimeFormatted', () => { + const state = { currentTime: 12.51 } + expect(store.getters['currentTimeFormatted'](state)).to.equal('00:13') + }) + it('progress', () => { + const state = { currentTime: 4, duration: 10 } + expect(store.getters['progress'](state)).to.equal(40) + }) + }) + describe('actions', () => { + it('incrementVolume', (done) => { + testAction({ + action: store.actions.incrementVolume, + payload: 0.2, + params: {state: {volume: 0.7}}, + expectedMutations: [ + { type: 'volume', payload: 0.7 + 0.2 } + ] + }, done) + }) + it('toggle play false', (done) => { + testAction({ + action: store.actions.togglePlay, + params: {state: {playing: false}}, + expectedMutations: [ + { type: 'playing', payload: true } + ] + }, done) + }) + it('toggle play true', (done) => { + testAction({ + action: store.actions.togglePlay, + params: {state: {playing: true}}, + expectedMutations: [ + { type: 'playing', payload: false } + ] + }, done) + }) + it('trackEnded', (done) => { + testAction({ + action: store.actions.trackEnded, + payload: {test: 'track'}, + expectedActions: [ + { type: 'trackListened', payload: {test: 'track'} }, + { type: 'queue/next', payload: null, options: {root: true} } + ] + }, done) + }) + it('trackErrored', (done) => { + testAction({ + action: store.actions.trackErrored, + payload: {test: 'track'}, + expectedMutations: [ + { type: 'errored', payload: true } + ], + expectedActions: [ + { type: 'queue/next', payload: null, options: {root: true} } + ] + }, done) + }) + it('updateProgress', (done) => { + testAction({ + action: store.actions.updateProgress, + payload: 1, + expectedMutations: [ + { type: 'currentTime', payload: 1 } + ] + }, done) + }) + }) +}) diff --git a/front/test/unit/utils.js b/front/test/unit/utils.js new file mode 100644 index 000000000..e67c7687f --- /dev/null +++ b/front/test/unit/utils.js @@ -0,0 +1,68 @@ +// helper for testing action with expected mutations +export const testAction = ({action, payload, params, expectedMutations, expectedActions}, done) => { + let mutationsCount = 0 + let actionsCount = 0 + + if (!expectedMutations) { + expectedMutations = [] + } + if (!expectedActions) { + expectedActions = [] + } + const isOver = () => { + return mutationsCount >= expectedMutations.length && actionsCount >= expectedActions.length + } + // mock commit + const commit = (type, payload) => { + const mutation = expectedMutations[mutationsCount] + + try { + expect(mutation.type).to.equal(type) + if (payload) { + expect(mutation.payload).to.deep.equal(payload) + } + } catch (error) { + done(error) + } + + mutationsCount++ + if (isOver()) { + done() + } + } + // mock dispatch + const dispatch = (type, payload, options) => { + const a = expectedActions[actionsCount] + + try { + expect(a.type).to.equal(type) + if (payload) { + expect(a.payload).to.deep.equal(payload) + } + if (a.options) { + expect(options).to.deep.equal(a.options) + } + } catch (error) { + done(error) + } + + actionsCount++ + if (isOver()) { + done() + } + } + + // call the action with mocked store and arguments + action({ commit, dispatch, ...params }, payload) + + // check if no mutations should have been dispatched + if (expectedMutations.length === 0) { + expect(mutationsCount).to.equal(0) + } + if (expectedActions.length === 0) { + expect(actionsCount).to.equal(0) + } + if (isOver()) { + done() + } +} From c7070042fb3312640fa35da1b1f8a573b0b1bf98 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 11 Jan 2018 18:46:28 +0100 Subject: [PATCH 07/10] Unit tests for queue store --- front/src/store/queue.js | 35 ++- front/test/unit/specs/store/queue.spec.js | 321 ++++++++++++++++++++++ 2 files changed, 338 insertions(+), 18 deletions(-) create mode 100644 front/test/unit/specs/store/queue.spec.js diff --git a/front/src/store/queue.js b/front/src/store/queue.js index 5dde19bd8..ac28a1e0e 100644 --- a/front/src/store/queue.js +++ b/front/src/store/queue.js @@ -55,33 +55,32 @@ export default { } }, actions: { - append (context, {track, index, skipPlay}) { - index = index || context.state.tracks.length - if (index > context.state.tracks.length - 1) { + append ({commit, state, dispatch}, {track, index, skipPlay}) { + index = index || state.tracks.length + if (index > state.tracks.length - 1) { // we simply push to the end - context.commit('insert', {track, index: context.state.tracks.length}) + commit('insert', {track, index: state.tracks.length}) } else { // we insert the track at given position - context.commit('insert', {track, index}) + commit('insert', {track, index}) } if (!skipPlay) { - context.dispatch('resume') + dispatch('resume') } - // this.cache() }, - appendMany (context, {tracks, index}) { + appendMany ({state, dispatch}, {tracks, index}) { logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title })) - if (context.state.tracks.length === 0) { + if (state.tracks.length === 0) { index = 0 } else { - index = index || context.state.tracks.length + index = index || state.tracks.length } tracks.forEach((t) => { - context.dispatch('append', {track: t, index: index, skipPlay: true}) + dispatch('append', {track: t, index: index, skipPlay: true}) index += 1 }) - context.dispatch('resume') + dispatch('resume') }, cleanTrack ({state, dispatch, commit}, index) { @@ -100,14 +99,14 @@ export default { } }, - resume (context) { - if (context.state.ended | context.rootState.player.errored) { - context.dispatch('next') + resume ({state, dispatch, rootState}) { + if (state.ended | rootState.player.errored) { + dispatch('next') } }, - previous (context) { - if (context.state.currentIndex > 0) { - context.dispatch('currentIndex', context.state.currentIndex - 1) + previous ({state, dispatch}) { + if (state.currentIndex > 0) { + dispatch('currentIndex', state.currentIndex - 1) } }, next ({state, dispatch, commit, rootState}) { diff --git a/front/test/unit/specs/store/queue.spec.js b/front/test/unit/specs/store/queue.spec.js new file mode 100644 index 000000000..2e12a254b --- /dev/null +++ b/front/test/unit/specs/store/queue.spec.js @@ -0,0 +1,321 @@ +import _ from 'lodash' + +import store from '@/store/queue' +import { testAction } from '../../utils' + +describe('store/queue', () => { + describe('mutations', () => { + it('currentIndex', () => { + const state = {} + store.mutations.currentIndex(state, 2) + expect(state.currentIndex).to.equal(2) + }) + it('ended', () => { + const state = {} + store.mutations.ended(state, false) + expect(state.ended).to.equal(false) + }) + it('tracks', () => { + const state = {} + store.mutations.tracks(state, [1, 2]) + expect(state.tracks).to.deep.equal([1, 2]) + }) + it('splice', () => { + const state = {tracks: [1, 2, 3]} + store.mutations.splice(state, {start: 1, size: 2}) + expect(state.tracks).to.deep.equal([1]) + }) + it('insert', () => { + const state = {tracks: [1, 3]} + store.mutations.insert(state, {track: 2, index: 1}) + expect(state.tracks).to.deep.equal([1, 2, 3]) + }) + it('reorder before', () => { + const state = {currentIndex: 3} + store.mutations.reorder(state, {oldIndex: 2, newIndex: 1}) + expect(state.currentIndex).to.equal(3) + }) + it('reorder from after to before', () => { + const state = {currentIndex: 3} + store.mutations.reorder(state, {oldIndex: 4, newIndex: 1}) + expect(state.currentIndex).to.equal(4) + }) + it('reorder after', () => { + const state = {currentIndex: 3} + store.mutations.reorder(state, {oldIndex: 4, newIndex: 5}) + expect(state.currentIndex).to.equal(3) + }) + it('reorder before to after', () => { + const state = {currentIndex: 3} + store.mutations.reorder(state, {oldIndex: 1, newIndex: 5}) + expect(state.currentIndex).to.equal(2) + }) + it('reorder current', () => { + const state = {currentIndex: 3} + store.mutations.reorder(state, {oldIndex: 3, newIndex: 1}) + expect(state.currentIndex).to.equal(1) + }) + }) + describe('getters', () => { + it('currentTrack', () => { + const state = { tracks: [1, 2, 3], currentIndex: 2 } + expect(store.getters['currentTrack'](state)).to.equal(3) + }) + it('hasNext true', () => { + const state = { tracks: [1, 2, 3], currentIndex: 1 } + expect(store.getters['hasNext'](state)).to.equal(true) + }) + it('hasNext false', () => { + const state = { tracks: [1, 2, 3], currentIndex: 2 } + expect(store.getters['hasNext'](state)).to.equal(false) + }) + it('hasPrevious true', () => { + const state = { currentIndex: 1 } + expect(store.getters['hasPrevious'](state)).to.equal(true) + }) + it('hasPrevious false', () => { + const state = { currentIndex: 0 } + expect(store.getters['hasPrevious'](state)).to.equal(false) + }) + }) + describe('actions', () => { + it('append at end', (done) => { + testAction({ + action: store.actions.append, + payload: {track: 4, skipPlay: true}, + params: {state: {tracks: [1, 2, 3]}}, + expectedMutations: [ + { type: 'insert', payload: {track: 4, index: 3} } + ] + }, done) + }) + it('append at index', (done) => { + testAction({ + action: store.actions.append, + payload: {track: 2, index: 1, skipPlay: true}, + params: {state: {tracks: [1, 3]}}, + expectedMutations: [ + { type: 'insert', payload: {track: 2, index: 1} } + ] + }, done) + }) + it('append and play', (done) => { + testAction({ + action: store.actions.append, + payload: {track: 3}, + params: {state: {tracks: [1, 2]}}, + expectedMutations: [ + { type: 'insert', payload: {track: 3, index: 2} } + ], + expectedActions: [ + { type: 'resume' } + ] + }, done) + }) + it('appendMany', (done) => { + const tracks = [{title: 1}, {title: 2}] + testAction({ + action: store.actions.appendMany, + payload: {tracks: tracks}, + params: {state: {tracks: []}}, + expectedActions: [ + { type: 'append', payload: {track: tracks[0], index: 0, skipPlay: true} }, + { type: 'append', payload: {track: tracks[1], index: 1, skipPlay: true} }, + { type: 'resume' } + ] + }, done) + }) + it('appendMany at index', (done) => { + const tracks = [{title: 1}, {title: 2}] + testAction({ + action: store.actions.appendMany, + payload: {tracks: tracks, index: 1}, + params: {state: {tracks: [1, 2]}}, + expectedActions: [ + { type: 'append', payload: {track: tracks[0], index: 1, skipPlay: true} }, + { type: 'append', payload: {track: tracks[1], index: 2, skipPlay: true} }, + { type: 'resume' } + ] + }, done) + }) + it('cleanTrack after current', (done) => { + testAction({ + action: store.actions.cleanTrack, + payload: 3, + params: {state: {currentIndex: 2}}, + expectedMutations: [ + { type: 'splice', payload: {start: 3, size: 1} } + ] + }, done) + }) + it('cleanTrack before current', (done) => { + testAction({ + action: store.actions.cleanTrack, + payload: 1, + params: {state: {currentIndex: 2}}, + expectedMutations: [ + { type: 'splice', payload: {start: 1, size: 1} } + ], + expectedActions: [ + { type: 'currentIndex', payload: 1 } + ] + }, done) + }) + it('cleanTrack current', (done) => { + testAction({ + action: store.actions.cleanTrack, + payload: 2, + params: {state: {currentIndex: 2}}, + expectedMutations: [ + { type: 'splice', payload: {start: 2, size: 1} } + ], + expectedActions: [ + { type: 'player/stop', payload: null, options: {root: true} }, + { type: 'currentIndex', payload: 2 } + ] + }, done) + }) + it('resume when ended', (done) => { + testAction({ + action: store.actions.resume, + params: {state: {ended: true}, rootState: {player: {errored: false}}}, + expectedActions: [ + { type: 'next' } + ] + }, done) + }) + it('resume when errored', (done) => { + testAction({ + action: store.actions.resume, + params: {state: {ended: false}, rootState: {player: {errored: true}}}, + expectedActions: [ + { type: 'next' } + ] + }, done) + }) + it('skip resume when not ended or not error', (done) => { + testAction({ + action: store.actions.resume, + params: {state: {ended: false}, rootState: {player: {errored: false}}}, + expectedActions: [] + }, done) + }) + it('previous when at beginning does nothing', (done) => { + testAction({ + action: store.actions.previous, + params: {state: {currentIndex: 0}}, + expectedActions: [] + }, done) + }) + it('previous', (done) => { + testAction({ + action: store.actions.previous, + params: {state: {currentIndex: 1}}, + expectedActions: [ + { type: 'currentIndex', payload: 0 } + ] + }, done) + }) + it('next on last track when looping on queue', (done) => { + testAction({ + action: store.actions.next, + params: {state: {tracks: [1, 2], currentIndex: 1}, rootState: {player: {looping: 2}}}, + expectedActions: [ + { type: 'currentIndex', payload: 0 } + ] + }, done) + }) + it('next track when last track', (done) => { + testAction({ + action: store.actions.next, + params: {state: {tracks: [1, 2], currentIndex: 1}, rootState: {player: {looping: 0}}}, + expectedMutations: [ + { type: 'ended', payload: true } + ] + }, done) + }) + it('next track when not last track', (done) => { + testAction({ + action: store.actions.next, + params: {state: {tracks: [1, 2], currentIndex: 0}, rootState: {player: {looping: 0}}}, + expectedActions: [ + { type: 'currentIndex', payload: 1 } + ] + }, done) + }) + it('currentIndex', (done) => { + testAction({ + action: store.actions.currentIndex, + payload: 1, + params: {state: {tracks: [1, 2], currentIndex: 0}, rootState: {radios: {running: false}}}, + expectedMutations: [ + { type: 'ended', payload: false }, + { type: 'player/currentTime', payload: 0, options: {root: true} }, + { type: 'player/playing', payload: true, options: {root: true} }, + { type: 'player/errored', payload: false, options: {root: true} }, + { type: 'currentIndex', payload: 1 } + ] + }, done) + }) + it('currentIndex with radio and many tracks remaining', (done) => { + testAction({ + action: store.actions.currentIndex, + payload: 1, + params: {state: {tracks: [1, 2, 3, 4], currentIndex: 0}, rootState: {radios: {running: true}}}, + expectedMutations: [ + { type: 'ended', payload: false }, + { type: 'player/currentTime', payload: 0, options: {root: true} }, + { type: 'player/playing', payload: true, options: {root: true} }, + { type: 'player/errored', payload: false, options: {root: true} }, + { type: 'currentIndex', payload: 1 } + ] + }, done) + }) + it('currentIndex with radio and less than two tracks remaining', (done) => { + testAction({ + action: store.actions.currentIndex, + payload: 1, + params: {state: {tracks: [1, 2, 3], currentIndex: 0}, rootState: {radios: {running: true}}}, + expectedMutations: [ + { type: 'ended', payload: false }, + { type: 'player/currentTime', payload: 0, options: {root: true} }, + { type: 'player/playing', payload: true, options: {root: true} }, + { type: 'player/errored', payload: false, options: {root: true} }, + { type: 'currentIndex', payload: 1 } + ], + expectedActions: [ + { type: 'radios/populateQueue', payload: null, options: {root: true} } + ] + }, done) + }) + it('clean', (done) => { + testAction({ + action: store.actions.clean, + expectedMutations: [ + { type: 'tracks', payload: [] }, + { type: 'ended', payload: true } + ], + expectedActions: [ + { type: 'player/stop', payload: null, options: {root: true} }, + { type: 'currentIndex', payload: -1 } + ] + }, done) + }) + it('shuffle', (done) => { + let _shuffle = sinon.stub(_, 'shuffle') + let tracks = [1, 2, 3] + let shuffledTracks = [2, 3, 1] + _shuffle.returns(shuffledTracks) + testAction({ + action: store.actions.shuffle, + params: {state: {tracks: tracks}}, + expectedMutations: [ + { type: 'tracks', payload: [] } + ], + expectedActions: [ + { type: 'appendMany', payload: {tracks: shuffledTracks} } + ] + }, done) + }) + }) +}) From e4edf55c4753aaac807316c029354532c112fce9 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 11 Jan 2018 21:35:51 +0100 Subject: [PATCH 08/10] Replaced vue-resource by axios --- api/funkwhale_api/favorites/views.py | 2 +- api/funkwhale_api/music/fake_data.py | 4 +-- api/tests/test_favorites.py | 7 ++-- front/package.json | 4 ++- front/src/components/audio/Search.vue | 17 ++------- front/src/components/auth/Settings.vue | 6 ++-- front/src/components/favorites/List.vue | 6 ++-- front/src/components/library/Album.vue | 7 ++-- front/src/components/library/Artist.vue | 7 ++-- front/src/components/library/Artists.vue | 6 ++-- front/src/components/library/Home.vue | 6 ++-- front/src/components/library/Radios.vue | 6 ++-- front/src/components/library/Track.vue | 9 +++-- .../library/import/ArtistImport.vue | 7 ++-- .../components/library/import/BatchDetail.vue | 7 ++-- .../components/library/import/BatchList.vue | 6 ++-- .../components/library/import/FileUpload.vue | 9 ++--- .../components/library/import/ImportMixin.vue | 8 ++--- .../components/library/import/TrackImport.vue | 8 ++--- .../src/components/library/radios/Builder.vue | 22 ++++++------ .../src/components/library/radios/Filter.vue | 5 +-- front/src/components/metadata/CardMixin.vue | 9 ++--- front/src/config.js | 2 +- front/src/main.js | 35 ++++++++++++------- front/src/store/auth.js | 16 +++------ front/src/store/favorites.js | 17 +++------ front/src/store/player.js | 7 ++-- front/src/store/radios.js | 13 ++----- 28 files changed, 111 insertions(+), 147 deletions(-) diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index 98c0cfc08..08ae00b68 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -43,7 +43,7 @@ class TrackFavoriteViewSet(mixins.CreateModelMixin, favorite = models.TrackFavorite.add(track=track, user=self.request.user) return favorite - @list_route(methods=['delete']) + @list_route(methods=['delete', 'post']) def remove(self, request, *args, **kwargs): try: pk = int(request.data['track']) diff --git a/api/funkwhale_api/music/fake_data.py b/api/funkwhale_api/music/fake_data.py index ef5eaf4be..892b784ca 100644 --- a/api/funkwhale_api/music/fake_data.py +++ b/api/funkwhale_api/music/fake_data.py @@ -4,7 +4,7 @@ Populates the database with fake data import random from funkwhale_api.music import models -from funkwhale_api.music.tests import factories +from funkwhale_api.music import factories def create_data(count=25): @@ -19,4 +19,4 @@ def create_data(count=25): if __name__ == '__main__': - main() + create_data() diff --git a/api/tests/test_favorites.py b/api/tests/test_favorites.py index 418166d8e..8165722ea 100644 --- a/api/tests/test_favorites.py +++ b/api/tests/test_favorites.py @@ -58,11 +58,14 @@ def test_user_can_remove_favorite_via_api(logged_in_client, factories, client): assert response.status_code == 204 assert TrackFavorite.objects.count() == 0 -def test_user_can_remove_favorite_via_api_using_track_id(factories, logged_in_client): + +@pytest.mark.parametrize('method', ['delete', 'post']) +def test_user_can_remove_favorite_via_api_using_track_id( + method, factories, logged_in_client): favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user) url = reverse('api:v1:favorites:tracks-remove') - response = logged_in_client.delete( + response = getattr(logged_in_client, method)( url, json.dumps({'track': favorite.track.pk}), content_type='application/json' ) diff --git a/front/package.json b/front/package.json index 2818757a3..aa5024d24 100644 --- a/front/package.json +++ b/front/package.json @@ -15,6 +15,7 @@ "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { + "axios": "^0.17.1", "dateformat": "^2.0.0", "js-logger": "^1.3.0", "jwt-decode": "^2.2.0", @@ -22,7 +23,6 @@ "semantic-ui-css": "^2.2.10", "vue": "^2.3.3", "vue-lazyload": "^1.1.4", - "vue-resource": "^1.3.4", "vue-router": "^2.3.1", "vue-upload-component": "^2.7.4", "vuedraggable": "^2.14.1", @@ -69,6 +69,7 @@ "karma-phantomjs-launcher": "^1.0.2", "karma-phantomjs-shim": "^1.4.0", "karma-sinon-chai": "^1.3.1", + "karma-sinon-stub-promise": "^1.0.0", "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "0.0.30", "karma-webpack": "^2.0.2", @@ -87,6 +88,7 @@ "shelljs": "^0.7.6", "sinon": "^2.1.0", "sinon-chai": "^2.8.0", + "sinon-stub-promise": "^4.0.0", "url-loader": "^0.5.8", "vue-loader": "^12.1.0", "vue-style-loader": "^3.0.1", diff --git a/front/src/components/audio/Search.vue b/front/src/components/audio/Search.vue index 2811c2b5c..bb0881862 100644 --- a/front/src/components/audio/Search.vue +++ b/front/src/components/audio/Search.vue @@ -29,13 +29,11 @@