Parcourir la source

Merge branch 'release/v0.2.7'

rigwild il y a 4 ans
Parent
commit
e46a569665
45 fichiers modifiés avec 930 ajouts et 395 suppressions
  1. 1 1
      README.md
  2. 32 0
      experimentConfig.default.js
  3. 3 5
      package.json
  4. 1 5
      server/index.js
  5. 65 0
      server/routes/experimentCollect.js
  6. 2 0
      server/routes/index.js
  7. 0 46
      server/webSocket/index.js
  8. 0 32
      server/webSocket/messageHandler.js
  9. 6 9
      src/App.vue
  10. 22 7
      src/config.utils.js
  11. 4 4
      src/functions.js
  12. 33 13
      src/main.js
  13. 1 0
      src/mixins/ExperimentBase.vue
  14. 1 5
      src/mixins/ExperimentBaseAreSameImages.vue
  15. 3 2
      src/mixins/ExperimentBaseExtracts.vue
  16. 18 0
      src/router/experiments.js
  17. 5 0
      src/router/index.js
  18. 20 38
      src/store/actions.js
  19. 1 7
      src/store/getters.js
  20. 1 0
      src/store/index.js
  21. 7 30
      src/store/mutations.js
  22. 1 5
      src/store/state.js
  23. 13 3
      src/views/Experiments/AreSameImagesRandom.vue
  24. 13 3
      src/views/Experiments/AreSameImagesReference.vue
  25. 4 6
      src/views/Experiments/AreSameImagesReferenceOneExtract.vue
  26. 170 0
      src/views/Experiments/IsImageCorrect.vue
  27. 169 0
      src/views/Experiments/IsImageCorrectOneExtract.vue
  28. 8 9
      src/views/Experiments/MatchExtractsWithReference.vue
  29. 5 5
      src/views/Experiments/PercentQualityRandom.vue
  30. 11 2
      src/views/ExperimentsList.vue
  31. 6 6
      src/views/HostConfig.vue
  32. 219 0
      src/views/LinkGenerator.vue
  33. 0 106
      test/api/databaseWebSocket.js
  34. 2 2
      test/api/dataCollect.js
  35. 39 0
      test/server/api/experimentCollect.js
  36. 2 2
      test/api/getImage.js
  37. 2 2
      test/api/getImageExtracts.js
  38. 2 2
      test/api/listScenes.js
  39. 2 2
      test/api/listScenesQualities.js
  40. 2 2
      test/api/ping.js
  41. 33 0
      test/server/database.js
  42. 0 21
      test/api/_test_functions.js
  43. 0 0
      test/utils/_test_setup_start.js
  44. 0 0
      test/utils/_test_setup_stop.js
  45. 1 13
      yarn.lock

+ 1 - 1
README.md

@@ -167,7 +167,7 @@ yarn test
 ```
 
 ## Documentation
-The `docker-compose` script will automatically build the documentation. Use the following command to build it by hand.
+The `docker-compose` script will automatically build the API documentation. Use the following command to build it by hand.
 ```sh
 yarn doc
 ```

+ 32 - 0
experimentConfig.default.js

@@ -88,5 +88,37 @@ export const experiments = {
       whitelist: ['Appart1opt02', 'EchecsBas'],
       blacklist: null
     }
+  },
+  IsImageCorrect: {
+    mixins: [mixins.ExperimentBase, mixins.ExperimentBaseExtracts],
+    defaultConfig: {
+      lockConfig: false,
+      showHoverBorder: false,
+      extractConfig: {
+        x: 2,
+        y: 1
+      }
+    },
+    scenesConfig: {},
+    availableScenes: {
+      whitelist: ['Appart1opt02', 'EchecsBas'],
+      blacklist: null
+    }
+  },
+  IsImageCorrectOneExtract: {
+    mixins: [mixins.ExperimentBase, mixins.ExperimentBaseExtracts],
+    defaultConfig: {
+      lockConfig: false,
+      showHoverBorder: false,
+      extractConfig: {
+        x: 4,
+        y: 4
+      }
+    },
+    scenesConfig: {},
+    availableScenes: {
+      whitelist: null,
+      blacklist: null
+    }
   }
 }

+ 3 - 5
package.json

@@ -1,6 +1,6 @@
 {
   "name": "expe-web",
-  "version": "0.2.4",
+  "version": "0.2.7",
   "private": true,
   "scripts": {
     "server:start": "node -r esm index.js",
@@ -10,7 +10,7 @@
     "app:build": "vue-cli-service build",
     "app:lint": "vue-cli-service lint",
     "doc": "apidoc -i server/routes -o doc",
-    "test": "node test/api/_test_setup_start.js && ava --verbose && node test/api/_test_setup_stop.js"
+    "test": "node test/utils/_test_setup_start.js && ava --verbose && node test/utils/_test_setup_stop.js"
   },
   "dependencies": {
     "@hapi/boom": "^7.4.2",
@@ -27,8 +27,7 @@
     "serve-static": "^1.13.2",
     "sharp": "^0.22.1",
     "ua-parser-js": "^0.7.19",
-    "winston": "^3.2.1",
-    "ws": "^7.0.0"
+    "winston": "^3.2.1"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "^3.7.0",
@@ -48,7 +47,6 @@
     "supertest": "^4.0.2",
     "vue": "^2.6.10",
     "vue-cli-plugin-vuetify": "^0.5.0",
-    "vue-native-websocket": "^2.0.13",
     "vue-router": "^3.0.6",
     "vue-template-compiler": "^2.6.10",
     "vuetify": "^1.5.14",

+ 1 - 5
server/index.js

@@ -11,7 +11,6 @@ import bodyParser from 'body-parser'
 import routes from './routes'
 import { errorHandler, formatLog } from './functions'
 import { apiPrefix, imageServedUrl, serverPort, serveClient, imagesPath, logger } from '../config'
-import startWebSocketServer from './webSocket'
 import connectDb from './database'
 const morgan = require('morgan')
 
@@ -56,8 +55,5 @@ export default async () => {
   await connectDb()
 
   // Start the server on the configured port
-  const server = app.listen(serverPort, () => logger.info(formatLog(`The server was started on http://localhost:${serverPort}`)))
-
-  // Start the WebSocket server on top of the started HTTP server
-  startWebSocketServer(server)
+  app.listen(serverPort, () => logger.info(formatLog(`The server was started on http://localhost:${serverPort}`)))
 }

+ 65 - 0
server/routes/experimentCollect.js

@@ -0,0 +1,65 @@
+'use strict'
+
+import express from 'express'
+import boom from '@hapi/boom'
+
+import { TEST_MODE } from '../../config'
+import DataController from '../database/controllers/Data'
+import { asyncMiddleware, checkRequiredParameters } from '../functions'
+
+const router = express.Router()
+
+/**
+ * @api {post} /experimentCollect /experimentCollect
+ * @apiVersion 0.1.11
+ * @apiName experimentCollect
+ * @apiGroup API
+ *
+ * @apiDescription Collect user's data
+ *
+ * @apiParam {String} msgId The type of message to store
+ * @apiParam {any} Any data that needs to be stored
+ *
+ * @apiExample Usage example
+ * curl -i -L -H "Content-Type: application/json" -X POST "http://diran.univ-littoral.fr/api/experimentCollect" -d {"msgId":"test","msg":{}}
+ *
+ * @apiSuccessExample {string} Success response example
+ * HTTP/1.1 204 OK /api/experimentCollect
+ *
+ * @apiError (Error 4xx) 400_[1] Missing parameter(s)
+ * @apiErrorExample {json} Missing parameter
+ * HTTP/1.1 400 Bad Request
+ * {
+ *   "message": "Missing parameter(s). Required parameters : msgId, msg."
+ * }
+ *
+ * @apiError (Error 4xx) 400_[2] Invalid query parameter
+ * @apiErrorExample {json} Invalid query parameter(s)
+ * HTTP/1.1 400 Bad Request
+ * {
+ *   "message": "Invalid body parameter(s).",
+ *   "data": [
+ *     "\"msgId\" must be a string."
+ *   ]
+ * }
+ *
+ */
+
+router.post('/', asyncMiddleware(async (req, res) => {
+  // Check the request contains all the required body parameters
+  const b = req.body
+  checkRequiredParameters(['msgId', 'msg'], b)
+
+  const { msgId, msg } = req.body
+
+  // Check there is no errors with parameters
+  if (typeof b.msgId !== 'string')
+    throw boom.badRequest('Invalid body parameter(s).', ['"msgId" must be a string.'])
+
+  // Add data to the database
+  if (!TEST_MODE) await DataController.add({ msgId, msg })
+
+  res.status(204).send()
+}))
+
+export default router

+ 2 - 0
server/routes/index.js

@@ -7,6 +7,7 @@ import getImage from './getImage'
 import getImageExtracts from './getImageExtracts'
 import ping from './ping'
 import dataCollect from './dataCollect'
+import experimentCollect from './experimentCollect'
 
 const router = express.Router()
 
@@ -15,6 +16,7 @@ router.use('/listSceneQualities', listSceneQualities)
 router.use('/getImage', getImage)
 router.use('/getImageExtracts', getImageExtracts)
 router.use('/dataCollect', dataCollect)
+router.use('/experimentCollect', experimentCollect)
 router.use('/ping', ping)
 
 export default router

+ 0 - 46
server/webSocket/index.js

@@ -1,46 +0,0 @@
-'use strict'
-
-import WebSocket from 'ws'
-import { formatLog, formatError } from '../functions'
-import { wsLogger, TEST_MODE } from '../../config'
-import messageHandler from './messageHandler'
-
-/**
- * @typedef {function} ErrorLogger
- * @param {Error} err an Error object
- */
-/**
- * Handle thrown errors
- *
- * @param {object} ws a WebSocket connected client
- * @returns {ErrorLogger} the actual error logger
- */
-export const errorHandler = ws => err => {
-  const errStr = formatError(err)
-  ws.send(JSON.stringify(errStr))
-  if (!TEST_MODE) wsLogger.error(errStr)
-}
-
-/**
- * Create the WebSocket server
- *
- * @param {*} httpServer an HTTP node object (provided by Express here)
- * @returns {void}
- */
-const createWsServer = httpServer => {
-  const wss = new WebSocket.Server({ server: httpServer })
-
-  wss.on('listening', () => wsLogger.info(formatLog('The WebSocket server was started')))
-  wss.on('error', err => wsLogger.error(formatError(err)))
-
-  wss.on('connection', ws => {
-    wsLogger.info(formatLog('New client connected.'))
-
-    ws.on('message', data => messageHandler(ws)(data).catch(err => errorHandler(ws)(err)))
-
-    ws.on('error', err => errorHandler(ws)(err))
-    ws.on('close', () => wsLogger.info(formatLog('Client disconnected.')))
-  })
-}
-
-export default createWsServer

+ 0 - 32
server/webSocket/messageHandler.js

@@ -1,32 +0,0 @@
-'use strict'
-
-import { formatLog } from '../functions'
-import { wsLogger, TEST_MODE } from '../../config'
-import DataController from '../database/controllers/Data'
-
-/**
- * @typedef {Function} MessageHandler
- * @param {string} data a message received from a client
- */
-/**
- * Treat received message from a WebSocket client
- * @param {object} ws a WebSocket connected client
- * @returns {MessageHandler} the message handler
- */
-const messageHandler = ws => async data => {
-  let json
-  try {
-    json = JSON.parse(data)
-  }
-  catch (err) {
-    throw new Error('Invalid JSON data.')
-  }
-  if (!TEST_MODE && !json.uuid)
-    throw new Error('"uuid" was not provided.')
-
-  await DataController.add(json)
-  if (!TEST_MODE) wsLogger.info(formatLog(json, 'message'))
-  ws.send('{"message":"ok"}')
-}
-
-export default messageHandler

+ 6 - 9
src/App.vue

@@ -95,16 +95,19 @@ export default {
     this.APP_LOADER()
   },
   methods: {
-    ...mapActions(['loadScenesList', 'connectToWs']),
+    ...mapActions(['loadScenesList']),
 
     // Main app function that redirect the user where he needs to be at
     async APP_LOADER() {
       if (this.isGdprValidated && this.isHostConfigured) {
-        await this.loadWebSocket()
         if (!this.areScenesLoaded) await this.loadScenes()
       }
     },
 
+    loadScenes() {
+      return this.load(this.loadScenesList, 'Loading scenes list...')
+    },
+
     async load(fn, loadingMessage) {
       try {
         this.loadingMessage = loadingMessage
@@ -118,14 +121,8 @@ export default {
       finally {
         this.loadingMessage = null
       }
-    },
-
-    loadScenes() {
-      return this.load(this.loadScenesList, 'Loading scenes list...')
-    },
-    loadWebSocket() {
-      return this.load(this.connectToWs, 'Connecting to WebSocket server...')
     }
+
   }
 }
 </script>

+ 22 - 7
src/config.utils.js

@@ -1,14 +1,31 @@
 import deepmerge from 'deepmerge'
 import store from '@/store'
+import { experiments as experimentsDEFAULT } from '@/../experimentConfig.default'
 import { experiments } from '@/../experimentConfig'
 
 // Merge a default config with a specific scene config
 const buildConfig = ({ defaultConfig = {}, scenesConfig = {} }, sceneName) =>
   deepmerge(defaultConfig, scenesConfig[sceneName] || {})
 
+// Merge multiple configs (used for multiple mixins)
 const buildMultiConfig = (confArr, sceneName) =>
   deepmerge.all(confArr.map(aConfig => buildConfig(aConfig, sceneName)))
 
+
+/**
+ * Find the configuration.
+ * Will use the default configuration if not found in the real one.
+ * @param {String} experimentName The selected experiment
+ * @returns {Object} Configuration object
+ */
+const getConfigObject = experimentName => {
+  if (experiments[experimentName])
+    return experiments
+  else if (experimentsDEFAULT[experimentName])
+    return experimentsDEFAULT
+  throw new Error(`Could not find the experiment "${experimentName}" in the config file nor the default config file.`)
+}
+
 /**
 * Build a configuration file by merging the default config with the asked scene.
 * The asked scene config will overwrite the default config.
@@ -20,13 +37,12 @@ const buildMultiConfig = (confArr, sceneName) =>
 * @returns {Object} The config for the selected experiment with the selected scene
 */
 export const getExperimentConfig = (experimentName, sceneName) => {
-  if (!experiments[experimentName])
-    throw new Error(`Could not find the experiment "${experimentName}" in the config file.`)
+  const config = getConfigObject(experimentName)
 
   // Build parent mixin config
-  const mixinConfig = buildMultiConfig(experiments[experimentName].mixins, sceneName)
+  const mixinConfig = buildMultiConfig(config[experimentName].mixins, sceneName)
   // Build global config
-  const globalConfig = buildConfig(experiments[experimentName], sceneName)
+  const globalConfig = buildConfig(config[experimentName], sceneName)
   // Merge configs
   return deepmerge(mixinConfig, globalConfig)
 }
@@ -40,12 +56,11 @@ export const getExperimentConfig = (experimentName, sceneName) => {
  * @returns {String[]} The list of available scenes for this experiment
  */
 export const getExperimentSceneList = experimentName => {
-  if (!experiments[experimentName])
-    throw new Error(`Could not find the experiment "${experimentName}" in the config file.`)
+  const config = getConfigObject(experimentName)
 
   let configuredScenesList = []
 
-  const confObj = experiments[experimentName].availableScenes
+  const confObj = config[experimentName].availableScenes
   const scenesList = store.state.scenesList
 
   // Apply whitelist

+ 4 - 4
src/functions.js

@@ -1,10 +1,11 @@
 export const API_PREFIX = '/api'
 export const API_ROUTES = {
-  ping: () => `${API_PREFIX}/ping`,
+  ping: `${API_PREFIX}/ping`,
 
-  dataCollect: () => `${API_PREFIX}/dataCollect`,
+  dataCollect: `${API_PREFIX}/dataCollect`,
+  experimentCollect: `${API_PREFIX}/experimentCollect`,
 
-  listScenes: () => `${API_PREFIX}/listScenes`,
+  listScenes: `${API_PREFIX}/listScenes`,
 
   listSceneQualities: sceneName => `${API_PREFIX}/listSceneQualities?${new URLSearchParams({ sceneName })}`,
 
@@ -23,7 +24,6 @@ export const API_ROUTES = {
 export const delay = ms => new Promise(res => setTimeout(res, ms))
 
 export const buildURI = (ssl, host, port, route = '') => `${ssl ? 'https' : 'http'}://${host}:${port}${route}`
-export const buildWsURI = (ssl, host, port) => `${ssl ? 'wss' : 'ws'}://${host}:${port}`
 
 export const sortIntArray = intArray => intArray ? intArray.sort((a, b) => a - b) : null
 

+ 33 - 13
src/main.js

@@ -3,24 +3,44 @@ import './plugins/vuetify'
 import App from './App.vue'
 import router from './router'
 import store from './store'
-import VueNativeSock from 'vue-native-websocket'
 
 Vue.config.productionTip = false
 
-// Connect the WebSocket client to the store
-Vue.use(VueNativeSock, 'ws://example.com', {
-  store,
-  connectManually: true,
-  reconnection: true,
-  reconnectionAttempts: 2,
-  reconnectionDelay: 1000
-})
-store.$socket = Vue.prototype.$socket
-
 // A function loaded before each route change
 router.beforeEach((to, from, next) => {
+  // Check if there is a special query in the URI
+  if (to.query.q) {
+    store.commit('setCustomLinkData', to.query.q)
+    // GDPR notice not approved
+    if (!store.getters.isGdprValidated) {
+      if (to.name !== 'GdprNotice')
+        return next('/gdpr')
+    }
+  }
+  if (store.getters.isGdprValidated && store.state.customLinkData) {
+    const request = JSON.parse(JSON.stringify(store.state.customLinkData)) // DEEP COPY
+    store.commit('clearCustomLinkData')
+
+    // Identify the user
+    store.dispatch('setAppUniqueId')
+
+    // Set the host configuration
+    store.commit('setHostConfig', request.hostConfig)
+
+    // Set the userId and experimentId (to explicitly identify the user)
+    store.commit('setUserExperimentId', { userId: request.userId, experimentId: request.experimentId })
+
+    // Redirect to the experiment scene selector (or directly to a scene if specified)
+    if (request.experimentName) {
+      if (request.sceneName)
+        return next(`/experiments/${request.experimentName}/${request.sceneName}`)
+      return next(`/experiments/${request.experimentName}/`)
+    }
+    return next()
+  }
+
   // Redirect from config pages if already configured
-  if (to.path === '/gdpr' && store.getters.isGdprValidated)
+  if (to.name === 'GdprNotice' && store.getters.isGdprValidated)
     return next('/hostConfig')
 
   if (to.path === '/hostConfig' && store.getters.isHostConfigured)
@@ -30,7 +50,7 @@ router.beforeEach((to, from, next) => {
   // Redirect to configuration pages
   // Check GDPR before doing anything and redirect if necessary
   if (!store.getters.isGdprValidated) {
-    if (to.path !== '/gdpr') return next('/gdpr')
+    if (to.name !== 'GdprNotice') return next('/gdpr')
     return next()
   }
 

+ 1 - 0
src/mixins/ExperimentBase.vue

@@ -114,6 +114,7 @@ export default {
           if (!res.ok) throw new Error(res.json.message + res.json.data ? `\n${res.json.data}` : '')
           return res.json
         })
+      data.link = this.getHostURI + data.link
       return data
     }
   }

+ 1 - 5
src/mixins/ExperimentBaseAreSameImages.vue

@@ -34,11 +34,7 @@ export default {
 
     // Get images links for a test
     async getTest(leftQuality, rightQuality) {
-      const res = await Promise.all([this.getImage(leftQuality), this.getImage(rightQuality)])
-      const [image1, image2] = res.map(x => {
-        x.link = `${this.getHostURI}${x.link}`
-        return x
-      })
+      const [image1, image2] = await Promise.all([this.getImage(leftQuality), this.getImage(rightQuality)])
       return { image1, image2 }
     },
 

+ 3 - 2
src/mixins/ExperimentBaseExtracts.vue

@@ -44,13 +44,14 @@ export default {
           if (!res.ok) throw new Error(res.json.message + res.json.data ? `\n${res.json.data}` : '')
           return res.json
         })
+      data.extracts = data.extracts.map(x => this.getHostURI + x)
       return data
     },
 
     // Convert a simple API extracts object to get more informations
     getExtractFullObject(extractsApiObj) {
       return extractsApiObj.extracts.map((url, i) => ({
-        link: this.getHostURI + url,
+        link: url,
         quality: extractsApiObj.info.image.quality,
         zone: i + 1,
         index: i,
@@ -109,7 +110,7 @@ export default {
 
         // Loading new extract
         const data = await this.getExtracts(newQuality)
-        this.extracts[index].link = this.getHostURI + data.extracts[index]
+        this.extracts[index].link = data.extracts[index]
         this.extracts[index].quality = data.info.image.quality
         this.extracts[index].nextQuality = findNearestUpper(data.info.image.quality, this.qualities)
         this.extracts[index].precQuality = findNearestLower(data.info.image.quality, this.qualities)

+ 18 - 0
src/router/experiments.js

@@ -43,5 +43,23 @@ export default [
     meta: {
       fullName: 'Choose a score for quality'
     }
+  },
+  {
+    path: '/experiments/IsImageCorrect/:sceneName',
+    name: 'IsImageCorrect',
+    component: () => import('@/views/Experiments/IsImageCorrect'),
+    props: true,
+    meta: {
+      fullName: 'Check if reconstructed image is correct'
+    }
+  },
+  {
+    path: '/experiments/IsImageCorrectOneExtract/:sceneName',
+    name: 'IsImageCorrectOneExtract',
+    component: () => import('@/views/Experiments/IsImageCorrectOneExtract'),
+    props: true,
+    meta: {
+      fullName: 'Check if reconstructed image with one different extract is correct'
+    }
   }
 ]

+ 5 - 0
src/router/index.js

@@ -22,6 +22,11 @@ export default new Router({
       name: 'HostConfig',
       component: HostConfig
     },
+    {
+      path: '/linkGenerator',
+      name: 'LinkGenerator',
+      component: () => import('@/views/LinkGenerator')
+    },
     {
       path: '/experiments',
       name: 'ExperimentsList',

+ 20 - 38
src/store/actions.js

@@ -1,6 +1,5 @@
-import Vue from 'vue'
 import router from '../router'
-import { API_ROUTES, buildURI, buildWsURI, delay, serialize } from '../functions'
+import { API_ROUTES, buildURI, serialize } from '../functions'
 
 export default {
   setGdprValidated({ state, commit }) {
@@ -14,19 +13,17 @@ export default {
     if (!state.uuid) commit('setAppUniqueId')
   },
 
-  resetApp({ commit, state }, { gdprConsent = false, hostConfig = false, progression = false }) {
-    if (hostConfig && state.socket.isConnected)
-      this._vm.$disconnect()
+  resetApp({ commit }, { gdprConsent = false, hostConfig = false, progression = false }) {
     commit('resetApp', { gdprConsent, hostConfig, progression })
   },
 
-  async setHostConfig({ state, commit, dispatch }, { ssl, host, port }) {
+  async setHostConfig({ commit, dispatch }, { ssl, host, port }) {
     // Timeout after 1s
     const controller = new AbortController()
     const signal = controller.signal
     setTimeout(() => controller.abort(), 1500)
 
-    const URI = buildURI(ssl, host, port, API_ROUTES.ping())
+    const URI = buildURI(ssl, host, port, API_ROUTES.ping)
     return fetch(URI, { signal })
       .then(async res => {
         if (res.status !== 200) throw new Error(`Received wrong HTTP status code : ${res.status} (Need 200).`)
@@ -34,14 +31,6 @@ export default {
         const content = await res.text()
         if (content !== 'pong') throw new Error('Received wrong web content (Need to receive "pong").')
 
-        this._vm.$connect(buildWsURI(ssl, host, port))
-
-        // $connect does not return a Promise, so we wait to know if it worked
-        await delay(300)
-        if (!state.socket.isConnected)
-          throw new Error('Could not connect to remote WebSocket server.')
-
-
         // Configuration is valid
         commit('setHostConfig', { ssl, host, port })
         router.push('/experiments')
@@ -57,23 +46,11 @@ export default {
     commit('setUserExperimentId', { userId, experimentId })
   },
 
-  async connectToWs({ state, getters }) {
-    if (state.socket.isConnected) return /*eslint-disable-line */
-    else if (getters.isHostConfigured) {
-      this._vm.$connect(getters.getHostWsURI)
-      // $connect does not return a Promise, so we wait to know if it worked
-      await delay(300)
-      if (!state.socket.isConnected)
-        throw new Error('Could not connect to remote WebSocket server.')
-    }
-    else throw new Error('Could not connect to WebSocket server. Host is not configured.')
-  },
-
   async collectUserData({ state, getters }) {
     let screen = serialize(window.screen)
     screen.orientation = serialize(window.screen.orientation)
 
-    return fetch(getters.getHostURI + API_ROUTES.dataCollect(), {
+    return fetch(getters.getHostURI + API_ROUTES.dataCollect, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json'
@@ -85,21 +62,26 @@ export default {
     })
   },
 
-  sendMessage({ state }, { msgId, msg = undefined }) {
-    Vue.prototype.$socket.send(JSON.stringify({
-      uuid: state.uuid,
-      userId: state.userId,
-      experimentId: state.experimentId,
-      msgId,
-      msg
-    }))
+  sendMessage({ state, getters: { getHostURI } }, { msgId, msg = undefined }) {
+    fetch(`${getHostURI}${API_ROUTES.experimentCollect}`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify({
+        uuid: state.uuid,
+        userId: state.userId,
+        experimentId: state.experimentId,
+        msgId,
+        msg
+      })
+    })
   },
 
   async loadScenesList({ getters: { isHostConfigured, getHostURI }, commit }) {
     if (!isHostConfigured) throw new Error('Host is not configured.')
 
-    const URI = getHostURI
-    const scenes = await fetch(`${URI}${API_ROUTES.listScenes()}`).then(res => res.json())
+    const scenes = await fetch(`${getHostURI}${API_ROUTES.listScenes}`).then(res => res.json())
     commit('setListScenes', scenes.data)
   },
 

+ 1 - 7
src/store/getters.js

@@ -1,4 +1,4 @@
-import { buildURI, buildWsURI } from '../functions'
+import { buildURI } from '../functions'
 
 export default {
   isGdprValidated(state) {
@@ -15,12 +15,6 @@ export default {
       return buildURI(state.hostConfig.ssl, state.hostConfig.host, state.hostConfig.port)
   },
 
-  getHostWsURI(state, getters) {
-    if (!state) return
-    if (getters.isHostConfigured)
-      return buildWsURI(state.hostConfig.ssl, state.hostConfig.host, state.hostConfig.port)
-  },
-
   areScenesLoaded(state) {
     if (!state) return
     return state.scenesList !== null

+ 1 - 0
src/store/index.js

@@ -12,6 +12,7 @@ const vuexLocal = new VuexPersistence({
   storage: window.localStorage,
   key: 'webexpe-state',
   reducer: state => ({
+    customLinkData: state.customLinkData,
     uuid: state.uuid,
     userId: state.userId,
     experimentId: state.experimentId,

+ 7 - 30
src/store/mutations.js

@@ -1,4 +1,3 @@
-import Vue from 'vue'
 import defaultState from '@/store/state'
 import Experiments from '@/router/experiments'
 
@@ -24,6 +23,13 @@ const createProgressionObj = (state, scenes) => {
 }
 
 export default {
+  setCustomLinkData(state, data) {
+    state.customLinkData = JSON.parse(atob(data))
+  },
+  clearCustomLinkData(state) {
+    state.customLinkData = null
+  },
+
   setGdprValidated(state) {
     state.gdprConsent = true
   },
@@ -77,34 +83,5 @@ export default {
   setExperimentDone(state, { experimentName, sceneName, done }) {
     checkProgression(state, experimentName, sceneName)
     state.progression[experimentName][sceneName].done = done
-  },
-
-  SOCKET_ONOPEN(state, event) {
-    if (event === null) return
-
-    console.info('Connected to WebSocket server')
-    Vue.prototype.$socket = event.currentTarget
-    state.socket.isConnected = true
-  },
-  SOCKET_ONCLOSE(state, _event) {
-    console.info('Disconnected from WebSocket server')
-    state.hostConfig = defaultState().hostConfig
-    state.socket.isConnected = false
-  },
-  SOCKET_ONERROR(state, event) {
-    console.error('WebSocket connection error', state, event)
-  },
-  // default handler called for all methods
-  SOCKET_ONMESSAGE(state, { data: rawMessage }) {
-    const message = JSON.parse(rawMessage)
-    state.socket.message = message
-  },
-  // mutations for reconnect methods
-  SOCKET_RECONNECT(state, count) {
-    console.info('Reconnect to WebSocket server', state, count)
-  },
-  SOCKET_RECONNECT_ERROR(state) {
-    console.error('Could not reconnect to WebSocket server')
-    state.socket.reconnectError = true
   }
 }

+ 1 - 5
src/store/state.js

@@ -11,9 +11,5 @@ export default () => JSON.parse(JSON.stringify({
   },
   scenesList: null,
   progression: {},
-  socket: {
-    isConnected: false,
-    message: '',
-    reconnectError: false
-  }
+  customLinkData: null
 }))

+ 13 - 3
src/views/Experiments/AreSameImagesRandom.vue

@@ -42,10 +42,10 @@
             <h2>Test {{ testCount }} / {{ maxTestCount }}</h2>
             <v-layout row wrap>
               <v-flex sm6 xs12>
-                <v-btn @click="areTheSameAction(false, getRandomTest)" color="error" large>Images are NOT the same</v-btn>
+                <v-btn @click="nextAction(false)" color="error" large>Images are NOT the same</v-btn>
               </v-flex>
               <v-flex sm6 xs12>
-                <v-btn @click="areTheSameAction(true, getRandomTest)" color="success" large>Images are the same</v-btn>
+                <v-btn @click="nextAction(true)" color="success" large>Images are the same</v-btn>
               </v-flex>
             </v-layout>
           </v-container>
@@ -93,6 +93,16 @@ export default {
     this.saveProgress()
   },
 
-  methods: {}
+  methods: {
+    // generate next action and save data
+    async nextAction(same) {
+      let additionalData = {
+        stepCounter: this.testCount,
+        maxStepCount: this.maxTestCount
+      }
+
+      this.areTheSameAction(same, this.getReferenceTest, additionalData)
+    }
+  }
 }
 </script>

+ 13 - 3
src/views/Experiments/AreSameImagesReference.vue

@@ -42,10 +42,10 @@
             <h2>Test {{ testCount }} / {{ maxTestCount }}</h2>
             <v-layout row wrap>
               <v-flex sm6 xs12>
-                <v-btn @click="areTheSameAction(false, getReferenceTest)" color="error" large>Images are NOT the same</v-btn>
+                <v-btn @click="nextAction(false)" color="error" large>Images are NOT the same</v-btn>
               </v-flex>
               <v-flex sm6 xs12>
-                <v-btn @click="areTheSameAction(true, getReferenceTest)" color="success" large>Images are the same</v-btn>
+                <v-btn @click="nextAction(true)" color="success" large>Images are the same</v-btn>
               </v-flex>
             </v-layout>
           </v-container>
@@ -94,6 +94,16 @@ export default {
     this.saveProgress()
   },
 
-  methods: {}
+  methods: {
+    // generate next action and save data
+    async nextAction(same) {
+      let additionalData = {
+        stepCounter: this.testCount,
+        maxStepCount: this.maxTestCount
+      }
+
+      this.areTheSameAction(same, this.getReferenceTest, additionalData)
+    }
+  }
 }
 </script>

+ 4 - 6
src/views/Experiments/AreSameImagesReferenceOneExtract.vue

@@ -129,17 +129,13 @@ export default {
       // Apply the random quality extract
       maxExtracts.extracts[randomZoneIndex] = randomExtracts.extracts[randomZoneIndex]
 
-      // Fix uris
-      const referenceWithOneExtract = maxExtracts.extracts.map(url => this.getHostURI + url)
-      maxImage.link = this.getHostURI + maxImage.link
-
       // Backup test data
       this.randomZoneIndex = randomZoneIndex
       this.randomZoneQuality = randomQuality
       this.imageOneExtractPosition = rand(0, 1) === 0 ? 'left' : 'right'
 
       return {
-        image1: referenceWithOneExtract,
+        image1: maxExtracts.extracts,
         image2: maxImage
       }
     },
@@ -149,7 +145,9 @@ export default {
         imageOneExtractPosition: this.imageOneExtractPosition,
         randomZoneIndex: this.randomZoneIndex,
         randomZone: this.randomZoneIndex + 1,
-        randomZoneQuality: this.randomZoneQuality
+        randomZoneQuality: this.randomZoneQuality,
+        stepCounter: this.testCount,
+        maxStepCount: this.maxTestCount
       }
       this.areTheSameAction(areTheSame, this.getReferenceOneExtractTest, additionalData)
     }

+ 170 - 0
src/views/Experiments/IsImageCorrect.vue

@@ -0,0 +1,170 @@
+<template>
+  <ExperimentBlock
+    :experiment-name="experimentName"
+    :scene-name="sceneName"
+    :loading-message="loadingMessage"
+    :loading-error-message="loadingErrorMessage"
+  >
+    <template v-slot:header>
+      <!-- ## Template to place in the header (example: Extract-configurator) ## -->
+    </template>
+
+    <template v-if="reconstructedImage" v-slot:content>
+      <!-- ## Actual experiment template ## -->
+
+      <!-- Image -->
+      <v-flex xs12 sm6 offset-sm3>
+        <v-card dark color="primary">
+          <v-card-text class="px-0">Reconstructed image</v-card-text>
+          <v-container class="pa-1">
+            <ExtractsToImage :extracts="reconstructedImage" :extract-config="extractConfig" />
+          </v-container>
+        </v-card>
+      </v-flex>
+      <!--/ Image -->
+
+      <!-- Experiment validation button -->
+      <v-layout justify-center align-content-center>
+        <div id="choice">
+          <v-container grid-list-md text-xs-center fluid>
+            <h2>Test {{ testCount }} / {{ maxTestCount }}</h2>
+            <v-layout row wrap>
+              <v-flex sm6 xs12>
+                <v-btn @click="nextReconstructedImage(false)" color="error" large>Images is NOT correct</v-btn>
+              </v-flex>
+              <v-flex sm6 xs12>
+                <v-btn @click="nextReconstructedImage(true)" color="success" large>Images is correct</v-btn>
+              </v-flex>
+            </v-layout>
+          </v-container>
+        </div>
+      </v-layout>
+      <!--/ Experiment validation button -->
+    </template>
+  </ExperimentBlock>
+</template>
+
+<script>
+import ExperimentBlock from '@/components/ExperimentBlock.vue'
+import ExperimentBaseExtracts from '@/mixins/ExperimentBaseExtracts'
+import ExtractsToImage from '@/components/ExperimentsComponents/ExtractsToImage'
+import { EXPERIMENT as experimentMsgId } from '@/../config.messagesId'
+import { rand } from '@/functions'
+
+export default {
+  name: 'IsImageCorrect', // experiment filename
+  components: {
+    ExperimentBlock,
+    ExtractsToImage
+  },
+  mixins: [
+    ExperimentBaseExtracts
+  ],
+  data() {
+    return {
+      experimentName: 'IsImageCorrect', // experiment filename
+      refImage: null,
+      randImage: null,
+      reconstructedImage: null,
+      selectedQuality: 50,
+      testCount: 1,
+      maxTestCount: 10
+    }
+  },
+
+  // When experiment is loaded, this function is ran
+  async mounted() {
+    // Load config and progress for this scene to local state
+    this.loadConfig()
+    this.loadProgress()
+
+    // ## Do your experiment initialization stuff here ##
+    this.loadingMessage = 'Loading experiment data...'
+    this.loadingErrorMessage = null
+    try {
+      // Load scene qualities list
+      await this.getQualitiesList()
+
+      if (!this.reconstructedImage) await this.getReconstructedImage()
+    }
+    catch (err) {
+      console.error(err)
+      this.loadingErrorMessage = err.message
+      return
+    }
+    finally {
+      this.loadingMessage = null
+    }
+    // ##/ Do your experiment initialization stuff here ##
+
+    // Save progress from local state into store
+    this.saveProgress()
+  },
+
+  // List of experiment-specific methods
+  methods: {
+    // load reconstructed image
+    async getReconstructedImage() {
+      const randomQuality = this.qualities[rand(0, this.qualities.length - 1)]
+      const maxQuality = this.qualities[this.qualities.length - 1]
+
+      // Get the reference image, extracts of reference image and random quality extracts
+      const [maxExtracts, randomExtracts, maxImage, randImage] = await Promise.all([
+        this.getExtracts('max'),
+        this.getExtracts(randomQuality),
+        this.getImage(maxQuality),
+        this.getImage(randomQuality)
+      ])
+
+      this.refImage = maxImage
+      this.randImage = randImage
+
+      // get part to keep into refImage
+      let position = rand(0, 1)
+      this.refPosition = position ? 'left' : 'right'
+
+      // construct new image with two different parts
+      maxExtracts.extracts[position] = randomExtracts.extracts[position]
+      this.reconstructedImage = maxExtracts.extracts
+    },
+
+    // get next reconstructed image
+    async nextReconstructedImage(correct) {
+      this.loadingMessage = 'Loading new test...'
+      this.loadingErrorMessage = null
+      try {
+        this.testCount++
+
+        const experimentalData = {
+          refImage: this.refImage,
+          randImage: this.randImage,
+          refPosition: this.refPosition,
+          imageCorrect: correct,
+          stepCounter: this.testCount,
+          maxStepCount: this.maxTestCount,
+          experimentName: this.experimentName,
+          sceneName: this.sceneName
+        }
+        this.sendMessage({ msgId: experimentMsgId.DATA, msg: experimentalData })
+
+        await this.getReconstructedImage()
+
+        // Experiment end
+        if (this.testCount > this.maxTestCount) return this.finishExperiment()
+      }
+      catch (err) {
+        console.error('Failed to load new test', err)
+        this.loadingErrorMessage = 'Failed to load new test. ' + err.message
+      }
+      finally {
+        this.loadingMessage = null
+        this.saveProgress()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* Experiment-specific style (CSS) */
+</style>

+ 169 - 0
src/views/Experiments/IsImageCorrectOneExtract.vue

@@ -0,0 +1,169 @@
+<template>
+  <ExperimentBlock
+    :experiment-name="experimentName"
+    :scene-name="sceneName"
+    :loading-message="loadingMessage"
+    :loading-error-message="loadingErrorMessage"
+  >
+    <template v-slot:header>
+      <!-- ## Template to place in the header (example: Extract-configurator) ## -->
+    </template>
+
+    <template v-if="reconstructedImage" v-slot:content>
+      <!-- ## Actual experiment template ## -->
+
+      <!-- Image -->
+      <v-flex xs12 sm6 offset-sm3>
+        <v-card dark color="primary">
+          <v-card-text class="px-0">Reconstructed image</v-card-text>
+          <v-container class="pa-1">
+            <ExtractsToImage :extracts="reconstructedImage" :extract-config="extractConfig" />
+          </v-container>
+        </v-card>
+      </v-flex>
+      <!--/ Image -->
+
+      <!-- Experiment validation button -->
+      <v-layout justify-center align-content-center>
+        <div id="choice">
+          <v-container grid-list-md text-xs-center fluid>
+            <h2>Test {{ testCount }} / {{ maxTestCount }}</h2>
+            <v-layout row wrap>
+              <v-flex sm6 xs12>
+                <v-btn @click="nextReconstructedImage(false)" color="error" large>Images is NOT correct</v-btn>
+              </v-flex>
+              <v-flex sm6 xs12>
+                <v-btn @click="nextReconstructedImage(true)" color="success" large>Images is correct</v-btn>
+              </v-flex>
+            </v-layout>
+          </v-container>
+        </div>
+      </v-layout>
+      <!--/ Experiment validation button -->
+    </template>
+  </ExperimentBlock>
+</template>
+
+<script>
+import ExperimentBlock from '@/components/ExperimentBlock.vue'
+import ExperimentBaseExtracts from '@/mixins/ExperimentBaseExtracts'
+import ExtractsToImage from '@/components/ExperimentsComponents/ExtractsToImage'
+import { EXPERIMENT as experimentMsgId } from '@/../config.messagesId'
+import { rand } from '@/functions'
+
+export default {
+  name: 'IsImageCorrectOneExtract', // experiment filename
+  components: {
+    ExperimentBlock,
+    ExtractsToImage
+  },
+  mixins: [
+    ExperimentBaseExtracts
+  ],
+  data() {
+    return {
+      experimentName: 'IsImageCorrectOneExtract', // experiment filename
+      refImage: null,
+      randImage: null,
+      reconstructedImage: null,
+      selectedQuality: 50,
+      testCount: 1,
+      maxTestCount: 10
+    }
+  },
+
+  // When experiment is loaded, this function is ran
+  async mounted() {
+    // Load config and progress for this scene to local state
+    this.loadConfig()
+    this.loadProgress()
+
+    // ## Do your experiment initialization stuff here ##
+    this.loadingMessage = 'Loading experiment data...'
+    this.loadingErrorMessage = null
+    try {
+      // Load scene qualities list
+      await this.getQualitiesList()
+
+      if (!this.reconstructedImage) await this.getReconstructedImage()
+    }
+    catch (err) {
+      console.error(err)
+      this.loadingErrorMessage = err.message
+      return
+    }
+    finally {
+      this.loadingMessage = null
+    }
+    // ##/ Do your experiment initialization stuff here ##
+
+    // Save progress from local state into store
+    this.saveProgress()
+  },
+
+  // List of experiment-specific methods
+  methods: {
+    // load reconstructed image
+    async getReconstructedImage() {
+      const randomQuality = this.qualities[rand(0, this.qualities.length - 1)]
+      const maxQuality = this.qualities[this.qualities.length - 1]
+
+      // Get the reference image, extracts of reference image and random quality extracts
+      const [maxExtracts, randomExtracts, maxImage, randImage] = await Promise.all([
+        this.getExtracts('max'),
+        this.getExtracts(randomQuality),
+        this.getImage(maxQuality),
+        this.getImage(randomQuality)
+      ])
+
+      this.refImage = maxImage
+      this.randImage = randImage
+
+      // get part to keep into refImage
+      let position = rand(0, randomExtracts.extracts.length)
+      this.randZonePosition = position
+
+      // construct new image with two different parts
+      maxExtracts.extracts[position] = randomExtracts.extracts[position]
+      this.reconstructedImage = maxExtracts.extracts
+    },
+    // get next reconstructed image
+    async nextReconstructedImage(correct) {
+      this.loadingMessage = 'Loading new test...'
+      this.loadingErrorMessage = null
+      try {
+        this.testCount++
+
+        const experimentalData = {
+          refImage: this.refImage,
+          randImage: this.randImage,
+          randZoneId: this.randZonePosition,
+          imageCorrect: correct,
+          stepCounter: this.testCount,
+          maxStepCount: this.maxTestCount,
+          experimentName: this.experimentName,
+          sceneName: this.sceneName
+        }
+        this.sendMessage({ msgId: experimentMsgId.DATA, msg: experimentalData })
+
+        await this.getReconstructedImage()
+
+        // Experiment end
+        if (this.testCount > this.maxTestCount) return this.finishExperiment()
+      }
+      catch (err) {
+        console.error('Failed to load new test', err)
+        this.loadingErrorMessage = 'Failed to load new test. ' + err.message
+      }
+      finally {
+        this.loadingMessage = null
+        this.saveProgress()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* Experiment-specific style (CSS) */
+</style>

+ 8 - 9
src/views/Experiments/MatchExtractsWithReference.vue

@@ -7,7 +7,12 @@
   >
     <template v-slot:header>
       <!-- Extract configuration -->
-      <extract-configuration v-if="lockConfig === false" @setExtractConfig="setExtractConfig($event, $refs.configurator)" :loading-error-message="loadingErrorMessage" ref="configurator" />
+      <extract-configuration
+        v-if="lockConfig === false"
+        @setExtractConfig="setExtractConfig($event, $refs.configurator)"
+        :loading-error-message="loadingErrorMessage"
+        ref="configurator"
+      />
       <!--/ Extract configuration -->
     </template>
 
@@ -104,11 +109,7 @@ export default {
 
     // Load scene data from the API
     await Promise.all([
-      this.getImage('max')
-        .then(res => {
-          this.referenceImage = this.getHostURI + res.link
-          this.saveProgress()
-        }),
+      this.getImage('max').then(res => (this.referenceImage = res.link)),
       this.getQualitiesList()
     ])
 
@@ -119,8 +120,6 @@ export default {
     if (this.extracts.length === 0) await this.setExtractConfig(this.extractConfig, this.$refs.configurator)
 
     this.saveProgress()
-  },
-
-  methods: {}
+  }
 }
 </script>

+ 5 - 5
src/views/Experiments/PercentQualityRandom.vue

@@ -13,7 +13,7 @@
       <!-- ## Actual experiment template ## -->
 
       <!-- Image -->
-      <v-flex xs6>
+      <v-flex xs12 sm6 offset-sm3>
         <v-card dark color="primary">
           <v-card-text class="px-0">Image 1</v-card-text>
 
@@ -26,6 +26,7 @@
           </v-img>
         </v-card>
       </v-flex>
+      <!--/ Image -->
 
       <!-- Quality Slider -->
       <v-flex xs12>
@@ -35,6 +36,7 @@
           thumb-label
         />
       </v-flex>
+      <!--/ Quality Slider -->
 
 
       <!-- Experiment validation button -->
@@ -44,7 +46,7 @@
             <h2>Test {{ testCount }} / {{ maxTestCount }}</h2>
             <v-layout row wrap>
               <v-flex sm12 xs12>
-                <v-btn @click="nextRandomImage()" color="primary" large>Validate quality</v-btn>
+                <v-btn @click="nextRandomImage" color="primary" large>Validate quality</v-btn>
               </v-flex>
             </v-layout>
           </v-container>
@@ -111,9 +113,7 @@ export default {
     // load image
     async getTest() {
       let randomQuality = this.qualities[rand(0, this.qualities.length - 1)]
-      let image = await this.getImage(randomQuality)
-      image.link = this.getHostURI + image.link
-      this.image1 = image
+      this.image1 = await this.getImage(randomQuality)
       this.selectedQuality = 50
     },
 

+ 11 - 2
src/views/ExperimentsList.vue

@@ -24,7 +24,8 @@
         <template v-slot:items="props">
           <td>{{ props.item.name }}</td>
           <td class="text-xs-center">{{ props.item.completion }}</td>
-          <td class="text-xs-center"><v-btn small dark :to="props.item.link">Start experiment</v-btn></td>
+          <td class="text-xs-center"><v-btn small dark :to="props.item.link">Start</v-btn></td>
+          <td class="text-xs-center"><v-btn small dark :to="props.item.linkRandom">Start with random scene</v-btn></td>
         </template>
         <template v-slot:no-results>
           <v-alert :value="true" color="error" icon="warning">
@@ -40,6 +41,7 @@
 import { mapState } from 'vuex'
 import Experiments from '@/router/experiments'
 import { getExperimentSceneList } from '@/config.utils'
+import { rand } from '@/functions'
 
 export default {
   name: 'ExperimentsList',
@@ -50,7 +52,8 @@ export default {
       headers: [
         { text: 'Experiment name', value: 'name' },
         { text: 'Completion', value: 'completion', align: 'center' },
-        { text: 'Start', value: 'name', sortable: false, align: 'center' }
+        { text: 'Start', value: 'start', sortable: false, align: 'center' },
+        { text: 'Start random scene', value: 'startRandom', sortable: false, align: 'center' }
       ],
       items: null
     }
@@ -71,6 +74,12 @@ export default {
         const numberOfDoneScenes = Object.keys(this.progression[expe.name]).filter(y => this.progression[expe.name][y].done).length
         const percentage = Math.round(numberOfDoneScenes / scenesList.length * 100)
         res.completion = `${numberOfDoneScenes}/${scenesList.length} - ${percentage}%`
+
+        // Get scenes that are NOT done
+        const unDoneScenes = Object.keys(this.progression[expe.name]).filter(y => scenesList.includes(y) && !this.progression[expe.name][y].done)
+        // Select a random scenes
+        const randomScene = unDoneScenes[rand(0, unDoneScenes.length - 1)]
+        res.linkRandom = `/experiments/${expe.name}/${randomScene}`
       }
       else res.completion = '0%'
 

+ 6 - 6
src/views/HostConfig.vue

@@ -123,12 +123,12 @@ export default {
   },
 
   mounted() {
-    if (process.env.NODE_ENV === 'development')
-      this.config = {
-        ssl: false,
-        host: 'localhost',
-        port: '5000'
-      }
+    // if (process.env.NODE_ENV === 'development')
+    //   this.config = {
+    //     ssl: false,
+    //     host: 'localhost',
+    //     port: '5000'
+    //   }
   },
 
   methods: {

+ 219 - 0
src/views/LinkGenerator.vue

@@ -0,0 +1,219 @@
+<template>
+  <v-layout justify-center align-center>
+    <v-flex xs12 sm8>
+      <v-card>
+        <v-container fluid fill-height>
+          <v-layout column align-center>
+            <v-card-title class="headline d-block text-md-center font-weight-bold">Link generator</v-card-title>
+
+            <v-card-text>
+              <v-form>
+                <h2>*Host configuration</h2>
+                <h4>Web application configuration</h4>
+                <v-text-field
+                  v-model="form.webAppUrl"
+                  label="*Web application link"
+                />
+
+                <h4>Server configuration</h4>
+                <v-flex xs3>
+                  <v-select
+                    v-model="form.server.ssl"
+                    :items="[false, true]"
+                    label="*SSL"
+                  />
+                </v-flex>
+
+                <v-text-field
+                  v-model="form.server.host"
+                  label="*Host IP address or hostname"
+                />
+
+                <v-text-field
+                  v-model="form.server.port"
+                  label="*Port"
+                  type="number"
+                />
+
+
+                <h2>User ID and experiment ID</h2>
+                <v-layout row wrap>
+                  <v-flex xs5>
+                    <v-checkbox
+                      v-model="form.userId.activated"
+                      color="primary"
+                      label="User ID"
+                    />
+                  </v-flex>
+                  <v-spacer />
+                  <v-flex xs6>
+                    <v-text-field
+                      v-model="form.userId.value"
+                      label="User ID"
+                      type="text"
+                      :disabled="!form.userId.activated"
+                    />
+                  </v-flex>
+                </v-layout>
+
+                <v-layout row wrap>
+                  <v-flex xs5>
+                    <v-checkbox
+                      v-model="form.experimentId.activated"
+                      color="primary"
+                      label="Experiment ID"
+                    />
+                  </v-flex>
+                  <v-spacer />
+                  <v-flex xs6>
+                    <v-text-field
+                      v-model="form.experimentId.value"
+                      label="Experiment ID"
+                      type="text"
+                      :disabled="!form.experimentId.activated"
+                    />
+                  </v-flex>
+                </v-layout>
+
+
+                <h2>Experiment name and scene name</h2>
+                <v-layout row wrap>
+                  <v-flex xs5>
+                    <v-checkbox
+                      v-model="form.experimentName.activated"
+                      color="primary"
+                      label="Experiment name"
+                    />
+                  </v-flex>
+                  <v-spacer />
+                  <v-flex xs6>
+                    <v-text-field
+                      v-model="form.experimentName.value"
+                      label="Experiment name"
+                      type="text"
+                      :disabled="!form.experimentName.activated"
+                    />
+                  </v-flex>
+                </v-layout>
+
+                <v-layout row wrap>
+                  <v-flex xs5>
+                    <v-checkbox
+                      v-model="form.sceneName.activated"
+                      color="primary"
+                      label="Scene name"
+                    />
+                  </v-flex>
+                  <v-spacer />
+                  <v-flex xs6>
+                    <v-text-field
+                      v-model="form.sceneName.value"
+                      label="Scene name"
+                      type="text"
+                      :disabled="!form.sceneName.activated"
+                    />
+                  </v-flex>
+                </v-layout>
+
+                <v-btn color="primary" @click="generateLink">Generate link</v-btn>
+
+                <div v-if="linkOutput && dataOutput">
+                  <h2 class="mt-5">Result</h2>
+                  <v-textarea
+                    :value="linkOutput"
+                    label="Your generated link"
+                    type="text"
+                    disabled
+                  />
+
+                  Data in the generated link:
+                  <pre>{{ dataOutput }}</pre>
+                </div>
+
+                <v-slide-y-transition mode="out-in">
+                  <v-alert v-if="alertMessage" :value="true" type="info" v-text="alertMessage" />
+                </v-slide-y-transition>
+              </v-form>
+            </v-card-text>
+          </v-layout>
+        </v-container>
+      </v-card>
+    </v-flex>
+  </v-layout>
+</template>
+
+<script>
+export default {
+  name: 'LinkGenerator',
+  components: {
+  },
+  data() {
+    return {
+      form: {
+        webAppUrl: 'http://diran.univ-littoral.fr',
+        server: {
+          ssl: false,
+          host: 'diran.univ-littoral.fr',
+          port: '80'
+        },
+
+        userId: {
+          activated: false,
+          value: ''
+        },
+
+        experimentId: {
+          activated: false,
+          value: ''
+        },
+
+        experimentName: {
+          activated: false,
+          value: ''
+        },
+
+        sceneName: {
+          activated: false,
+          value: ''
+        }
+      },
+
+      linkOutput: null,
+      dataOutput: null,
+      alertMessage: null
+    }
+  },
+
+  methods: {
+    generateLink() {
+      this.alertMessage = null
+
+      // Check host configuration is set
+      if (this.form.webAppUrl === '' || this.form.server.host === '' || this.form.server.port === '') {
+        this.alertMessage = 'The host configuration is required.'
+        this.linkOutput = null
+        this.dataOutput = null
+        return
+      }
+
+      // Generate the link
+      const obj = {
+        hostConfig: {
+          ssl: this.form.server.ssl,
+          host: this.form.server.host,
+          port: this.form.server.port
+        }
+      }
+      if (this.form.userId.activated && this.form.userId.value !== '')obj.userId = this.form.userId.value
+      if (this.form.experimentId.activated && this.form.experimentId.value !== '')obj.experimentId = this.form.experimentId.value
+      if (this.form.experimentName.activated && this.form.experimentName.value !== '')obj.experimentName = this.form.experimentName.value
+      if (this.form.sceneName.activated && this.form.sceneName.value !== '')obj.sceneName = this.form.sceneName.value
+
+      // eslint-disable-next-line no-div-regex
+      const q = btoa(JSON.stringify(obj)).replace(/=/g, '')
+      this.linkOutput = `${this.form.webAppUrl}/#/?q=${q}`
+      this.dataOutput = JSON.stringify(obj, null, 2)
+    }
+  }
+}
+</script>

+ 0 - 106
test/api/databaseWebSocket.js

@@ -1,106 +0,0 @@
-'use strict'
-
-import test from 'ava'
-import WebSocket from 'ws'
-import { json, getHttpServer, getWebSocketServer, connectDb } from './_test_functions'
-import DataController from '../../server/database/controllers/Data'
-
-// Database and WebSocket testing
-
-// Before all tests, connect to the database
-test.beforeEach(async t => (t.context.server = await getHttpServer()))
-
-test('Check database is working', async t => {
-  // Connect to database
-  await connectDb()
-
-  // Add the document
-  const testData = { AUTOMATED_TEST_DB: true, TEST_DATABASE_OBJ: { msg: 'add' } }
-  const doc = await DataController.add(testData)
-  t.deepEqual(doc.data, testData, json(doc))
-
-  // Find the document
-  const findDoc = await DataController.find(doc.id)
-  t.deepEqual(findDoc.data, testData, json(findDoc))
-
-  // Update the document
-  testData.TEST_DATABASE_OBJ.msg = 'updated'
-  const updateTo = { AUTOMATED_TEST_DB: true, newObject: 'test', newProperties: { test: true } }
-  const docUpdated = await DataController.update(doc.id, updateTo)
-  t.deepEqual(docUpdated.data, updateTo, json(docUpdated))
-
-  // Delete the added document
-  await t.notThrowsAsync(DataController.del(doc.id))
-})
-
-test('Check WebSocket server is working', async t => {
-  // Connect to database
-  await connectDb()
-
-  // Start the server and get its ephemeral port
-  const server = t.context.server.listen(0)
-  const { port } = server.address()
-
-  // Start the WebSocket server
-  getWebSocketServer(server)
-
-  t.timeout(15000)
-  t.plan(11)
-
-  // Start the WebSocket client
-  const ws = new WebSocket(`ws://localhost:${port}`)
-  await new Promise((resolve, reject) => {
-    let sent = 0
-    let received = 0
-    ws.on('open', async () => {
-      // Send data on connect
-      ws.send(JSON.stringify({ AUTOMATED_TEST_WS: true, TEST_OBJECT: { msg: 'open' } }))
-      t.pass()
-      sent++
-    })
-    ws.on('message', async receivedData => {
-      received++
-      if (sent === 1) {
-        // Send data on receive
-        t.is('{"message":"ok"}', receivedData, json(receivedData))
-        ws.send(JSON.stringify({ AUTOMATED_TEST_WS: true, TEST_OBJECT: { msg: 'message' } }))
-        t.pass()
-        sent++
-      }
-      else if (sent === 2) {
-        // Send invalid JSON data
-        t.is('{"message":"ok"}', receivedData, json(receivedData))
-        ws.send('Not a valid JSON string')
-        t.pass()
-        sent++
-      }
-      else if (sent === 3) {
-        // Received error from server, check it is valid JSON
-        let obj = null
-        t.notThrows(() => {
-          try {
-            obj = JSON.parse(receivedData)
-          }
-          catch (err) {
-            throw new Error('Not valid JSON')
-          }
-        })
-        t.truthy(obj, json(receivedData))
-        t.is(obj.log.error, 'Invalid JSON data.', json(obj))
-        t.truthy(obj.log.stack, json(obj))
-        t.is(sent, received)
-        resolve()
-      }
-    })
-    ws.on('error', async err => {
-      // Unknown WebSocket error
-      t.fail(json(err.message))
-      reject(err)
-    })
-  })
-
-  // Delete every collected data during test
-  const db = DataController.Model
-  const found = await db.deleteMany({ 'data.AUTOMATED_TEST_WS': true })
-  t.true(found.deletedCount >= 2)
-})

+ 2 - 2
test/api/dataCollect.js

@@ -2,8 +2,8 @@
 
 import test from 'ava'
 import request from 'supertest'
-import { apiPrefix } from '../../config'
-import { json, getHttpServer } from './_test_functions'
+import { apiPrefix } from '../../../config'
+import { json, getHttpServer } from '../../utils/_test_functions'
 
 // ROUTE /dataCollect
 

+ 39 - 0
test/server/api/experimentCollect.js

@@ -0,0 +1,39 @@
+'use strict'
+
+import test from 'ava'
+import request from 'supertest'
+import { apiPrefix } from '../../../config'
+import { json, getHttpServer } from '../../utils/_test_functions'
+
+// ROUTE /experimentCollect
+
+// Before each tests, start a server
+test.beforeEach(async t => (t.context.server = await getHttpServer()))
+
+test('POST /experimentCollect - No body', async t => {
+  const res = await request(t.context.server)
+    .post(`${apiPrefix}/experimentCollect`)
+
+  t.is(res.status, 400, json(res))
+  t.true(res.body.message.includes('Missing parameter'), json(res.body))
+  t.true(res.body.message.includes('msgId'), json(res.body))
+  t.true(res.body.message.includes('msg'), json(res.body))
+})
+
+test('POST /experimentCollect - Invalid body parameters', async t => {
+  const res = await request(t.context.server)
+    .post(`${apiPrefix}/experimentCollect`)
+    .send({ msgId: { notAstring: 'not a string' }, msg: 'Valid data' })
+
+  t.is(res.status, 400, json(res))
+  t.true(res.body.message.includes('Invalid body parameter'), json(res.body))
+  t.truthy(res.body.data.find(x => x.includes('"msgId" must be a string.')), json(res.body))
+})
+
+test('POST /experimentCollect - Valid body parameters', async t => {
+  const res = await request(t.context.server)
+    .post(`${apiPrefix}/experimentCollect`)
+    .send({ msgId: 'TEST_FEATURE', msg: { some: 'data' } })
+
+  t.is(res.status, 204)
+})

+ 2 - 2
test/api/getImage.js

@@ -2,8 +2,8 @@
 
 import test from 'ava'
 import request from 'supertest'
-import { apiPrefix, imageServedUrl } from '../../config'
-import { json, getHttpServer } from './_test_functions'
+import { apiPrefix, imageServedUrl } from '../../../config'
+import { json, getHttpServer } from '../../utils/_test_functions'
 
 // ROUTE /getImage
 

+ 2 - 2
test/api/getImageExtracts.js

@@ -5,8 +5,8 @@ import request from 'supertest'
 import sharp from 'sharp'
 import fs from 'fs-extra'
 import path from 'path'
-import { apiPrefix, imageServedUrl, imagesPath } from '../../config'
-import { json, getHttpServer } from './_test_functions'
+import { apiPrefix, imageServedUrl, imagesPath } from '../../../config'
+import { json, getHttpServer } from '../../utils/_test_functions'
 
 // ROUTE /getImageExtracts
 

+ 2 - 2
test/api/listScenes.js

@@ -2,8 +2,8 @@
 
 import test from 'ava'
 import request from 'supertest'
-import { apiPrefix } from '../../config'
-import { json, getHttpServer } from './_test_functions'
+import { apiPrefix } from '../../../config'
+import { json, getHttpServer } from '../../utils/_test_functions'
 
 // ROUTE /listScenes
 

+ 2 - 2
test/api/listScenesQualities.js

@@ -2,8 +2,8 @@
 
 import test from 'ava'
 import request from 'supertest'
-import { apiPrefix } from '../../config'
-import { json, getHttpServer } from './_test_functions'
+import { apiPrefix } from '../../../config'
+import { json, getHttpServer } from '../../utils/_test_functions'
 
 // ROUTE /listSceneQualities
 

+ 2 - 2
test/api/ping.js

@@ -2,8 +2,8 @@
 
 import test from 'ava'
 import request from 'supertest'
-import { apiPrefix } from '../../config'
-import { json, getHttpServer } from './_test_functions'
+import { apiPrefix } from '../../../config'
+import { json, getHttpServer } from '../../utils/_test_functions'
 
 // ROUTE /ping
 

+ 33 - 0
test/server/database.js

@@ -0,0 +1,33 @@
+'use strict'
+
+import test from 'ava'
+import { json, getHttpServer, connectDb } from '../utils/_test_functions'
+import DataController from '../../server/database/controllers/Data'
+
+// Database and WebSocket testing
+
+// Before all tests, connect to the database
+test.beforeEach(async t => (t.context.server = await getHttpServer()))
+
+test('Check database is working', async t => {
+  // Connect to database
+  await connectDb()
+
+  // Add the document
+  const testData = { AUTOMATED_TEST_DB: true, TEST_DATABASE_OBJ: { msg: 'add' } }
+  const doc = await DataController.add(testData)
+  t.deepEqual(doc.data, testData, json(doc))
+
+  // Find the document
+  const findDoc = await DataController.find(doc.id)
+  t.deepEqual(findDoc.data, testData, json(findDoc))
+
+  // Update the document
+  testData.TEST_DATABASE_OBJ.msg = 'updated'
+  const updateTo = { AUTOMATED_TEST_DB: true, newObject: 'test', newProperties: { test: true } }
+  const docUpdated = await DataController.update(doc.id, updateTo)
+  t.deepEqual(docUpdated.data, updateTo, json(docUpdated))
+
+  // Delete the added document
+  await t.notThrowsAsync(DataController.del(doc.id))
+})

+ 0 - 21
test/api/_test_functions.js

@@ -3,14 +3,11 @@
 import path from 'path'
 import express from 'express'
 import bodyParser from 'body-parser'
-import WebSocket from 'ws'
 import serveStatic from 'serve-static'
 import routes from '../../server/routes'
 import { apiPrefix, imageServedUrl, imagesPath } from '../../config'
 import connectDb from '../../server/database'
 import { errorHandler } from '../../server/functions'
-import { errorHandler as wsErrorHandler } from '../../server/webSocket'
-import wsMessageHandler from '../../server/webSocket/messageHandler'
 
 // Path to `test` directory
 export const testDir = path.resolve(__dirname, '..')
@@ -38,23 +35,5 @@ export const getHttpServer = () => {
   return app
 }
 
-/**
- * Open a WebSocket server on top of a HTTP server
- *
- * @param {object} httpServer a HTTP server instance (ie. Express server object)
- * @returns {object} a WebSocket server instance
- */
-export const getWebSocketServer = httpServer => {
-  const wss = new WebSocket.Server({ server: httpServer })
-  wss.on('error', err => {
-    throw err
-  })
-  wss.on('connection', ws => {
-    ws.on('message', data => wsMessageHandler(ws)(data).catch(wsErrorHandler(ws)))
-    ws.on('error', wsErrorHandler(ws))
-  })
-  return wss
-}
-
 /** Connect to the database */
 export { connectDb }

test/api/_test_setup_start.js → test/utils/_test_setup_start.js


test/api/_test_setup_stop.js → test/utils/_test_setup_stop.js


+ 1 - 13
yarn.lock

@@ -1517,7 +1517,7 @@ async-each@^1.0.1:
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
   integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
 
-async-limiter@^1.0.0, async-limiter@~1.0.0:
+async-limiter@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
   integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
@@ -9718,11 +9718,6 @@ vue-loader@^15.7.0:
     vue-hot-reload-api "^2.3.0"
     vue-style-loader "^4.1.0"
 
-vue-native-websocket@^2.0.13:
-  version "2.0.13"
-  resolved "https://registry.yarnpkg.com/vue-native-websocket/-/vue-native-websocket-2.0.13.tgz#5eaba0e7ba08749d7bff331e3290cdf5e61ca918"
-  integrity sha512-w91n76ZcvjCUzWRKX7SkVTqr9YXTxbdYQmf4RX1LvMdsM0RQvpRdWvzTWaY6kw/egsdRKVSceqDsJKbr+WTyMQ==
-
 vue-router@^3.0.6:
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.6.tgz#2e4f0f9cbb0b96d0205ab2690cfe588935136ac3"
@@ -10069,13 +10064,6 @@ ws@^6.0.0:
   dependencies:
     async-limiter "~1.0.0"
 
-ws@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.0.0.tgz#79351cbc3f784b3c20d0821baf4b4ff809ffbf51"
-  integrity sha512-cknCal4k0EAOrh1SHHPPWWh4qm93g1IuGGGwBjWkXmCG7LsDtL8w9w+YVfaF+KSVwiHQKDIMsSLBVftKf9d1pg==
-  dependencies:
-    async-limiter "^1.0.0"
-
 x-xss-protection@1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.1.0.tgz#4f1898c332deb1e7f2be1280efb3e2c53d69c1a7"